Index: trunk/parsers/wikidom/tests/hype/es.DocumentModel.test.js |
— | — | @@ -499,7 +499,7 @@ |
500 | 500 | ); |
501 | 501 | } ); |
502 | 502 | |
503 | | -test( 'es.DocumentModel.commit, es.DocumentModel.rollback', 8, function() { |
| 503 | +test( 'es.DocumentModel.commit, es.DocumentModel.rollback', 10, function() { |
504 | 504 | var documentModel = es.DocumentModel.newFromPlainObject( obj ); |
505 | 505 | |
506 | 506 | var elementAttributeChange = documentModel.prepareElementAttributeChange( |
— | — | @@ -584,6 +584,18 @@ |
585 | 585 | ); |
586 | 586 | |
587 | 587 | // Test 6 |
| 588 | + deepEqual( |
| 589 | + documentModel[0].getContent(), |
| 590 | + [ |
| 591 | + 'a', |
| 592 | + ['b', { 'type': 'bold', 'hash': '#bold' }], |
| 593 | + ['c', { 'type': 'italic', 'hash': '#italic' }], |
| 594 | + 'd' |
| 595 | + ], |
| 596 | + 'commit keeps model tree up to date' |
| 597 | + ); |
| 598 | + |
| 599 | + // Test 7 |
588 | 600 | documentModel.rollback( insertion ); |
589 | 601 | deepEqual( |
590 | 602 | documentModel.getData( new es.Range( 0, 5 ) ), |
— | — | @@ -597,9 +609,20 @@ |
598 | 610 | 'rollback reverses the effect of an insertion transaction on the content' |
599 | 611 | ); |
600 | 612 | |
| 613 | + // Test 8 |
| 614 | + deepEqual( |
| 615 | + documentModel[0].getContent(), |
| 616 | + [ |
| 617 | + 'a', |
| 618 | + ['b', { 'type': 'bold', 'hash': '#bold' }], |
| 619 | + ['c', { 'type': 'italic', 'hash': '#italic' }] |
| 620 | + ], |
| 621 | + 'rollback keeps model tree up to date' |
| 622 | + ); |
| 623 | + |
601 | 624 | var removal = documentModel.prepareRemoval( new es.Range( 2, 4 ) ); |
602 | 625 | |
603 | | - // Test 7 |
| 626 | + // Test 9 |
604 | 627 | documentModel.commit( removal ); |
605 | 628 | deepEqual( |
606 | 629 | documentModel.getData( new es.Range( 0, 3 ) ), |
— | — | @@ -611,7 +634,7 @@ |
612 | 635 | 'commit applies a removal transaction to the content' |
613 | 636 | ); |
614 | 637 | |
615 | | - // Test 8 |
| 638 | + // Test 10 |
616 | 639 | documentModel.rollback( removal ); |
617 | 640 | deepEqual( |
618 | 641 | documentModel.getData( new es.Range( 0, 5 ) ), |
Index: trunk/parsers/wikidom/lib/hype/models/es.DocumentModel.js |
— | — | @@ -16,10 +16,13 @@ |
17 | 17 | // Properties |
18 | 18 | node.data = $.isArray( data ) ? data : []; |
19 | 19 | node.attributes = $.isPlainObject( attributes ) ? attributes : {}; |
20 | | - |
21 | | - // Auto-generate tree |
22 | | - this.growNodeTreeFromData( node, node.data ); |
23 | | - |
| 20 | + |
| 21 | + // Auto-generate model tree |
| 22 | + var nodes = es.DocumentModel.createNodesFromData( node.data ); |
| 23 | + for ( var i = 0; i < nodes.length; i++ ) { |
| 24 | + node.push( nodes[i] ); |
| 25 | + } |
| 26 | + |
24 | 27 | return node; |
25 | 28 | }; |
26 | 29 | |
— | — | @@ -36,19 +39,86 @@ |
37 | 40 | * Each function is called in the context of a state, and takes an operation object as a parameter. |
38 | 41 | */ |
39 | 42 | es.DocumentModel.operations = ( function() { |
| 43 | + function containsElements( op ) { |
| 44 | + for ( i = 0; i < op.data.length; i++ ) { |
| 45 | + if ( op.data.type !== undefined ) { |
| 46 | + return true; |
| 47 | + } |
| 48 | + } |
| 49 | + return false; |
| 50 | + } |
| 51 | + |
| 52 | + function isStructural( offset ) { |
| 53 | + var edge = offset === 0 || offset === this.data.length - 1, |
| 54 | + elementLeft = this.data[offset - 1].type !== undefined, |
| 55 | + elementRight = this.data[offset].type !== undefined; |
| 56 | + if ( edge || ( elementLeft && elementRight ) ) { |
| 57 | + return true; |
| 58 | + } |
| 59 | + return false; |
| 60 | + } |
| 61 | + |
40 | 62 | function retain( op ) { |
41 | 63 | annotate.call( this, this.cursor + op.length ); |
42 | 64 | this.cursor += op.length; |
43 | 65 | } |
44 | | - |
| 66 | + |
45 | 67 | function insert( op ) { |
46 | | - es.spliceArray( this.data, this.cursor, op.data ); |
47 | | - annotate.call( this, this.cursor + op.data.length ); |
| 68 | + if ( isStructural( this.cursor ) ) { |
| 69 | + // TODO: Support tree updates when inserting between elements |
| 70 | + } else { |
| 71 | + // Get the node we are about to insert into |
| 72 | + var node = this.tree.getNodeFromOffset( this.cursor ); |
| 73 | + if ( containsElements( op ) ) { |
| 74 | + var nodeParent = node.getParent(); |
| 75 | + if ( !nodeParent ) { |
| 76 | + throw 'Missing parent error. Node does not have a parent node.'; |
| 77 | + } |
| 78 | + var offset = this.tree.getOffsetFromNode( node ), |
| 79 | + length = node.getElementLength() + op.data.length, |
| 80 | + index = nodeParent.indexOf( node ); |
| 81 | + if ( index === -1 ) { |
| 82 | + throw 'Missing child error. Node could not be found in it\'s parent node.'; |
| 83 | + } |
| 84 | + // Remove the node we are about to insert into from the model tree |
| 85 | + nodeParent.splice( index, 1 ); |
| 86 | + // Perform insert on linear data model |
| 87 | + es.spliceArray( this.data, this.cursor, op.data ); |
| 88 | + annotate.call( this, this.cursor + op.data.length ); |
| 89 | + // Regenerate nodes for the data we've affected |
| 90 | + var nodes = es.DocumentModel.createNodesFromData( |
| 91 | + this.data.slice( offset, length ) |
| 92 | + ); |
| 93 | + // Insert new elements into the tree where the old one used to be |
| 94 | + for ( var i = nodes.length; i >= 0; i-- ) { |
| 95 | + this.tree.splice( index, nodes[i] ); |
| 96 | + } |
| 97 | + } else { |
| 98 | + // Perform insert on linear data model |
| 99 | + es.spliceArray( this.data, this.cursor, op.data ); |
| 100 | + annotate.call( this, this.cursor + op.data.length ); |
| 101 | + // Update model tree |
| 102 | + node.adjustContentLength( op.data.length ); |
| 103 | + } |
| 104 | + } |
48 | 105 | this.cursor += op.data.length; |
49 | 106 | } |
50 | 107 | |
51 | 108 | function remove( op ) { |
52 | | - this.data.splice( this.cursor, op.data.length ); |
| 109 | + if ( isStructural( this.cursor ) && isStructural( this.cursor + op.data.length ) ) { |
| 110 | + // TODO: Support tree updates when removing whole elements |
| 111 | + } else { |
| 112 | + if ( containsElements( op ) ) { |
| 113 | + // TODO: Support tree updates when removing partial elements |
| 114 | + } else { |
| 115 | + // Get the node we are removing content from |
| 116 | + var node = this.tree.getNodeFromOffset( this.cursor ); |
| 117 | + // Update model tree |
| 118 | + node.adjustContentLength( -op.data.length ); |
| 119 | + // Remove content from linear data model |
| 120 | + this.data.splice( this.cursor, op.data.length ); |
| 121 | + } |
| 122 | + } |
53 | 123 | } |
54 | 124 | |
55 | 125 | function attribute( op, invert ) { |
— | — | @@ -183,6 +253,56 @@ |
184 | 254 | |
185 | 255 | /* Static Methods */ |
186 | 256 | |
| 257 | +/* |
| 258 | + * Create child nodes from an array of data. |
| 259 | + * |
| 260 | + * These child nodes are used for the model tree, which is a space partitioning data structure in |
| 261 | + * which each node contains the length of itself (1 for opening, 1 for closing) and the lengths of |
| 262 | + * it's child nodes. |
| 263 | + */ |
| 264 | +es.DocumentModel.createNodesFromData = function( data ) { |
| 265 | + var currentNode = new es.DocumentModelNode(); |
| 266 | + for ( var i = 0, length = data.length; i < length; i++ ) { |
| 267 | + if ( data[i].type !== undefined ) { |
| 268 | + // It's an element, figure out it's type |
| 269 | + var element = data[i], |
| 270 | + type = element.type, |
| 271 | + open = type[0] !== '/'; |
| 272 | + // Trim the "/" off the beginning of closing tag types |
| 273 | + if ( !open ) { |
| 274 | + type = type.substr( 1 ); |
| 275 | + } |
| 276 | + if ( open ) { |
| 277 | + // Validate the element type |
| 278 | + if ( !( type in es.DocumentModel.nodeModels ) ) { |
| 279 | + throw 'Unsuported element error. No class registered for element type: ' + type; |
| 280 | + } |
| 281 | + // Create a model node for the element |
| 282 | + var newNode = new es.DocumentModel.nodeModels[element.type]( element ); |
| 283 | + // Add the new model node as a child |
| 284 | + currentNode.push( newNode ); |
| 285 | + // Descend into the new model node |
| 286 | + currentNode = newNode; |
| 287 | + } else { |
| 288 | + // Return to the parent node |
| 289 | + currentNode = currentNode.getParent(); |
| 290 | + } |
| 291 | + } else { |
| 292 | + // It's content, let's start tracking the length |
| 293 | + var start = i; |
| 294 | + // Move forward to the next object, tracking the length as we go |
| 295 | + while ( data[i].type === undefined && i < length ) { |
| 296 | + i++; |
| 297 | + } |
| 298 | + // Now we know how long the current node is |
| 299 | + currentNode.setContentLength( i - start ); |
| 300 | + // The while loop left us 1 element to far |
| 301 | + i--; |
| 302 | + } |
| 303 | + } |
| 304 | + return currentNode.slice(); |
| 305 | +}; |
| 306 | + |
187 | 307 | /** |
188 | 308 | * Creates a document model from a plain object. |
189 | 309 | * |
— | — | @@ -407,54 +527,6 @@ |
408 | 528 | return deep ? es.copyArray( data ) : data; |
409 | 529 | }; |
410 | 530 | |
411 | | -/* |
412 | | - * Grow child nodes onto a root node from an array of data. |
413 | | - * |
414 | | - * A model tree is a space partitioning data structure in which each node contains the length of |
415 | | - * itself (1 for opening, 1 for closing) and the lengths of it's child nodes. |
416 | | - */ |
417 | | -es.DocumentModel.prototype.growNodeTreeFromData = function( root, data ) { |
418 | | - var currentNode = root; |
419 | | - for ( var i = 0, length = data.length; i < length; i++ ) { |
420 | | - if ( data[i].type !== undefined ) { |
421 | | - // It's an element, figure out it's type |
422 | | - var element = data[i], |
423 | | - type = element.type, |
424 | | - open = type[0] !== '/'; |
425 | | - // Trim the "/" off the beginning of closing tag types |
426 | | - if ( !open ) { |
427 | | - type = type.substr( 1 ); |
428 | | - } |
429 | | - if ( open ) { |
430 | | - // Validate the element type |
431 | | - if ( !( type in es.DocumentModel.nodeModels ) ) { |
432 | | - throw 'Unsuported element error. No class registered for element type: ' + type; |
433 | | - } |
434 | | - // Create a model node for the element |
435 | | - var newNode = new es.DocumentModel.nodeModels[element.type]( element ); |
436 | | - // Add the new model node as a child |
437 | | - currentNode.push( newNode ); |
438 | | - // Descend into the new model node |
439 | | - currentNode = newNode; |
440 | | - } else { |
441 | | - // Return to the parent node |
442 | | - currentNode = currentNode.getParent(); |
443 | | - } |
444 | | - } else { |
445 | | - // It's content, let's start tracking the length |
446 | | - var start = i; |
447 | | - // Move forward to the next object, tracking the length as we go |
448 | | - while ( data[i].type === undefined && i < length ) { |
449 | | - i++; |
450 | | - } |
451 | | - // Now we know how long the current node is |
452 | | - currentNode.setContentLength( i - start ); |
453 | | - // The while loop left us 1 element to far |
454 | | - i--; |
455 | | - } |
456 | | - } |
457 | | -}; |
458 | | - |
459 | 531 | /** |
460 | 532 | * Gets the content offset of a node. |
461 | 533 | * |
— | — | @@ -586,7 +658,8 @@ |
587 | 659 | /*function isStructuralData( data ) { |
588 | 660 | return data.length >= 2 && |
589 | 661 | data[0].type !== undefined && data[0].type.charAt( 0 ) != '/' && |
590 | | - data[data.length - 1].type !== undefined && data[data.length - 1].type.charAt( 0 ) == '/'; |
| 662 | + data[data.length - 1].type !== |
| 663 | + undefined && data[data.length - 1].type.charAt( 0 ) == '/'; |
591 | 664 | }*/ |
592 | 665 | function isStructuralData( data ) { |
593 | 666 | var i; |
— | — | @@ -602,7 +675,8 @@ |
603 | 676 | // The following are structural locations: |
604 | 677 | // * The beginning of the document (offset 0) |
605 | 678 | // * The end of the document (offset length-1) |
606 | | - // * Any location between elements, i.e. the item before is a closing and the item after is an opening |
| 679 | + // * Any location between elements, i.e. the item before is a closing and the item after is |
| 680 | + // * an opening |
607 | 681 | return offset <= 0 || offset >= data.length - 1 || ( |
608 | 682 | data[offset - 1].type !== undefined && data[offset - 1].type.charAt( 0 ) == '/' && |
609 | 683 | data[offset].type !== undefined && data[offset].type.charAt( 0 ) != '/' |
— | — | @@ -611,7 +685,8 @@ |
612 | 686 | |
613 | 687 | /** |
614 | 688 | * Balances mismatched openings/closings in data |
615 | | - * @return data itself if nothing was changed, or a clone of data with balancing changes made. data itself is never touched |
| 689 | + * @return data itself if nothing was changed, or a clone of data with balancing changes made. |
| 690 | + * data itself is never touched |
616 | 691 | */ |
617 | 692 | function balance( data ) { |
618 | 693 | var i, stack = [], element, workingData = null; |
— | — | @@ -638,7 +713,8 @@ |
639 | 714 | // Closing doesn't match what's expected |
640 | 715 | // This means the input is malformed and cannot possibly |
641 | 716 | // have been a fragment taken from well-formed data |
642 | | - throw 'Input is malformed: expected /' + element + ' but got ' + data[i].type + ' at index ' + i; |
| 717 | + throw 'Input is malformed: expected /' + element + ' but got ' + data[i].type + |
| 718 | + ' at index ' + i; |
643 | 719 | } |
644 | 720 | } |
645 | 721 | } |