Index: trunk/parsers/wikidom/tests/hype/es.DocumentModel.test.js |
— | — | @@ -111,6 +111,7 @@ |
112 | 112 | * |
113 | 113 | * {Array} Annotated character |
114 | 114 | * {String} Character |
| 115 | + * {String} Hash |
115 | 116 | * {Object}... List of annotation object references |
116 | 117 | * |
117 | 118 | * {Object} Opening or closing structural element |
— | — | @@ -124,9 +125,9 @@ |
125 | 126 | // 1 - Plain content |
126 | 127 | 'a', |
127 | 128 | // 2 - Annotated content |
128 | | - ['b', { 'type': 'bold' }], |
| 129 | + ['b', { 'type': 'bold', 'hash': '#bold' }], |
129 | 130 | // 3 - Annotated content |
130 | | - ['c', { 'type': 'italic' }], |
| 131 | + ['c', { 'type': 'italic', 'hash': '#italic' }], |
131 | 132 | // 4 - End of paragraph |
132 | 133 | { 'type': '/paragraph' }, |
133 | 134 | // 5 - Beginning of table |
Index: trunk/parsers/wikidom/lib/hype/models/es.DocumentModel.js |
— | — | @@ -76,7 +76,7 @@ |
77 | 77 | function retain( op ) { |
78 | 78 | annotate.call( this, this.cursor + op.length ); |
79 | 79 | this.cursor += op.length; |
80 | | - }; |
| 80 | + } |
81 | 81 | |
82 | 82 | function insert( op ) { |
83 | 83 | // Splice content into document in 1024 element chunks, as to not overflow max allowed |
— | — | @@ -90,18 +90,18 @@ |
91 | 91 | } |
92 | 92 | annotate.call( this, this.cursor + op.data.length ); |
93 | 93 | this.cursor += op.data.length; |
94 | | - }; |
| 94 | + } |
95 | 95 | |
96 | 96 | function remove( op ) { |
97 | 97 | this.data.splice( this.cursor, op.data.length ); |
98 | | - }; |
| 98 | + } |
99 | 99 | |
100 | 100 | function indexOfAnnotation( character, annotation ) { |
101 | 101 | if ( $.isArray( character ) ) { |
102 | 102 | // Find the index of a comparable annotation (checking for same value, not reference) |
103 | 103 | var index; |
104 | | - for ( var i = 0; i < target.length; i++ ) { |
105 | | - if ( es.compareObjects( target[i], op.annotation ) ) { |
| 104 | + for ( var i = 0; i < character.length; i++ ) { |
| 105 | + if ( character[i].hash === op.annotation.hash ) { |
106 | 106 | return index; |
107 | 107 | } |
108 | 108 | } |
— | — | @@ -136,7 +136,7 @@ |
137 | 137 | } else { |
138 | 138 | throw 'Invalid method error. Can not operate attributes this way: ' + method; |
139 | 139 | } |
140 | | - }; |
| 140 | + } |
141 | 141 | |
142 | 142 | function annotate( to ) { |
143 | 143 | // Handle annotations |
— | — | @@ -150,6 +150,8 @@ |
151 | 151 | this.data[j] = [this.data[j], annotation]; |
152 | 152 | } |
153 | 153 | } |
| 154 | + // Rebuild annotation hash |
| 155 | + annotation.hash = es.DocumentModel.getAnnotationHash( annotation ); |
154 | 156 | } |
155 | 157 | } |
156 | 158 | if ( this.clear.length ) { |
— | — | @@ -161,6 +163,8 @@ |
162 | 164 | this.data[j].splice( index, 1 ); |
163 | 165 | } |
164 | 166 | } |
| 167 | + // Rebuild annotation hash |
| 168 | + annotation.hash = es.DocumentModel.getAnnotationHash( annotation ); |
165 | 169 | } |
166 | 170 | } |
167 | 171 | } |
— | — | @@ -177,15 +181,8 @@ |
178 | 182 | if ( op.bias === 'start' ) { |
179 | 183 | target.push( op.annotation ); |
180 | 184 | } 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 ) { |
| 185 | + var index = indexOfAnnotation( target[i], op.annotation ); |
| 186 | + if ( index === -1 ) { |
190 | 187 | throw 'Annotation stack error. Annotation is missing.'; |
191 | 188 | } |
192 | 189 | target.splice( index, 1 ); |
— | — | @@ -252,6 +249,29 @@ |
253 | 250 | }; |
254 | 251 | |
255 | 252 | /** |
| 253 | + * Generates a hash of an annotation object based on it's name and data. |
| 254 | + * |
| 255 | + * TODO: Add support for deep hashing of array and object properties of annotation data. |
| 256 | + * |
| 257 | + * @static |
| 258 | + * @method |
| 259 | + * @param {Object} annotation Annotation object to generate hash for |
| 260 | + * @returns {String} Hash of annotation |
| 261 | + */ |
| 262 | +es.DocumentModel.getAnnotationHash = function( annotation ) { |
| 263 | + var hash = '#' + annotation.type; |
| 264 | + if ( annotation.data ) { |
| 265 | + var keys = []; |
| 266 | + for ( var key in annotation.data ) { |
| 267 | + keys.push( key + ':' + annotation.data ); |
| 268 | + } |
| 269 | + keys.sort(); |
| 270 | + hash += '|' + keys.join( '|' ); |
| 271 | + } |
| 272 | + return hash; |
| 273 | +}; |
| 274 | + |
| 275 | +/** |
256 | 276 | * Creates an es.ContentModel object from a plain content object. |
257 | 277 | * |
258 | 278 | * A plain content object contains plain text and a series of annotations to be applied to ranges of |
— | — | @@ -288,12 +308,15 @@ |
289 | 309 | var data = obj.text.split(''); |
290 | 310 | // Render annotations |
291 | 311 | if ( $.isArray( obj.annotations ) ) { |
292 | | - $.each( obj.annotations, function( i, src ) { |
| 312 | + for ( var i = 0, length = obj.annotations.length; i < length; i++ ) { |
| 313 | + var src = obj.annotations[i]; |
293 | 314 | // Build simplified annotation object |
294 | 315 | var dst = { 'type': src.type }; |
295 | 316 | if ( 'data' in src ) { |
296 | 317 | dst.data = es.copyObject( src.data ); |
297 | 318 | } |
| 319 | + // Add a hash to the annotation for faster comparison |
| 320 | + dst.hash = es.DocumentModel.getAnnotationHash( dst ); |
298 | 321 | // Apply annotation to range |
299 | 322 | if ( src.start < 0 ) { |
300 | 323 | // TODO: The start can not be lower than 0! Throw error? |
— | — | @@ -305,13 +328,13 @@ |
306 | 329 | // Clamp end value |
307 | 330 | src.end = data.length; |
308 | 331 | } |
309 | | - for ( var i = src.start; i < src.end; i++ ) { |
| 332 | + for ( var j = src.start; j < src.end; j++ ) { |
310 | 333 | // Auto-convert to array |
311 | | - typeof data[i] === 'string' && ( data[i] = [data[i]] ); |
| 334 | + typeof data[j] === 'string' && ( data[j] = [data[j]] ); |
312 | 335 | // Append |
313 | | - data[i].push( dst ); |
| 336 | + data[j].push( dst ); |
314 | 337 | } |
315 | | - } ); |
| 338 | + } |
316 | 339 | } |
317 | 340 | return data; |
318 | 341 | } |
— | — | @@ -488,7 +511,31 @@ |
489 | 512 | * @returns {es.Transaction} |
490 | 513 | */ |
491 | 514 | es.DocumentModel.prototype.prepareInsertion = function( offset, data ) { |
492 | | - // |
| 515 | + /* |
| 516 | + * There are 2 basic types of locations the insertion point can be: |
| 517 | + * Structural locations |
| 518 | + * |<p>a</p><p>b</p> - Beginning of the document |
| 519 | + * <p>a</p>|<p>b</p> - Between elements (like in a document or list) |
| 520 | + * <p>a</p><p>b</p>| - End of the document |
| 521 | + * Content locations |
| 522 | + * <p>|a</p><p>b</p> - Inside an element (like in a paragraph or listItem) |
| 523 | + * |
| 524 | + * if ( Incoming data contains structural elements ) { |
| 525 | + * if ( Insertion point is a structural location ) { |
| 526 | + * if ( Incoming data is not a complete structural element ) { |
| 527 | + * Incoming data must be balanced |
| 528 | + * } |
| 529 | + * } else { |
| 530 | + * Closing and opening elements for insertion point must be added to incoming data |
| 531 | + * } |
| 532 | + * } else { |
| 533 | + * if ( Insertion point is a structural location ) { |
| 534 | + * Incoming data must be balanced |
| 535 | + * } else { |
| 536 | + * Content being inserted into content is OK, do nothing |
| 537 | + * } |
| 538 | + * } |
| 539 | + */ |
493 | 540 | }; |
494 | 541 | |
495 | 542 | /** |