mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-30 09:04:21 +00:00
7465b670e1
This has some TODOs still but I want to land it now anyway, and fix the TODOs later. * Add this.offsetMap which maps each linear model offset to a model tree node * Refactor createNodesFromData() ** Rename it to buildSubtreeFromData() ** Have it build an offset map as well as a node subtree ** Have it set the root on the fake root node so that when the subtree is attached to the main tree later, we don't get a rippling root update all the way down ** Normalize the way the loop processes content, that way adding offsets for content is easier * Add rebuildNodes() which uses buildSubtreeFromData() to rebuild stuff * Use rebuildNodes() in DocumentSynchronizer * Use pushRebuild() in TransactionProcessor * Optimize setRoot() for the case where the root is already set correctly Change-Id: I8b827d0823c969e671615ddd06e5f1bd70e9d54c
991 lines
33 KiB
JavaScript
991 lines
33 KiB
JavaScript
module( 've/dm' );
|
|
|
|
test( 've.dm.DocumentNode.getData', 1, function() {
|
|
var documentModel = ve.dm.DocumentNode.newFromPlainObject( veTest.obj );
|
|
|
|
// Test 1
|
|
deepEqual( documentModel.getData(), veTest.data, 'Flattening plain objects results in correct data' );
|
|
} );
|
|
|
|
test( 've.dm.DocumentNode.getChildren', 1, function() {
|
|
var documentModel = ve.dm.DocumentNode.newFromPlainObject( veTest.obj );
|
|
|
|
function equalLengths( a, b ) {
|
|
if ( a.length !== b.length ) {
|
|
return false;
|
|
}
|
|
for ( var i = 0; i < a.length; i++ ) {
|
|
if ( a[i].getContentLength() !== b[i].getContentLength() ) {
|
|
console.log( 'mismatched content lengths', a[i], b[i] );
|
|
return false;
|
|
}
|
|
var aIsBranch = typeof a[i].getChildren === 'function';
|
|
var bIsBranch = typeof b[i].getChildren === 'function';
|
|
if ( aIsBranch !== bIsBranch ) {
|
|
return false;
|
|
}
|
|
if ( aIsBranch && !equalLengths( a[i].getChildren(), b[i].getChildren() ) ) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Test 1
|
|
ok(
|
|
equalLengths( documentModel.getChildren(), veTest.tree ),
|
|
'Nodes in the model tree contain correct lengths'
|
|
);
|
|
} );
|
|
|
|
test( 've.dm.DocumentNode.getRelativeContentOffset', 7, function() {
|
|
var documentModel = ve.dm.DocumentNode.newFromPlainObject( veTest.obj );
|
|
|
|
// Test 1
|
|
equal(
|
|
documentModel.getRelativeContentOffset( 1, 1 ),
|
|
2,
|
|
'getRelativeContentOffset advances forwards through the inside of elements'
|
|
);
|
|
// Test 2
|
|
equal(
|
|
documentModel.getRelativeContentOffset( 2, -1 ),
|
|
1,
|
|
'getRelativeContentOffset advances backwards through the inside of elements'
|
|
);
|
|
// Test 3
|
|
equal(
|
|
documentModel.getRelativeContentOffset( 3, 1 ),
|
|
4,
|
|
'getRelativeContentOffset uses the offset after the last character in an element'
|
|
);
|
|
// Test 4
|
|
equal(
|
|
documentModel.getRelativeContentOffset( 1, -1 ),
|
|
1,
|
|
'getRelativeContentOffset does not allow moving before the content of the first node'
|
|
);
|
|
// Test 5
|
|
equal(
|
|
documentModel.getRelativeContentOffset( 33, 1 ),
|
|
33,
|
|
'getRelativeContentOffset does not allow moving after the content of the last node'
|
|
);
|
|
// Test 6
|
|
equal(
|
|
documentModel.getRelativeContentOffset( 4, 1 ),
|
|
9,
|
|
'getRelativeContentOffset advances forwards between elements'
|
|
);
|
|
// Test 7
|
|
equal(
|
|
documentModel.getRelativeContentOffset( 32, -1 ),
|
|
25,
|
|
'getRelativeContentOffset advances backwards between elements'
|
|
);
|
|
} );
|
|
|
|
test( 've.dm.DocumentNode.getContentData', 6, function() {
|
|
var documentModel = ve.dm.DocumentNode.newFromPlainObject( veTest.obj ),
|
|
childNodes = documentModel.getChildren();
|
|
|
|
// Test 1
|
|
deepEqual(
|
|
childNodes[0].getContentData( new ve.Range( 1, 3 ) ),
|
|
[
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }]
|
|
],
|
|
'getContentData can return an ending portion of the content'
|
|
);
|
|
|
|
// Test 2
|
|
deepEqual(
|
|
childNodes[0].getContentData( new ve.Range( 0, 2 ) ),
|
|
['a', ['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }]],
|
|
'getContentData can return a beginning portion of the content'
|
|
);
|
|
|
|
// Test 3
|
|
deepEqual(
|
|
childNodes[0].getContentData( new ve.Range( 1, 2 ) ),
|
|
[['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }]],
|
|
'getContentData can return a middle portion of the content'
|
|
);
|
|
|
|
// Test 4
|
|
try {
|
|
childNodes[0].getContentData( new ve.Range( -1, 3 ) );
|
|
} catch ( negativeIndexError ) {
|
|
ok( true, 'getContentData throws exceptions when given a range with start < 0' );
|
|
}
|
|
|
|
// Test 5
|
|
try {
|
|
childNodes[0].getContentData( new ve.Range( 0, 4 ) );
|
|
} catch ( outOfRangeError ) {
|
|
ok( true, 'getContentData throws exceptions when given a range with end > length' );
|
|
}
|
|
|
|
// Test 6
|
|
deepEqual( childNodes[2].getContentData(), ['h'], 'Content can be extracted from nodes' );
|
|
} );
|
|
|
|
test( 've.dm.DocumentNode.getIndexOfAnnotation', 3, function() {
|
|
var documentModel = ve.dm.DocumentNode.newFromPlainObject( veTest.obj );
|
|
|
|
var bold = { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' },
|
|
italic = { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' },
|
|
nothing = { 'type': 'nothing', 'hash': '{"type":"nothing"}' },
|
|
character = ['a', bold, italic];
|
|
|
|
// Test 1
|
|
equal(
|
|
ve.dm.DocumentNode.getIndexOfAnnotation( character, bold ),
|
|
1,
|
|
'getIndexOfAnnotation get the correct index'
|
|
);
|
|
|
|
// Test 2
|
|
equal(
|
|
ve.dm.DocumentNode.getIndexOfAnnotation( character, italic ),
|
|
2,
|
|
'getIndexOfAnnotation get the correct index'
|
|
);
|
|
|
|
// Test 3
|
|
equal(
|
|
ve.dm.DocumentNode.getIndexOfAnnotation( character, nothing ),
|
|
-1,
|
|
'getIndexOfAnnotation returns -1 if the annotation was not found'
|
|
);
|
|
} );
|
|
|
|
test( 've.dm.DocumentNode.getWordBoundaries', 2, function() {
|
|
var documentModel = ve.dm.DocumentNode.newFromPlainObject( veTest.obj );
|
|
deepEqual(
|
|
documentModel.getWordBoundaries( 2 ),
|
|
new ve.Range( 1, 4 ),
|
|
'getWordBoundaries returns range around nearest whole word'
|
|
);
|
|
strictEqual(
|
|
documentModel.getWordBoundaries( 5 ),
|
|
null,
|
|
'getWordBoundaries returns null when given non-content offset'
|
|
);
|
|
} );
|
|
|
|
test( 've.dm.DocumentNode.getAnnotationBoundaries', 2, function() {
|
|
var documentModel = ve.dm.DocumentNode.newFromPlainObject( veTest.obj );
|
|
deepEqual(
|
|
documentModel.getAnnotationBoundaries( 2, { 'type': 'textStyle/bold' } ),
|
|
new ve.Range( 2, 3 ),
|
|
'getWordBoundaries returns range around content covered by annotation'
|
|
);
|
|
strictEqual(
|
|
documentModel.getAnnotationBoundaries( 1, { 'type': 'textStyle/bold' } ),
|
|
null,
|
|
'getWordBoundaries returns null if offset is not covered by annotation'
|
|
);
|
|
} );
|
|
|
|
test( 've.dm.DocumentNode.getAnnotationsFromOffset', 4, function() {
|
|
var documentModel = ve.dm.DocumentNode.newFromPlainObject( veTest.obj );
|
|
deepEqual(
|
|
documentModel.getAnnotationsFromOffset( 1 ),
|
|
[],
|
|
'getAnnotationsFromOffset returns empty array for non-annotated content'
|
|
);
|
|
deepEqual(
|
|
documentModel.getAnnotationsFromOffset( 2 ),
|
|
[{ 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
'getAnnotationsFromOffset returns annotations of annotated content correctly'
|
|
);
|
|
deepEqual(
|
|
documentModel.getAnnotationsFromOffset( 3 ),
|
|
[{ 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }],
|
|
'getAnnotationsFromOffset returns annotations of annotated content correctly'
|
|
);
|
|
deepEqual(
|
|
documentModel.getAnnotationsFromOffset( 0 ),
|
|
[],
|
|
'getAnnotationsFromOffset returns empty array when given a non-content offset'
|
|
);
|
|
} );
|
|
|
|
function compareOffsets( documentModel, expectedOffsets, descPrefix, start, end ) {
|
|
start = start || 0;
|
|
end = end || expectedOffsets.length;
|
|
equal( documentModel.offsetMap.length, expectedOffsets.length, descPrefix + ' (offset map has the correct length)' );
|
|
|
|
// We use a loop instead of equal( documentModel.offsetMap, expectedOffsets ); because
|
|
// the latter will descend into the elements and their children
|
|
for ( var i = start; i < end; i++ ) {
|
|
ok( documentModel.offsetMap[i] == expectedOffsets[i], descPrefix + ' (offset map elements point to the correct nodes (element ' + i + '))' );
|
|
}
|
|
}
|
|
|
|
function repeatArray( element, n ) {
|
|
var a = [];
|
|
for ( var i = 0; i < n; i++ ) {
|
|
a[i] = element;
|
|
}
|
|
return a;
|
|
}
|
|
|
|
test( 've.dm.DocumentNode.offsetMap', function() {
|
|
var documentModel = new ve.dm.DocumentNode( veTest.data );
|
|
var expectedOffsets = veTest.getOffsets( documentModel );
|
|
compareOffsets( documentModel, expectedOffsets, 'Constructor creates offset map correctly' );
|
|
} );
|
|
|
|
test( 've.dm.DocumentNode.rebuildNodes', function() {
|
|
var documentModel = new ve.dm.DocumentNode( veTest.data.slice( 0 ) );
|
|
var originalLength = veTest.data.length;
|
|
var expectedOffsets = veTest.getOffsets( documentModel );
|
|
|
|
// Make the first paragraph longer
|
|
documentModel.data.splice( 3, 0, 'F', 'O', 'O' );
|
|
documentModel.rebuildNodes( documentModel, 0, 1, 0, documentModel.data.slice( 0, 8 ) );
|
|
ve.batchedSplice( expectedOffsets, 1, 4, repeatArray( documentModel.children[0], 7 ) );
|
|
equal( documentModel.children[0].getElementType(), 'paragraph', 'rebuildNodes() makes a paragraph longer (first child is a paragraph)' );
|
|
equal( documentModel.children[0].getContentLength(), 6, 'rebuildNodes() rmakes a paragraph longer (content length is updated)' );
|
|
equal( documentModel.children.length, 3, 'rebuildNodes() makes a paragraph longer (document still has 3 children)' );
|
|
equal( documentModel.getContentLength(), originalLength + 3, 'rebuildNodes() makes a paragraph longer (document content length is updated)' );
|
|
compareOffsets( documentModel, expectedOffsets, 'rebuildNodes() makes a paragraph longer', 0, 9 );
|
|
|
|
// Now make it shorter
|
|
documentModel.data.splice( 2, 4 );
|
|
documentModel.rebuildNodes( documentModel, 0, 1, 0, documentModel.data.slice( 0, 4 ) );
|
|
ve.batchedSplice( expectedOffsets, 1, 7, repeatArray( documentModel.children[0], 3 ) );
|
|
equal( documentModel.children[0].getElementType(), 'paragraph', 'rebuildNodes() makes a paragraph shorter (first child is a paragraph)' );
|
|
equal( documentModel.children[0].getContentLength(), 2, 'rebuildNodes() makes a paragraph shorter (content length is updated)' );
|
|
equal( documentModel.children.length, 3, 'rebuildNodes() makes a paragraph shorter (document still has 3 children)' );
|
|
equal( documentModel.getContentLength(), originalLength - 1, 'rebuildNodes() makes a paragraph shorter (document content length is updated)' );
|
|
compareOffsets( documentModel, expectedOffsets, 'rebuildNodes() makes a paragraph shorter', 0, 5 );
|
|
|
|
// Split the first paragraph up
|
|
documentModel.data.splice( 2, 0, { 'type': '/paragraph' }, { 'type': 'paragraph' } );
|
|
documentModel.rebuildNodes( documentModel, 0, 1, 0, documentModel.data.slice( 0, 6 ) );
|
|
expectedOffsets.splice( 1, 3, documentModel.children[0], documentModel.children[0], documentModel, documentModel.children[1], documentModel.children[1] );
|
|
equal( documentModel.children[0].getElementType(), 'paragraph', 'rebuildNodes() splits a paragraph (first child is a paragraph)' );
|
|
equal( documentModel.children[1].getElementType(), 'paragraph', 'rebuildNodes() splits a paragraph (second child is a paragraph)' );
|
|
equal( documentModel.children[0].getContentLength(), 1, 'rebuildNodes() splits a paragraph (content length of first paragraph is updated)' );
|
|
equal( documentModel.children[1].getContentLength(), 1, 'rebuildNodes() splits a paragraph (content length of second paragraph is updated)' );
|
|
equal( documentModel.children.length, 4, 'rebuildNodes() splits a paragraph (document now has 4 children)' );
|
|
equal( documentModel.getContentLength(), originalLength + 1, 'rebuildNodes() splits a paragraph (document content length is updated)' );
|
|
compareOffsets( documentModel, expectedOffsets, 'rebuildNodes() splits a paragraph', 0, 7 );
|
|
|
|
// Join it back together
|
|
documentModel.data.splice( 2, 2 );
|
|
documentModel.rebuildNodes( documentModel, 0, 2, 0, documentModel.data.slice( 0, 4 ) );
|
|
ve.batchedSplice( expectedOffsets, 1, 5, repeatArray( documentModel.children[0], 3 ) );
|
|
equal( documentModel.children[0].getElementType(), 'paragraph', 'rebuildNodes() joins two paragraphs (first child is a paragraph)' );
|
|
equal( documentModel.children[0].getContentLength(), 2, 'rebuildNodes() joins two paragraphs (content length is updated)' );
|
|
equal( documentModel.children.length, 3, 'rebuildNodes() joins two paragraphsr (document still has 3 children)' );
|
|
equal( documentModel.getContentLength(), originalLength - 1, 'rebuildNodes() joins two paragraphs (document content length is updated)' );
|
|
compareOffsets( documentModel, expectedOffsets, 'rebuildNodes() joins two paragraphs', 0, 5 );
|
|
|
|
// Add a paragraph to the first listItem by rebuilding the listItem
|
|
documentModel.data.splice( 12, 0, { 'type': 'paragraph' }, 'B', 'A', 'R', { 'type': '/paragraph' } );
|
|
var list = documentModel.children[1].children[0].children[0].children[1];
|
|
documentModel.rebuildNodes( list, 0, 1, 11, documentModel.data.slice( 11, 21 ) );
|
|
var newListItem = documentModel.children[1].children[0].children[0].children[1].children[0];
|
|
ve.batchedSplice( expectedOffsets, 11, 5,
|
|
[ list, newListItem ].concat(
|
|
repeatArray( newListItem.children[0], 4 ) ).concat(
|
|
[ newListItem, newListItem.children[1], newListItem.children[1], newListItem ] )
|
|
);
|
|
equal( newListItem.getElementType(), 'listItem', 'rebuildNodes() adds a paragraph to a listItem (listItem is still a listItem)' );
|
|
equal( newListItem.children.length, 2, 'rebuildNodes() adds a paragraph to a listItem (listItem now has 2 children)' );
|
|
equal( newListItem.children[0].getElementType(), 'paragraph', 'rebuildNodes() adds a paragraph to a listItem (first child is a paragraph)' );
|
|
equal( newListItem.children[1].getElementType(), 'paragraph', 'rebuildNodes() adds a paragraph to a listItem (second child is a paragraph)' );
|
|
equal( newListItem.children[0].getContentLength(), 3, 'rebuildNodes() adds a paragraph to a listItem (first paragraph has correct content length)' );
|
|
equal( newListItem.children[1].getContentLength(), 1, 'rebuildNodes() adds a paragraph to a listItem (second paragraph has correct content length)' );
|
|
equal( newListItem.getContentLength(), 8, 'rebuildNodes() adds a paragraph to a listItem (content length of listItem is updated)' );
|
|
equal( documentModel.getContentLength(), originalLength + 4, 'rebuildNodes() adds a paragraph to a listItem (document content length is updated)' );
|
|
compareOffsets( documentModel, expectedOffsets, 'rebuildNodes() adds a paragraph to a listItem', 10, 22 );
|
|
|
|
// Add another paragraph to the first listItem using a zero rebuild
|
|
documentModel.data.splice( 20, 0, { 'type': 'paragraph' }, 'B', 'A', 'Z', { 'type': '/paragraph' } );
|
|
documentModel.rebuildNodes( newListItem, 2, 0, 20, documentModel.data.slice( 20, 25 ) );
|
|
ve.batchedSplice( expectedOffsets, 20, 0,
|
|
[ newListItem ].concat( repeatArray( newListItem.children[2], 4 ) )
|
|
);
|
|
equal( newListItem.getElementType(), 'listItem', 'rebuildNodes() adds a paragraph to a listItem with zero rebuild (listItem is still a listItem) ');
|
|
equal( newListItem.children.length, 3, 'rebuildNodes() adds a paragraph to a listItem with zero rebuild (listItem now has 3 children)' );
|
|
equal( newListItem.children[0].getElementType(), 'paragraph', 'rebuildNodes() adds a paragraph to a listItem with zero rebuild (first child is a paragraph)' );
|
|
equal( newListItem.children[1].getElementType(), 'paragraph', 'rebuildNodes() adds a paragraph to a listItem with zero rebuild (second child is a paragraph)' );
|
|
equal( newListItem.children[2].getElementType(), 'paragraph', 'rebuildNodes() adds a paragraph to a listItem with zero rebuild (third child is a paragraph)' );
|
|
equal( newListItem.children[0].getContentLength(), 3, 'rebuildNodes() adds a paragraph to a listItem with zero rebuild (first paragraph has correct content length)' );
|
|
equal( newListItem.children[1].getContentLength(), 1, 'rebuildNodes() adds a paragraph to a listItem with zero rebuild (second paragraph has correct content length)' );
|
|
equal( newListItem.children[2].getContentLength(), 3, 'rebuildNodes() adds a paragraph to a listItem with zero rebuild (third paragraph has correct content length)' );
|
|
equal( newListItem.getContentLength(), 13, 'rebuildNodes() adds a paragraph to a listItem with zero rebuild (content length of listItem is updated)' );
|
|
equal( documentModel.getContentLength(), originalLength + 9, 'rebuildNodes() adds a paragraph to a listItem with zero rebuild (document content length is updated)' );
|
|
compareOffsets( documentModel, expectedOffsets, 'rebuildNodes() adds a paragraph to a listItem with zero rebuild', 10, 28 );
|
|
} );
|
|
|
|
test( 've.dm.DocumentNode.prepareElementAttributeChange', 4, function() {
|
|
var documentModel = ve.dm.DocumentNode.newFromPlainObject( veTest.obj );
|
|
|
|
// Test 1
|
|
deepEqual(
|
|
documentModel.prepareElementAttributeChange( 0, 'test', 1234 ).getOperations(),
|
|
[
|
|
{ 'type': 'attribute', 'key': 'test', 'from': undefined, 'to': 1234 },
|
|
{ 'type': 'retain', 'length': 34 }
|
|
],
|
|
'prepareElementAttributeChange retains data after attribute change for first element'
|
|
);
|
|
|
|
// Test 2
|
|
deepEqual(
|
|
documentModel.prepareElementAttributeChange( 5, 'test', 1234 ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 5 },
|
|
{ 'type': 'attribute', 'key': 'test', 'from': undefined, 'to': 1234 },
|
|
{ 'type': 'retain', 'length': 29 }
|
|
],
|
|
'prepareElementAttributeChange retains data before and after attribute change'
|
|
);
|
|
|
|
// Test 3
|
|
try {
|
|
documentModel.prepareElementAttributeChange( 1, 'set', 'test', 1234 );
|
|
} catch ( invalidOffsetError ) {
|
|
ok(
|
|
true,
|
|
'prepareElementAttributeChange throws an exception when offset is not an element'
|
|
);
|
|
}
|
|
|
|
// Test 4
|
|
try {
|
|
documentModel.prepareElementAttributeChange( 4, 'set', 'test', 1234 );
|
|
} catch ( closingElementError ) {
|
|
ok(
|
|
true,
|
|
'prepareElementAttributeChange throws an exception when offset is a closing element'
|
|
);
|
|
}
|
|
} );
|
|
|
|
test( 've.dm.DocumentNode.prepareContentAnnotation', 3, function() {
|
|
var documentModel = ve.dm.DocumentNode.newFromPlainObject( veTest.obj );
|
|
|
|
// Test 1
|
|
deepEqual(
|
|
documentModel.prepareContentAnnotation(
|
|
new ve.Range( 1, 4 ), 'set', { 'type': 'textStyle/bold' }
|
|
).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 1 },
|
|
{
|
|
'type': 'annotate',
|
|
'method': 'set',
|
|
'bias': 'start',
|
|
'annotation': { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }
|
|
},
|
|
{ 'type': 'retain', 'length': 1 },
|
|
{
|
|
'type': 'annotate',
|
|
'method': 'set',
|
|
'bias': 'stop',
|
|
'annotation': { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }
|
|
},
|
|
{ 'type': 'retain', 'length': 1 },
|
|
{
|
|
'type': 'annotate',
|
|
'method': 'set',
|
|
'bias': 'start',
|
|
'annotation': { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }
|
|
},
|
|
{ 'type': 'retain', 'length': 1 },
|
|
{
|
|
'type': 'annotate',
|
|
'method': 'set',
|
|
'bias': 'stop',
|
|
'annotation': { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }
|
|
},
|
|
{ 'type': 'retain', 'length': 30 }
|
|
],
|
|
'prepareContentAnnotation skips over content that is already set or cleared'
|
|
);
|
|
|
|
// Test 2
|
|
deepEqual(
|
|
documentModel.prepareContentAnnotation(
|
|
new ve.Range( 3, 10 ), 'set', { 'type': 'textStyle/bold' }
|
|
).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 3 },
|
|
{
|
|
'type': 'annotate',
|
|
'method': 'set',
|
|
'bias': 'start',
|
|
'annotation': { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }
|
|
},
|
|
{ 'type': 'retain', 'length': 1 },
|
|
{
|
|
'type': 'annotate',
|
|
'method': 'set',
|
|
'bias': 'stop',
|
|
'annotation': { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }
|
|
},
|
|
{ 'type': 'retain', 'length': 5 },
|
|
{
|
|
'type': 'annotate',
|
|
'method': 'set',
|
|
'bias': 'start',
|
|
'annotation': { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }
|
|
},
|
|
{ 'type': 'retain', 'length': 1 },
|
|
{
|
|
'type': 'annotate',
|
|
'method': 'set',
|
|
'bias': 'stop',
|
|
'annotation': { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }
|
|
},
|
|
{ 'type': 'retain', 'length': 24 }
|
|
],
|
|
'prepareContentAnnotation works across element boundaries'
|
|
);
|
|
|
|
// Test 3
|
|
deepEqual(
|
|
documentModel.prepareContentAnnotation(
|
|
new ve.Range( 4, 11 ), 'set', { 'type': 'textStyle/bold' }
|
|
).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 9 },
|
|
{
|
|
'type': 'annotate',
|
|
'method': 'set',
|
|
'bias': 'start',
|
|
'annotation': { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }
|
|
},
|
|
{ 'type': 'retain', 'length': 1 },
|
|
{
|
|
'type': 'annotate',
|
|
'method': 'set',
|
|
'bias': 'stop',
|
|
'annotation': { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }
|
|
},
|
|
{ 'type': 'retain', 'length': 24 }
|
|
],
|
|
'prepareContentAnnotation works when given structural offsets'
|
|
);
|
|
} );
|
|
|
|
test( 've.dm.DocumentNode.prepareRemoval', 11, function() {
|
|
var documentModel = ve.dm.DocumentNode.newFromPlainObject( veTest.obj );
|
|
|
|
// Test 1
|
|
deepEqual(
|
|
documentModel.prepareRemoval( new ve.Range( 1, 4 ) ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 1 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [
|
|
'a',
|
|
['b', { 'type': 'textStyle/bold', 'hash': '{"type":"textStyle/bold"}' }],
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }]
|
|
]
|
|
},
|
|
{ 'type': 'retain', 'length': 30 }
|
|
],
|
|
'prepareRemoval includes the content being removed'
|
|
);
|
|
|
|
// Test 2
|
|
deepEqual(
|
|
documentModel.prepareRemoval( new ve.Range( 17, 22 ) ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 17 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'f',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' }
|
|
]
|
|
},
|
|
{ 'type': 'retain', 'length': 12 }
|
|
],
|
|
'prepareRemoval removes entire elements'
|
|
);
|
|
|
|
// Test 3
|
|
deepEqual(
|
|
documentModel.prepareRemoval( new ve.Range( 3, 9 ) ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 3 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [
|
|
['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }]
|
|
]
|
|
},
|
|
{ 'type': 'retain', 'length': 30 }
|
|
],
|
|
'prepareRemoval works across structural nodes'
|
|
);
|
|
|
|
// Test 4
|
|
deepEqual(
|
|
documentModel.prepareRemoval( new ve.Range( 3, 24 ) ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 3 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }]]
|
|
},
|
|
{ 'type': 'retain', 'length': 4 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [{ 'type': 'paragraph' }, 'd', { 'type': '/paragraph' }]
|
|
},
|
|
{ 'type': 'retain', 'length': 1 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'e',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'f',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' }
|
|
]
|
|
},
|
|
{ 'type': 'retain', 'length': 12 }
|
|
],
|
|
'prepareRemoval strips and drops correctly when working across structural nodes'
|
|
);
|
|
|
|
// Test 5
|
|
deepEqual(
|
|
documentModel.prepareRemoval( new ve.Range( 3, 25 ) ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 3 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [['c', { 'type': 'textStyle/italic', 'hash': '{"type":"textStyle/italic"}' }]]
|
|
},
|
|
{ 'type': 'retain', 'length': 4 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [{ 'type': 'paragraph' }, 'd', { 'type': '/paragraph' }]
|
|
},
|
|
{ 'type': 'retain', 'length': 1 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'e',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'f',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' }
|
|
]
|
|
},
|
|
{ 'type': 'retain', 'length': 2 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [ 'g' ]
|
|
},
|
|
{ 'type': 'retain', 'length': 9 }
|
|
],
|
|
'prepareRemoval strips and drops correctly when working across structural nodes (2)'
|
|
);
|
|
|
|
// Test 6
|
|
deepEqual(
|
|
documentModel.prepareRemoval( new ve.Range( 9, 17 ) ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 9 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [ 'd' ]
|
|
},
|
|
{ 'type': 'retain', 'length': 2 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'e',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' }
|
|
]
|
|
},
|
|
{ 'type': 'retain', 'length': 17 }
|
|
],
|
|
'prepareRemoval will not merge items of unequal types'
|
|
);
|
|
|
|
// Test 7
|
|
deepEqual(
|
|
documentModel.prepareRemoval( new ve.Range( 9, 27 ) ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 9 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [ 'd' ]
|
|
},
|
|
{ 'type': 'retain', 'length': 2 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'e',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'f',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['number'] } },
|
|
{ 'type': 'paragraph' },
|
|
'g',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' }
|
|
]
|
|
},
|
|
{ 'type': 'retain', 'length': 7 }
|
|
],
|
|
'prepareRemoval blanks a paragraph and a list'
|
|
);
|
|
|
|
// Test 8
|
|
deepEqual(
|
|
documentModel.prepareRemoval( new ve.Range( 21, 23 ) ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 21 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['number'] } }
|
|
]
|
|
},
|
|
{ 'type': 'retain', 'length': 11 }
|
|
],
|
|
'prepareRemoval merges two list items'
|
|
);
|
|
|
|
// Test 9
|
|
deepEqual(
|
|
documentModel.prepareRemoval( new ve.Range( 20, 24 ) ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 20 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['number'] } },
|
|
{ 'type': 'paragraph' }
|
|
]
|
|
},
|
|
{ 'type': 'retain', 'length': 10 }
|
|
],
|
|
'prepareRemoval merges two list items and the paragraphs inside them'
|
|
);
|
|
|
|
// Test 10
|
|
deepEqual(
|
|
documentModel.prepareRemoval( new ve.Range( 20, 23 ) ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 34 }
|
|
],
|
|
'prepareRemoval returns a null transaction when attempting an unbalanced merge'
|
|
);
|
|
|
|
// Test 11
|
|
deepEqual(
|
|
documentModel.prepareRemoval( new ve.Range( 15, 24 ) ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 15 },
|
|
{
|
|
'type': 'remove',
|
|
'data': [
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } },
|
|
{ 'type': 'paragraph' },
|
|
'f',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': '/listItem' },
|
|
{ 'type': 'listItem', 'attributes': { 'styles': ['number'] } },
|
|
{ 'type': 'paragraph' }
|
|
]
|
|
},
|
|
{ 'type': 'retain', 'length': 10 }
|
|
],
|
|
'prepareRemoval merges two list items and the paragraphs inside them'
|
|
);
|
|
|
|
} );
|
|
|
|
test( 've.dm.DocumentNode.prepareInsertion', 11, function() {
|
|
var documentModel = ve.dm.DocumentNode.newFromPlainObject( veTest.obj );
|
|
|
|
// Test 1
|
|
deepEqual(
|
|
documentModel.prepareInsertion( 1, ['d', 'e', 'f'] ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 1 },
|
|
{ 'type': 'insert', 'data': ['d', 'e', 'f'] },
|
|
{ 'type': 'retain', 'length': 33 }
|
|
],
|
|
'prepareInsertion retains data up to the offset and includes the content being inserted'
|
|
);
|
|
|
|
// Test 2
|
|
deepEqual(
|
|
documentModel.prepareInsertion(
|
|
5, [{ 'type': 'paragraph' }, 'd', 'e', 'f', { 'type': '/paragraph' }]
|
|
).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 5 },
|
|
{
|
|
'type': 'insert',
|
|
'data': [{ 'type': 'paragraph' }, 'd', 'e', 'f', { 'type': '/paragraph' }]
|
|
},
|
|
{ 'type': 'retain', 'length': 29 }
|
|
],
|
|
'prepareInsertion inserts a paragraph between two structural elements'
|
|
);
|
|
|
|
// Test 3
|
|
deepEqual(
|
|
documentModel.prepareInsertion( 5, ['d', 'e', 'f'] ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 5 },
|
|
{
|
|
'type': 'insert',
|
|
'data': [{ 'type': 'paragraph' }, 'd', 'e', 'f', { 'type': '/paragraph' }]
|
|
},
|
|
{ 'type': 'retain', 'length': 29 }
|
|
],
|
|
'prepareInsertion wraps unstructured content inserted between elements in a paragraph'
|
|
);
|
|
|
|
// Test 4
|
|
deepEqual(
|
|
documentModel.prepareInsertion(
|
|
5, [{ 'type': 'paragraph' }, 'd', 'e', 'f']
|
|
).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 5 },
|
|
{
|
|
'type': 'insert',
|
|
'data': [{ 'type': 'paragraph' }, 'd', 'e', 'f', { 'type': '/paragraph' }]
|
|
},
|
|
{ 'type': 'retain', 'length': 29 }
|
|
],
|
|
'prepareInsertion completes opening elements in inserted content'
|
|
);
|
|
|
|
// Test 5
|
|
deepEqual(
|
|
documentModel.prepareInsertion(
|
|
2, [ { 'type': 'table' }, { 'type': '/table' } ]
|
|
).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 2 },
|
|
{
|
|
'type': 'insert',
|
|
'data': [
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': 'table' },
|
|
{ 'type': '/table' },
|
|
{ 'type': 'paragraph' }
|
|
]
|
|
},
|
|
{ 'type': 'retain', 'length': 32 }
|
|
],
|
|
'prepareInsertion splits up paragraph when inserting a table in the middle'
|
|
);
|
|
|
|
// Test 6
|
|
deepEqual(
|
|
documentModel.prepareInsertion(
|
|
2, [ 'f', 'o', 'o', { 'type': '/paragraph' }, { 'type': 'paragraph' }, 'b', 'a', 'r' ]
|
|
).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 2 },
|
|
{
|
|
'type': 'insert',
|
|
'data': [
|
|
'f',
|
|
'o',
|
|
'o',
|
|
{ 'type': '/paragraph' },
|
|
{ 'type': 'paragraph' },
|
|
'b',
|
|
'a',
|
|
'r'
|
|
]
|
|
},
|
|
{ 'type': 'retain', 'length': 32 }
|
|
],
|
|
'prepareInsertion splits paragraph when inserting a paragraph closing and opening inside it'
|
|
);
|
|
|
|
// Test 7
|
|
deepEqual(
|
|
documentModel.prepareInsertion(
|
|
0, [ { 'type': 'paragraph' }, 'f', 'o', 'o', { 'type': '/paragraph' } ]
|
|
).getOperations(),
|
|
[
|
|
{
|
|
'type': 'insert',
|
|
'data': [ { 'type': 'paragraph' }, 'f', 'o', 'o', { 'type': '/paragraph' } ]
|
|
},
|
|
{ 'type': 'retain', 'length': 34 }
|
|
],
|
|
'prepareInsertion inserts at the beginning, then retains up to the end'
|
|
);
|
|
|
|
// Test 8
|
|
deepEqual(
|
|
documentModel.prepareInsertion(
|
|
34, [ { 'type': 'paragraph' }, 'f', 'o', 'o', { 'type': '/paragraph' } ]
|
|
).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 34 },
|
|
{
|
|
'type': 'insert',
|
|
'data': [ { 'type': 'paragraph' }, 'f', 'o', 'o', { 'type': '/paragraph' } ]
|
|
}
|
|
],
|
|
'prepareInsertion inserts at the end'
|
|
);
|
|
|
|
// Test 9
|
|
raises(
|
|
function() {
|
|
documentModel.prepareInsertion(
|
|
-1,
|
|
[ { 'type': 'paragraph' }, 'f', 'o', 'o', { 'type': '/paragraph' } ]
|
|
);
|
|
},
|
|
/^Offset -1 out of bounds/,
|
|
'prepareInsertion throws exception for negative offset'
|
|
);
|
|
|
|
// Test 10
|
|
raises(
|
|
function() {
|
|
documentModel.prepareInsertion(
|
|
35,
|
|
[ { 'type': 'paragraph' }, 'f', 'o', 'o', { 'type': '/paragraph' } ]
|
|
);
|
|
},
|
|
/^Offset 35 out of bounds/,
|
|
'prepareInsertion throws exception for offset past the end'
|
|
);
|
|
|
|
// Test 11
|
|
raises(
|
|
function() {
|
|
documentModel.prepareInsertion(
|
|
5,
|
|
[{ 'type': 'paragraph' }, 'a', { 'type': 'listItem' }, { 'type': '/paragraph' }]
|
|
);
|
|
},
|
|
/^Input is malformed: expected \/listItem but got \/paragraph at index 3$/,
|
|
'prepareInsertion throws exception for malformed input'
|
|
);
|
|
} );
|
|
|
|
test( 've.dm.DocumentNode.prepareWrap', 6, function() {
|
|
var documentModel = ve.dm.DocumentNode.newFromPlainObject( veTest.obj );
|
|
|
|
// Test 1
|
|
deepEqual(
|
|
documentModel.prepareWrap( new ve.Range( 1, 4 ), [ { 'type': 'paragraph' } ], [ { 'type': 'heading', 'level': 2 } ], [], [] ).getOperations(),
|
|
[
|
|
{ 'type': 'replace', 'remove': [ { 'type': 'paragraph' } ], 'replacement': [ { 'type': 'heading', 'level': 2 } ] },
|
|
{ 'type': 'retain', 'length': 3 },
|
|
{ 'type': 'replace', 'remove': [ { 'type': '/paragraph' } ], 'replacement': [ { 'type': '/heading' } ] },
|
|
{ 'type': 'retain', 'length': 29 }
|
|
],
|
|
'prepareWrap changes a paragraph to a heading'
|
|
);
|
|
|
|
// Test 2
|
|
deepEqual(
|
|
documentModel.prepareWrap( new ve.Range( 12, 27 ), [ { 'type': 'list' } ], [], [ { 'type': 'listItem' } ], [] ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 11 },
|
|
{ 'type': 'replace', 'remove': [ { 'type': 'list' } ], 'replacement': [] },
|
|
{ 'type': 'replace', 'remove': [ { 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } } ], 'replacement': [] },
|
|
{ 'type': 'retain', 'length': 3 },
|
|
{ 'type': 'replace', 'remove': [ { 'type': '/listItem' } ], 'replacement': [] },
|
|
{ 'type': 'replace', 'remove': [ { 'type': 'listItem', 'attributes': { 'styles': ['bullet', 'bullet'] } } ], 'replacement': [] },
|
|
{ 'type': 'retain', 'length': 3 },
|
|
{ 'type': 'replace', 'remove': [ { 'type': '/listItem' } ], 'replacement': [] },
|
|
{ 'type': 'replace', 'remove': [ { 'type': 'listItem', 'attributes': { 'styles': ['number'] } } ], 'replacement': [] },
|
|
{ 'type': 'retain', 'length': 3 },
|
|
{ 'type': 'replace', 'remove': [ { 'type': '/listItem' } ], 'replacement': [] },
|
|
{ 'type': 'replace', 'remove': [ { 'type': '/list' } ], 'replacement': [] },
|
|
{ 'type': 'retain', 'length': 6 }
|
|
],
|
|
'prepareWrap unwraps a list'
|
|
);
|
|
|
|
// Test 3
|
|
deepEqual(
|
|
documentModel.prepareWrap( new ve.Range( 8, 28 ), [ { 'type': 'table' }, { 'type': 'tableRow' }, { 'type': 'tableCell' } ], [ { 'type': 'list' }, { 'type': 'listItem' } ], [], [] ).getOperations(),
|
|
[
|
|
{ 'type': 'retain', 'length': 5 },
|
|
{ 'type': 'replace', 'remove': [ { 'type': 'table' }, { 'type': 'tableRow' }, { 'type': 'tableCell' } ], 'replacement': [ { 'type': 'list' }, { 'type': 'listItem' } ] },
|
|
{ 'type': 'retain', 'length': 20 },
|
|
{ 'type': 'replace', 'remove': [ { 'type': '/tableCell' }, { 'type': '/tableRow' }, { 'type': '/table' } ], 'replacement': [ { 'type': '/listItem' }, { 'type': '/list' } ] },
|
|
{ 'type': 'retain', 'length': 3 }
|
|
],
|
|
'prepareWrap replaces a table with a list'
|
|
);
|
|
|
|
// Test 4
|
|
raises(
|
|
function() {
|
|
documentModel.prepareWrap( new ve.Range( 12, 27 ), [ { 'type': 'table' } ], [], [], [] );
|
|
},
|
|
/^Element in unwrapOuter does not match: expected table but found list$/,
|
|
'prepareWrap checks integrity of unwrapOuter parameter'
|
|
);
|
|
|
|
// Test 5
|
|
raises(
|
|
function() {
|
|
documentModel.prepareWrap( new ve.Range( 12, 27 ), [ { 'type': 'list' } ], [], [ { 'type': 'paragraph' } ], [] );
|
|
},
|
|
/^Element in unwrapEach does not match: expected paragraph but found listItem$/,
|
|
'prepareWrap checks integrity of unwrapEach parameter'
|
|
);
|
|
|
|
// Test 6
|
|
raises(
|
|
function() {
|
|
documentModel.prepareWrap( new ve.Range( 1, 4 ), [ { 'type': 'listItem' }, { 'type': 'paragraph' } ], [], [], [] );
|
|
},
|
|
/^unwrapOuter is longer than the data preceding the range$/,
|
|
'prepareWrap checks that unwrapOuter fits before the range'
|
|
);
|
|
} ); |