mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-09-26 19:56:49 +00:00
Correctly preserve metadata in Transaction.newFromUnwrap
.
The `Transaction.pushReplace` method has a corner case if the removed region has metadata and the inserted region is empty. This works fine unless there are two adjacent `pushReplace` operations, which can occur in `Transaction.newFromUnwrap`. Fix this by having `pushReplace` look at a preceding replace and correctly merge the two operations if possible (in particular in the tricky case where the previous case has a zero-length insertion). Pleasantly, this can be done without a lot of special-casing code in `pushReplace` or `newFromUnwrap`. Add test cases verifying the `newFromUnwrap` works correctly (both in commit and in rollback) when there is metadata present. Change-Id: I6cfec0d2b1823dad724422f018a3c73dc0c7f186
This commit is contained in:
parent
8a2b55321c
commit
5830bce72c
|
@ -890,11 +890,22 @@ ve.dm.Transaction.prototype.pushReplace = function ( doc, offset, removeLength,
|
||||||
removeMetadata = metadataReplace.remove,
|
removeMetadata = metadataReplace.remove,
|
||||||
insertMetadata = metadataReplace.insert;
|
insertMetadata = metadataReplace.insert;
|
||||||
|
|
||||||
if ( lastOp && lastOp.type === 'replace' && !lastOp.removeMetadata && !removeMetadata ) {
|
// simple replaces can be combined
|
||||||
// simple replaces can just be concatenated
|
// (but don't do this if there is metadata to be removed and the previous
|
||||||
// TODO: allow replaces with meta to be merged?
|
// replace had a non-zero insertion, because that would shift the metadata
|
||||||
lastOp.insert = lastOp.insert.concat( insert );
|
// location.)
|
||||||
lastOp.remove = lastOp.remove.concat( remove );
|
if (
|
||||||
|
lastOp && lastOp.type === 'replace' &&
|
||||||
|
!( lastOp.insert.length > 0 && removeMetadata !== undefined )
|
||||||
|
) {
|
||||||
|
lastOp = this.operations.pop();
|
||||||
|
this.lengthDifference -= lastOp.insert.length - lastOp.remove.length;
|
||||||
|
this.pushReplace(
|
||||||
|
doc,
|
||||||
|
offset - lastOp.remove.length,
|
||||||
|
lastOp.remove.length + removeLength,
|
||||||
|
lastOp.insert.concat( insert )
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
op = {
|
op = {
|
||||||
'type': 'replace',
|
'type': 'replace',
|
||||||
|
@ -906,8 +917,8 @@ ve.dm.Transaction.prototype.pushReplace = function ( doc, offset, removeLength,
|
||||||
op.insertMetadata = insertMetadata;
|
op.insertMetadata = insertMetadata;
|
||||||
}
|
}
|
||||||
this.operations.push( op );
|
this.operations.push( op );
|
||||||
|
this.lengthDifference += insert.length - remove.length;
|
||||||
}
|
}
|
||||||
this.lengthDifference += insert.length - remove.length;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1024,6 +1024,9 @@ QUnit.test( 'newFromContentBranchConversion', function ( assert ) {
|
||||||
QUnit.test( 'newFromWrap', function ( assert ) {
|
QUnit.test( 'newFromWrap', function ( assert ) {
|
||||||
var i, key,
|
var i, key,
|
||||||
doc = ve.dm.example.createExampleDocument(),
|
doc = ve.dm.example.createExampleDocument(),
|
||||||
|
metaDoc = ve.dm.example.createExampleDocument( 'withMeta' ),
|
||||||
|
listMetaDoc = ve.dm.example.createExampleDocument( 'listWithMeta' ),
|
||||||
|
listDoc = ve.dm.example.createExampleDocumentFromObject( 'listDoc', null, { 'listDoc': listMetaDoc.getData() } ),
|
||||||
cases = {
|
cases = {
|
||||||
'changes a heading to a paragraph': {
|
'changes a heading to a paragraph': {
|
||||||
'args': [doc, new ve.Range( 1, 4 ), [ { 'type': 'heading', 'attributes': { 'level': 1 } } ], [ { 'type': 'paragraph' } ], [], []],
|
'args': [doc, new ve.Range( 1, 4 ), [ { 'type': 'heading', 'attributes': { 'level': 1 } } ], [ { 'type': 'paragraph' } ], [], []],
|
||||||
|
@ -1048,6 +1051,25 @@ QUnit.test( 'newFromWrap', function ( assert ) {
|
||||||
{ 'type': 'retain', 'length': 37 }
|
{ 'type': 'retain', 'length': 37 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
'unwraps a multiple-item list': {
|
||||||
|
'args': [listDoc, new ve.Range( 1, 11 ), [ { 'type': 'list' } ], [], [ { 'type': 'listItem', 'attributes': {'styles': ['bullet']} } ], [] ],
|
||||||
|
'ops': [
|
||||||
|
{ 'type': 'replace',
|
||||||
|
'remove': [ { 'type': 'list' }, { 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } } ],
|
||||||
|
'insert': []
|
||||||
|
},
|
||||||
|
{ 'type': 'retain', 'length': 3 },
|
||||||
|
{ 'type': 'replace',
|
||||||
|
'remove': [ { 'type': '/listItem' }, { 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } } ],
|
||||||
|
'insert': []
|
||||||
|
},
|
||||||
|
{ 'type': 'retain', 'length': 3 },
|
||||||
|
{ 'type': 'replace',
|
||||||
|
'remove': [ { 'type': '/listItem' }, { 'type': '/list' } ],
|
||||||
|
'insert': []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
'replaces a table with a list': {
|
'replaces a table with a list': {
|
||||||
'args': [doc, new ve.Range( 9, 33 ), [ { 'type': 'table' }, { 'type': 'tableSection', 'attributes': { 'style': 'body' } }, { 'type': 'tableRow' }, { 'type': 'tableCell' } ], [ { 'type': 'list' }, { 'type': 'listItem' } ], [], []],
|
'args': [doc, new ve.Range( 9, 33 ), [ { 'type': 'table' }, { 'type': 'tableSection', 'attributes': { 'style': 'body' } }, { 'type': 'tableRow' }, { 'type': 'tableCell' } ], [ { 'type': 'list' }, { 'type': 'listItem' } ], [], []],
|
||||||
'ops': [
|
'ops': [
|
||||||
|
@ -1094,6 +1116,48 @@ QUnit.test( 'newFromWrap', function ( assert ) {
|
||||||
{ 'type': 'retain', 'length': 2 }
|
{ 'type': 'retain', 'length': 2 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
'metadata is preserved on wrap': {
|
||||||
|
'args': [metaDoc, new ve.Range( 1, 10 ), [ { 'type': 'paragraph' } ], [ { 'type': 'heading', 'level': 1 } ], [], [] ],
|
||||||
|
'ops': [
|
||||||
|
{ 'type': 'replace',
|
||||||
|
'remove': [ { 'type': 'paragraph' } ],
|
||||||
|
'insert': [ { 'type': 'heading', 'level': 1 } ],
|
||||||
|
'insertMetadata': metaDoc.getMetadata().slice(0, 1),
|
||||||
|
'removeMetadata': metaDoc.getMetadata().slice(0, 1)
|
||||||
|
},
|
||||||
|
{ 'type': 'retain', 'length': 9 },
|
||||||
|
{ 'type': 'replace',
|
||||||
|
'remove': [ { 'type': '/paragraph' } ],
|
||||||
|
'insert': [ { 'type': '/heading' } ]
|
||||||
|
},
|
||||||
|
{ 'type': 'retain', 'length': 2 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'metadata is preserved on unwrap': {
|
||||||
|
'args': [listMetaDoc, new ve.Range( 1, 11 ), [ { 'type': 'list' } ], [], [ { 'type': 'listItem', 'attributes': {'styles': ['bullet']} } ], [] ],
|
||||||
|
'ops': [
|
||||||
|
{ 'type': 'replace',
|
||||||
|
'remove': [ { 'type': 'list' }, { 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } } ],
|
||||||
|
'insert': [],
|
||||||
|
'insertMetadata': ve.dm.MetaLinearData.static.merge( listMetaDoc.getMetadata().slice(0, 3) ),
|
||||||
|
'removeMetadata': listMetaDoc.getMetadata().slice(0, 3)
|
||||||
|
},
|
||||||
|
{ 'type': 'retain', 'length': 3 },
|
||||||
|
{ 'type': 'replace',
|
||||||
|
'remove': [ { 'type': '/listItem' }, { 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } } ],
|
||||||
|
'insert': [],
|
||||||
|
'insertMetadata': ve.dm.MetaLinearData.static.merge( listMetaDoc.getMetadata().slice(5, 8) ),
|
||||||
|
'removeMetadata': listMetaDoc.getMetadata().slice(5, 8)
|
||||||
|
},
|
||||||
|
{ 'type': 'retain', 'length': 3 },
|
||||||
|
{ 'type': 'replace',
|
||||||
|
'remove': [ { 'type': '/listItem' }, { 'type': '/list' } ],
|
||||||
|
'insert': [],
|
||||||
|
'insertMetadata': ve.dm.MetaLinearData.static.merge( listMetaDoc.getMetadata().slice(10, 13) ),
|
||||||
|
'removeMetadata': listMetaDoc.getMetadata().slice(10, 13)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
'checks integrity of unwrapOuter parameter': {
|
'checks integrity of unwrapOuter parameter': {
|
||||||
'args': [doc, new ve.Range( 13, 32 ), [ { 'type': 'table' } ], [], [], []],
|
'args': [doc, new ve.Range( 13, 32 ), [ { 'type': 'table' } ], [], [], []],
|
||||||
'exception': Error
|
'exception': Error
|
||||||
|
|
|
@ -452,6 +452,22 @@ QUnit.test( 'commit/rollback', function ( assert ) {
|
||||||
data.splice( 2, 2 );
|
data.splice( 2, 2 );
|
||||||
data.splice( 4, 3 );
|
data.splice( 4, 3 );
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'preserves metadata on unwrap': {
|
||||||
|
'data': ve.dm.example.listWithMeta,
|
||||||
|
'calls': [
|
||||||
|
[ 'newFromWrap', new ve.Range( 1, 11 ),
|
||||||
|
[ { 'type': 'list' } ], [],
|
||||||
|
[ { 'type': 'listItem', 'attributes': {'styles': ['bullet']} } ], [] ]
|
||||||
|
],
|
||||||
|
'expected': function ( data ) {
|
||||||
|
data.splice( 35, 1 ); // remove '/list'
|
||||||
|
data.splice( 32, 1 ); // remove '/listItem'
|
||||||
|
data.splice( 20, 1 ); // remove 'listItem'
|
||||||
|
data.splice( 17, 1 ); // remove '/listItem'
|
||||||
|
data.splice( 5, 1 ); // remove 'listItem'
|
||||||
|
data.splice( 2, 1 ); // remove 'list'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -473,10 +489,15 @@ QUnit.test( 'commit/rollback', function ( assert ) {
|
||||||
|
|
||||||
tx = new ve.dm.Transaction();
|
tx = new ve.dm.Transaction();
|
||||||
for ( i = 0; i < cases[msg].calls.length; i++ ) {
|
for ( i = 0; i < cases[msg].calls.length; i++ ) {
|
||||||
// pushReplace needs the document as its first argument
|
// some calls need the document as its first argument
|
||||||
if ( cases[msg].calls[i][0] === 'pushReplace' ) {
|
if ( /^(pushReplace$|new)/.test( cases[msg].calls[i][0] ) ) {
|
||||||
cases[msg].calls[i].splice( 1, 0, testDoc );
|
cases[msg].calls[i].splice( 1, 0, testDoc );
|
||||||
}
|
}
|
||||||
|
// special case static methods of Transaction
|
||||||
|
if ( /^new/.test( cases[msg].calls[i][0] ) ) {
|
||||||
|
tx = ve.dm.Transaction[cases[msg].calls[i][0]].apply( null, cases[msg].calls[i].slice( 1 ) );
|
||||||
|
break;
|
||||||
|
}
|
||||||
tx[cases[msg].calls[i][0]].apply( tx, cases[msg].calls[i].slice( 1 ) );
|
tx[cases[msg].calls[i][0]].apply( tx, cases[msg].calls[i].slice( 1 ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -589,6 +589,118 @@ ve.dm.example.withMetaMetaData = [
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
ve.dm.example.listWithMeta = [
|
||||||
|
// 0 - Beginning of list
|
||||||
|
{
|
||||||
|
'type': 'alienMeta',
|
||||||
|
'attributes': {
|
||||||
|
'domElements': $( '<meta property="one" />' ).toArray()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 'type': '/alienMeta' },
|
||||||
|
{ 'type': 'list' },
|
||||||
|
// 1 - Beginning of first list item
|
||||||
|
{
|
||||||
|
'type': 'alienMeta',
|
||||||
|
'attributes': {
|
||||||
|
'domElements': $( '<meta property="two" />' ).toArray()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 'type': '/alienMeta' },
|
||||||
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } },
|
||||||
|
// 2 - Beginning of paragraph
|
||||||
|
{
|
||||||
|
'type': 'alienMeta',
|
||||||
|
'attributes': {
|
||||||
|
'domElements': $( '<meta property="three" />' ).toArray()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 'type': '/alienMeta' },
|
||||||
|
{ 'type': 'paragraph' },
|
||||||
|
// 3 - Plain "a"
|
||||||
|
{
|
||||||
|
'type': 'alienMeta',
|
||||||
|
'attributes': {
|
||||||
|
'domElements': $( '<meta property="four" />' ).toArray()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 'type': '/alienMeta' },
|
||||||
|
'a',
|
||||||
|
// 4 - End of paragraph
|
||||||
|
{
|
||||||
|
'type': 'alienMeta',
|
||||||
|
'attributes': {
|
||||||
|
'domElements': $( '<meta property="five" />' ).toArray()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 'type': '/alienMeta' },
|
||||||
|
{ 'type': '/paragraph' },
|
||||||
|
// 5 - End of first list item
|
||||||
|
{
|
||||||
|
'type': 'alienMeta',
|
||||||
|
'attributes': {
|
||||||
|
'domElements': $( '<meta property="six" />' ).toArray()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 'type': '/alienMeta' },
|
||||||
|
{ 'type': '/listItem' },
|
||||||
|
// 6 - Beginning of second list item
|
||||||
|
{
|
||||||
|
'type': 'alienMeta',
|
||||||
|
'attributes': {
|
||||||
|
'domElements': $( '<meta property="seven" />' ).toArray()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 'type': '/alienMeta' },
|
||||||
|
{ 'type': 'listItem', 'attributes': { 'styles': ['bullet'] } },
|
||||||
|
// 7 - Beginning of paragraph
|
||||||
|
{
|
||||||
|
'type': 'alienMeta',
|
||||||
|
'attributes': {
|
||||||
|
'domElements': $( '<meta property="eight" />' ).toArray()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 'type': '/alienMeta' },
|
||||||
|
{ 'type': 'paragraph' },
|
||||||
|
// 8 - Plain "b"
|
||||||
|
{
|
||||||
|
'type': 'alienMeta',
|
||||||
|
'attributes': {
|
||||||
|
'domElements': $( '<meta property="nine" />' ).toArray()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 'type': '/alienMeta' },
|
||||||
|
'b',
|
||||||
|
// 9 - End of paragraph
|
||||||
|
{
|
||||||
|
'type': 'alienMeta',
|
||||||
|
'attributes': {
|
||||||
|
'domElements': $( '<meta property="ten" />' ).toArray()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 'type': '/alienMeta' },
|
||||||
|
{ 'type': '/paragraph' },
|
||||||
|
// 10 - End of second list item
|
||||||
|
{
|
||||||
|
'type': 'alienMeta',
|
||||||
|
'attributes': {
|
||||||
|
'domElements': $( '<meta property="eleven" />' ).toArray()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 'type': '/alienMeta' },
|
||||||
|
{ 'type': '/listItem' },
|
||||||
|
// 11 - End of list
|
||||||
|
{
|
||||||
|
'type': 'alienMeta',
|
||||||
|
'attributes': {
|
||||||
|
'domElements': $( '<meta property="twelve" />' ).toArray()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ 'type': '/alienMeta' },
|
||||||
|
{ 'type': '/list' }
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
ve.dm.example.complexTableHtml = '<table><caption>Foo</caption><thead><tr><th>Bar</th></tr></thead>' +
|
ve.dm.example.complexTableHtml = '<table><caption>Foo</caption><thead><tr><th>Bar</th></tr></thead>' +
|
||||||
'<tfoot><tr><td>Baz</td></tr></tfoot><tbody><tr><td>Quux</td><td>Whee</td></tr></tbody></table>';
|
'<tfoot><tr><td>Baz</td></tr></tfoot><tbody><tr><td>Quux</td><td>Whee</td></tr></tbody></table>';
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue