Index: trunk/parsers/wikidom/lib/hype/models/es.DocumentModel.js |
— | — | @@ -67,6 +67,168 @@ |
68 | 68 | */ |
69 | 69 | es.DocumentModel.nodeModels = {}; |
70 | 70 | |
| 71 | +/** |
| 72 | + * Mapping of operation types to pure functions. |
| 73 | + * |
| 74 | + * Each function is called in the context of a state, and takes an operation object as a parameter. |
| 75 | + */ |
| 76 | +es.DocumentModel.operations = ( function() { |
| 77 | + function retain( op ) { |
| 78 | + annotate.call( this, this.cursor + op.length ); |
| 79 | + this.cursor += op.length; |
| 80 | + }; |
| 81 | + |
| 82 | + function insert( op ) { |
| 83 | + // Splice content into document in 1024 element chunks, as to not overflow max allowed |
| 84 | + // arguments, which apply is limited by |
| 85 | + var index = 0; |
| 86 | + while ( index < op.data.length ) { |
| 87 | + this.data.splice.apply( |
| 88 | + this.data, [this.cursor, 0].concat( op.data.slice( index, index + 1024 ) ) |
| 89 | + ); |
| 90 | + index += 1024; |
| 91 | + } |
| 92 | + annotate.call( this, this.cursor + op.data.length ); |
| 93 | + this.cursor += op.data.length; |
| 94 | + }; |
| 95 | + |
| 96 | + function remove( op ) { |
| 97 | + this.data.splice( this.cursor, op.data.length ); |
| 98 | + }; |
| 99 | + |
| 100 | + function indexOfAnnotation( character, annotation ) { |
| 101 | + if ( $.isArray( character ) ) { |
| 102 | + // Find the index of a comparable annotation (checking for same value, not reference) |
| 103 | + var index; |
| 104 | + for ( var i = 0; i < target.length; i++ ) { |
| 105 | + if ( es.compareObjects( target[i], op.annotation ) ) { |
| 106 | + return index; |
| 107 | + } |
| 108 | + } |
| 109 | + } |
| 110 | + return -1; |
| 111 | + } |
| 112 | + |
| 113 | + function attribute( op, invert ) { |
| 114 | + var element = this.data[this.cursor]; |
| 115 | + if ( element.type === undefined ) { |
| 116 | + throw 'Invalid element error. Can not set attributes on non-element data.'; |
| 117 | + } |
| 118 | + if ( op.method === 'set' || ( op.method === 'clear' && invert ) ) { |
| 119 | + // Automatically initialize attributes object |
| 120 | + if ( !element.attributes ) { |
| 121 | + element.attributes = {}; |
| 122 | + } |
| 123 | + element.attributes[op.name] = op.value; |
| 124 | + } else if ( op.method === 'clear' || ( op.method === 'set' && invert ) ) { |
| 125 | + if ( element.attributes ) { |
| 126 | + delete element.attributes[op.name]; |
| 127 | + } |
| 128 | + // Automatically clean up attributes object |
| 129 | + var empty = true; |
| 130 | + for ( key in element.attributes ) { |
| 131 | + empty = false; |
| 132 | + break; |
| 133 | + } |
| 134 | + if ( empty ) { |
| 135 | + delete element.attributes; |
| 136 | + } |
| 137 | + } else { |
| 138 | + throw 'Invalid method error. Can not operate attributes this way: ' + method; |
| 139 | + } |
| 140 | + }; |
| 141 | + |
| 142 | + function annotate( to ) { |
| 143 | + // Handle annotations |
| 144 | + if ( this.set.length ) { |
| 145 | + for ( var i = 0, length = this.set.length; i < length; i++ ) { |
| 146 | + var annotation = this.set[i]; |
| 147 | + for ( var j = this.cursor; j < to; j++ ) { |
| 148 | + if ( $.isArray( this.data[j] ) ) { |
| 149 | + this.data[j].push( annotation ); |
| 150 | + } else { |
| 151 | + this.data[j] = [this.data[j], annotation]; |
| 152 | + } |
| 153 | + } |
| 154 | + } |
| 155 | + } |
| 156 | + if ( this.clear.length ) { |
| 157 | + for ( var i = 0, length = this.clear.length; i < length; i++ ) { |
| 158 | + var annotation = this.clear[i]; |
| 159 | + for ( var j = this.cursor; j < to; j++ ) { |
| 160 | + var index = indexOfAnnotation( this.data[j], annotation ); |
| 161 | + if ( index !== -1 ) { |
| 162 | + this.data[j].splice( index, 1 ); |
| 163 | + } |
| 164 | + } |
| 165 | + } |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + function mark( op, invert ) { |
| 170 | + var target; |
| 171 | + if ( op.method === 'set' || ( op.method === 'clear' && invert ) ) { |
| 172 | + target = this.set; |
| 173 | + } else if ( op.method === 'clear' || ( op.method === 'set' && invert ) ) { |
| 174 | + target = this.clear; |
| 175 | + } else { |
| 176 | + throw 'Invalid method error. Can not operate attributes this way: ' + method; |
| 177 | + } |
| 178 | + if ( op.bias === 'start' ) { |
| 179 | + target.push( op.annotation ); |
| 180 | + } else if ( op.bias === 'end' ) { |
| 181 | + // Find the index of a comparable annotation (checking for same value, not reference) |
| 182 | + var index; |
| 183 | + for ( var i = 0; i < target.length; i++ ) { |
| 184 | + if ( es.compareObjects( target[i], op.annotation ) ) { |
| 185 | + index = i; |
| 186 | + break; |
| 187 | + } |
| 188 | + } |
| 189 | + if ( index === undefined ) { |
| 190 | + throw 'Annotation stack error. Annotation is missing.'; |
| 191 | + } |
| 192 | + target.splice( index, 1 ); |
| 193 | + } |
| 194 | + }; |
| 195 | + |
| 196 | + return { |
| 197 | + // Retain |
| 198 | + 'retain': { |
| 199 | + 'commit': retain, |
| 200 | + 'rollback': retain |
| 201 | + }, |
| 202 | + // Insert |
| 203 | + 'insert': { |
| 204 | + 'commit': insert, |
| 205 | + 'rollback': remove |
| 206 | + }, |
| 207 | + // Remove |
| 208 | + 'remove': { |
| 209 | + 'commit': remove, |
| 210 | + 'rollback': insert |
| 211 | + }, |
| 212 | + // Change element attributes |
| 213 | + 'attribute': { |
| 214 | + 'commit': function( op ) { |
| 215 | + attribute( op, false ); |
| 216 | + }, |
| 217 | + 'rollback': function( op ) { |
| 218 | + attribute( op, true ); |
| 219 | + } |
| 220 | + }, |
| 221 | + // Change content annotations |
| 222 | + 'annotate': { |
| 223 | + 'commit': function( op ) { |
| 224 | + mark( op, false ); |
| 225 | + }, |
| 226 | + 'rollback': function( op ) { |
| 227 | + mark( op, true ); |
| 228 | + } |
| 229 | + } |
| 230 | + }; |
| 231 | +} )(); |
| 232 | + |
71 | 233 | /* Static Methods */ |
72 | 234 | |
73 | 235 | /** |
— | — | @@ -367,7 +529,20 @@ |
368 | 530 | * @param {es.Transaction} |
369 | 531 | */ |
370 | 532 | es.DocumentModel.prototype.commit = function( transaction ) { |
371 | | - // |
| 533 | + var state = { |
| 534 | + 'data': this.data, |
| 535 | + 'cursor': 0, |
| 536 | + 'set': [], |
| 537 | + 'clear': [] |
| 538 | + }; |
| 539 | + for ( var i = 0, length = this.operations.length; i < length; i++ ) { |
| 540 | + var op = this.operations[i]; |
| 541 | + if ( op.type in this.operations ) { |
| 542 | + this.operations[op.type].commit.call( state, op ); |
| 543 | + } else { |
| 544 | + throw 'Invalid operation error. Operation type is not supported: ' + op.type; |
| 545 | + } |
| 546 | + } |
372 | 547 | }; |
373 | 548 | |
374 | 549 | /** |
— | — | @@ -377,7 +552,20 @@ |
378 | 553 | * @param {es.Transaction} |
379 | 554 | */ |
380 | 555 | es.DocumentModel.prototype.rollback = function( transaction ) { |
381 | | - // |
| 556 | + var state = { |
| 557 | + 'data': this.data, |
| 558 | + 'cursor': 0, |
| 559 | + 'set': [], |
| 560 | + 'clear': [] |
| 561 | + }; |
| 562 | + for ( var i = 0, length = this.operations.length; i < length; i++ ) { |
| 563 | + var op = this.operations[i]; |
| 564 | + if ( op.type in this.operations ) { |
| 565 | + this.operations[op.type].rollback.call( state, op ); |
| 566 | + } else { |
| 567 | + throw 'Invalid operation error. Operation type is not supported: ' + op.type; |
| 568 | + } |
| 569 | + } |
382 | 570 | }; |
383 | 571 | |
384 | 572 | /* Inheritance */ |