mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-15 18:39:52 +00:00
Added ve.dm.Transaction.newFromRemoval
Also: * Refactored tests * Added tests for ve.dm.Transaction.newFromInsertion * Added tests for ve.dm.Transaction.newFromRemoval * Fixed problems with ve.dm.Transaction.newFromInsertion * Added ve.dm.Node.canBeMergedWith which is partially a port of ve.Node.getCommonAncestorPaths merged with canMerge from within ve.dm.DocumentNode.prepareRemoval from the old ve codebase Change-Id: Ibbc3887d08286d8ab33fd6296487802d65b319fa
This commit is contained in:
parent
2f18605a1a
commit
32bddaf088
|
@ -160,6 +160,38 @@ ve.dm.Node.prototype.getAttribute = function( key ) {
|
|||
return this.attributes[key];
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if this node can be merged with another.
|
||||
*
|
||||
* For two nodes to be mergeable, this node and the given node must either be the same node or:
|
||||
* - Have the same type
|
||||
* - Have the same depth
|
||||
* - Have similar ancestory (each node upstream must have the same type)
|
||||
*
|
||||
* @method
|
||||
* @param {ve.dm.Node} node Node to consider merging with
|
||||
* @returns {Boolean} Nodes can be merged
|
||||
*/
|
||||
ve.dm.Node.prototype.canBeMergedWith = function( node ) {
|
||||
var n1 = this,
|
||||
n2 = node;
|
||||
// Move up from n1 and n2 simultaneously until we find a common ancestor
|
||||
while ( n1 !== n2 ) {
|
||||
if (
|
||||
// Check if we have reached a root (means there's no common ancestor or unequal depth)
|
||||
( n1 === null || n2 === null ) ||
|
||||
// Ensure that types match
|
||||
n1.getType() !== n2.getType()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// Move up
|
||||
n1 = n1.getParent();
|
||||
n2 = n2.getParent();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/* Inheritance */
|
||||
|
||||
ve.extendClass( ve.dm.Node, ve.Node );
|
||||
|
|
|
@ -11,24 +11,110 @@ ve.dm.Transaction = function() {
|
|||
|
||||
/* Static Methods */
|
||||
|
||||
ve.dm.Transaction.newFromInsertion = function( doc, offset, data ) {
|
||||
var toInsert = doc.fixupInsertion( data, offset );
|
||||
var tx = new ve.dm.Transaction();
|
||||
tx.pushRetain( offset );
|
||||
tx.pushReplace( [], toInsert );
|
||||
/**
|
||||
* Generates a transaction that inserts data at a given offset.
|
||||
*
|
||||
* @static
|
||||
* @method
|
||||
* @param {ve.dm.Document} doc Document to create transaction for
|
||||
* @param {Integer} offset Offset to insert at
|
||||
* @param {Array} data Data to insert
|
||||
* @returns {ve.dm.Transaction} Transcation that inserts data
|
||||
*/
|
||||
ve.dm.Transaction.newFromInsertion = function( doc, offset, insertion ) {
|
||||
var tx = new ve.dm.Transaction(),
|
||||
data = doc.getData();
|
||||
// Fix up the insertion
|
||||
insertion = doc.fixupInsertion( insertion, offset );
|
||||
// Retain up to insertion point, if needed
|
||||
if ( offset ) {
|
||||
tx.pushRetain( offset );
|
||||
}
|
||||
// Insert data
|
||||
tx.pushReplace( [], insertion );
|
||||
// Retain to end of document, if needed (for completeness)
|
||||
if ( offset < data.length ) {
|
||||
tx.pushRetain( data.length - offset );
|
||||
}
|
||||
return tx;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a transaction which removes data from a given range.
|
||||
*
|
||||
* There are three possible results from a removal:
|
||||
* 1. Remove content only
|
||||
* - Occurs when the range starts and ends on elements of different type, depth or ancestry
|
||||
* 2. Remove entire elements and their content
|
||||
* - Occurs when the range spans accross an entire element
|
||||
* 3. Merge two elements by removing the end of one and the beginning of another
|
||||
* - Occurs when the range starts and ends on elements of similar type, depth and ancestry
|
||||
*
|
||||
* This function uses the following logic to decide what to actually remove:
|
||||
* 1. Elements are only removed if range being removed covers the entire element
|
||||
* 2. Elements can only be merged if {ve.dm.Node.canBeMergedWith} returns true
|
||||
* 3. Merges take place at the highest common ancestor
|
||||
*
|
||||
* @method
|
||||
* @param {ve.dm.Document} doc Document to create transaction for
|
||||
* @param {ve.Range} range Range of data to remove
|
||||
* @returns {ve.dm.Transaction} Transcation that removes data
|
||||
* @throws 'Invalid range, can not remove from {range.start} to {range.end}'
|
||||
*/
|
||||
ve.dm.Transaction.newFromRemoval = function( doc, range ) {
|
||||
// Implement me!
|
||||
};
|
||||
|
||||
ve.dm.Transaction.newFromReplacement = function( doc, range, data ) {
|
||||
// Implement me!
|
||||
var tx = new ve.dm.Transaction(),
|
||||
data = doc.getData();
|
||||
// Normalize and validate range
|
||||
range.normalize();
|
||||
if ( range.start === range.end ) {
|
||||
// Empty range, nothing to remove, retain up to the end of the document (for completeness)
|
||||
tx.pushRetain( data.length );
|
||||
return tx;
|
||||
}
|
||||
// Select nodes and validate selection
|
||||
var selection = doc.selectNodes( range, 'leaves' );
|
||||
if ( selection.length === 0 ) {
|
||||
// Empty selection? Something is wrong!
|
||||
throw 'Invalid range, can not remove from ' + range.start + ' to ' + range.end;
|
||||
}
|
||||
// Decide whether to merge or strip
|
||||
if ( selection[0].node.canBeMergedWith( selection[selection.length - 1].node ) ) {
|
||||
// Retain to the start of the range
|
||||
if ( range.start > 0 ) {
|
||||
tx.pushRetain( range.start );
|
||||
}
|
||||
// Remove all data in a given range.
|
||||
tx.pushReplace( data.slice( range.start, range.end ), [] );
|
||||
// Retain up to the end of the document, if needed (for completeness)
|
||||
if ( range.end < data.length ) {
|
||||
tx.pushRetain( data.length - range.end );
|
||||
}
|
||||
} else {
|
||||
var offset = 0,
|
||||
nodeRange;
|
||||
for ( var i = 0; i < selection.length; i++ ) {
|
||||
nodeRange = selection[i].nodeRange;
|
||||
// Retain up to where the next removal starts
|
||||
if ( nodeRange.start > offset ) {
|
||||
tx.pushRetain( nodeRange.start - offset );
|
||||
}
|
||||
// Remove data
|
||||
if ( nodeRange.getLength() ) {
|
||||
tx.pushReplace( data.slice( nodeRange.start, nodeRange.end ), [] );
|
||||
}
|
||||
// Advance to the next node
|
||||
offset = nodeRange.end;
|
||||
}
|
||||
// Retain up to the end of the document, if needed (for completeness)
|
||||
if ( offset < data.length ) {
|
||||
tx.pushRetain( data.length - offset );
|
||||
}
|
||||
}
|
||||
return tx;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a transaction that changes an attribute.
|
||||
* Generates a transaction that changes an attribute.
|
||||
*
|
||||
* @static
|
||||
* @method
|
||||
|
@ -67,7 +153,7 @@ ve.dm.Transaction.newFromAttributeChange = function( doc, offset, key, value ) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Get a transaction that annotates content.
|
||||
* Generates a transaction that annotates content.
|
||||
*
|
||||
* @static
|
||||
* @method
|
||||
|
|
|
@ -1,7 +1,200 @@
|
|||
module( 've.dm.Transaction' );
|
||||
|
||||
/* Methods */
|
||||
|
||||
ve.dm.Transaction.runBuilderTests = function( cases ) {
|
||||
for ( var msg in cases ) {
|
||||
var tx = new ve.dm.Transaction();
|
||||
for ( var i = 0; i < cases[msg].calls.length; i++ ) {
|
||||
tx[cases[msg].calls[i][0]].apply( tx, cases[msg].calls[i].slice( 1 ) );
|
||||
}
|
||||
deepEqual( tx.getOperations(), cases[msg].ops, msg + ': operations match' );
|
||||
deepEqual( tx.getLengthDifference(), cases[msg].diff, msg + ': length differences match' );
|
||||
}
|
||||
};
|
||||
|
||||
ve.dm.Transaction.runConstructorTests = function( constructor, cases ) {
|
||||
for ( var msg in cases ) {
|
||||
if ( cases[msg].ops ) {
|
||||
var tx = constructor.apply(
|
||||
ve.dm.Transaction, cases[msg].args
|
||||
);
|
||||
deepEqual( tx.getOperations(), cases[msg].ops, msg + ': operations match' );
|
||||
} else if ( cases[msg].exception ) {
|
||||
/*jshint loopfunc:true*/
|
||||
raises( function() {
|
||||
var tx = constructor.apply(
|
||||
ve.dm.Transaction, cases[msg].args
|
||||
);
|
||||
}, cases[msg].exception, msg + ': throw exception' );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* Tests */
|
||||
|
||||
test( 'newFromInsertion', function() {
|
||||
var doc = new ve.dm.Document( ve.dm.example.data ),
|
||||
cases = {
|
||||
'content in first element': {
|
||||
'args': [doc, 1, ['1', '2', '3']],
|
||||
'ops': [
|
||||
{ 'type': 'retain', 'length': 1 },
|
||||
{
|
||||
'type': 'replace',
|
||||
'remove': [],
|
||||
'insert': ['1', '2', '3']
|
||||
},
|
||||
{ 'type': 'retain', 'length': 58 }
|
||||
]
|
||||
},
|
||||
'before first element': {
|
||||
'args': [doc, 0, [{ 'type': 'paragraph' }, '1', { 'type': '/paragraph' }]],
|
||||
'ops': [
|
||||
{
|
||||
'type': 'replace',
|
||||
'remove': [],
|
||||
'insert': [{ 'type': 'paragraph' }, '1', { 'type': '/paragraph' }]
|
||||
},
|
||||
{ 'type': 'retain', 'length': 59 }
|
||||
]
|
||||
},
|
||||
'after last element': {
|
||||
'args': [doc, 59, [{ 'type': 'paragraph' }, '1', { 'type': '/paragraph' }]],
|
||||
'ops': [
|
||||
{ 'type': 'retain', 'length': 59 },
|
||||
{
|
||||
'type': 'replace',
|
||||
'remove': [],
|
||||
'insert': [{ 'type': 'paragraph' }, '1', { 'type': '/paragraph' }]
|
||||
}
|
||||
]
|
||||
},
|
||||
'split paragraph': {
|
||||
'args': [doc, 9, ['1', { 'type': '/paragraph' }, { 'type': 'paragraph' }]],
|
||||
'ops': [
|
||||
{ 'type': 'retain', 'length': 9 },
|
||||
{
|
||||
'type': 'replace',
|
||||
'remove': [],
|
||||
'insert': ['1', { 'type': '/paragraph' }, { 'type': 'paragraph' }]
|
||||
},
|
||||
{ 'type': 'retain', 'length': 50 }
|
||||
]
|
||||
}
|
||||
};
|
||||
ve.dm.Transaction.runConstructorTests( ve.dm.Transaction.newFromInsertion, cases );
|
||||
} );
|
||||
|
||||
test( 'newFromRemoval', function() {
|
||||
var doc = new ve.dm.Document( ve.dm.example.data ),
|
||||
cases = {
|
||||
'content in first element': {
|
||||
'args': [doc, new ve.Range( 1, 3 )],
|
||||
'ops': [
|
||||
{ 'type': 'retain', 'length': 1 },
|
||||
{
|
||||
'type': 'replace',
|
||||
'remove': [
|
||||
'a',
|
||||
['b', { '{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' } }]
|
||||
],
|
||||
'insert': []
|
||||
},
|
||||
{ 'type': 'retain', 'length': 56 }
|
||||
]
|
||||
},
|
||||
'content in last element': {
|
||||
'args': [doc, new ve.Range( 57, 58 )],
|
||||
'ops': [
|
||||
{ 'type': 'retain', 'length': 57 },
|
||||
{
|
||||
'type': 'replace',
|
||||
'remove': ['m'],
|
||||
'insert': []
|
||||
},
|
||||
{ 'type': 'retain', 'length': 1 }
|
||||
]
|
||||
},
|
||||
'first element': {
|
||||
'args': [doc, new ve.Range( 0, 5 )],
|
||||
'ops': [
|
||||
{
|
||||
'type': 'replace',
|
||||
'remove': [
|
||||
{ 'type': 'heading', 'attributes': { 'level': 1 } },
|
||||
'a',
|
||||
['b', { '{"type":"textStyle/bold"}': { 'type': 'textStyle/bold' } }],
|
||||
['c', { '{"type":"textStyle/italic"}': { 'type': 'textStyle/italic' } }],
|
||||
{ 'type': '/heading' }
|
||||
],
|
||||
'insert': []
|
||||
},
|
||||
{ 'type': 'retain', 'length': 54 }
|
||||
]
|
||||
},
|
||||
'middle element with image': {
|
||||
'args': [doc, new ve.Range( 36, 40 )],
|
||||
'ops': [
|
||||
{ 'type': 'retain', 'length': 36 },
|
||||
{
|
||||
'type': 'replace',
|
||||
'remove': [
|
||||
'h',
|
||||
{ 'type': 'image', 'attributes': { 'html/src': 'image.png' } },
|
||||
{ 'type': '/image' },
|
||||
'i'
|
||||
],
|
||||
'insert': []
|
||||
},
|
||||
{ 'type': 'retain', 'length': 19 }
|
||||
]
|
||||
},
|
||||
'last element': {
|
||||
'args': [doc, new ve.Range( 56, 59 )],
|
||||
'ops': [
|
||||
{ 'type': 'retain', 'length': 56 },
|
||||
{
|
||||
'type': 'replace',
|
||||
'remove': [{ 'type': 'paragraph' }, 'm', { 'type': '/paragraph' }],
|
||||
'insert': []
|
||||
}
|
||||
]
|
||||
},
|
||||
'merge last two elements': {
|
||||
'args': [doc, new ve.Range( 55, 57 )],
|
||||
'ops': [
|
||||
{ 'type': 'retain', 'length': 55 },
|
||||
{
|
||||
'type': 'replace',
|
||||
'remove': [{ 'type': '/paragraph' }, { 'type': 'paragraph' }],
|
||||
'insert': []
|
||||
},
|
||||
{ 'type': 'retain', 'length': 2 }
|
||||
]
|
||||
},
|
||||
'strip out of paragraph in tableCell and paragraph in listItem': {
|
||||
'args': [doc, new ve.Range( 9, 15 )],
|
||||
'ops': [
|
||||
{ 'type': 'retain', 'length': 9 },
|
||||
{
|
||||
'type': 'replace',
|
||||
'remove': ['d'],
|
||||
'insert': []
|
||||
},
|
||||
{ 'type': 'retain', 'length': 4 },
|
||||
{
|
||||
'type': 'replace',
|
||||
'remove': ['e'],
|
||||
'insert': []
|
||||
},
|
||||
{ 'type': 'retain', 'length': 44 }
|
||||
]
|
||||
}
|
||||
};
|
||||
ve.dm.Transaction.runConstructorTests( ve.dm.Transaction.newFromRemoval, cases );
|
||||
} );
|
||||
|
||||
test( 'newFromAttributeChange', function() {
|
||||
var doc = new ve.dm.Document( ve.dm.example.data ),
|
||||
cases = {
|
||||
|
@ -39,21 +232,7 @@ test( 'newFromAttributeChange', function() {
|
|||
'exception': /^Can not set attributes on closing element$/
|
||||
}
|
||||
};
|
||||
for ( var msg in cases ) {
|
||||
if ( cases[msg].ops ) {
|
||||
var tx = ve.dm.Transaction.newFromAttributeChange.apply(
|
||||
ve.dm.Transaction, cases[msg].args
|
||||
);
|
||||
deepEqual( tx.getOperations(), cases[msg].ops, msg + ': operations match' );
|
||||
} else if ( cases[msg].exception ) {
|
||||
/*jshint loopfunc:true*/
|
||||
raises( function() {
|
||||
var tx = ve.dm.Transaction.newFromAttributeChange.apply(
|
||||
ve.dm.Transaction, cases[msg].args
|
||||
);
|
||||
}, cases[msg].exception, msg + ': throw exception' );
|
||||
}
|
||||
}
|
||||
ve.dm.Transaction.runConstructorTests( ve.dm.Transaction.newFromAttributeChange, cases );
|
||||
} );
|
||||
|
||||
test( 'newFromAnnotation', function() {
|
||||
|
@ -154,15 +333,10 @@ test( 'newFromAnnotation', function() {
|
|||
]
|
||||
}
|
||||
};
|
||||
for ( var msg in cases ) {
|
||||
var tx = ve.dm.Transaction.newFromAnnotation.apply(
|
||||
ve.dm.Transaction, cases[msg].args
|
||||
);
|
||||
deepEqual( tx.getOperations(), cases[msg].ops, msg + ': operations match' );
|
||||
}
|
||||
ve.dm.Transaction.runConstructorTests( ve.dm.Transaction.newFromAnnotation, cases );
|
||||
} );
|
||||
|
||||
test( 'constructor', function() {
|
||||
test( 'pushRetain', function() {
|
||||
var cases = {
|
||||
'retain': {
|
||||
'calls': [['pushRetain', 5]],
|
||||
|
@ -173,10 +347,16 @@ test( 'constructor', function() {
|
|||
'calls': [['pushRetain', 5], ['pushRetain', 3]],
|
||||
'ops': [{ 'type': 'retain', 'length': 8 }],
|
||||
'diff': 0
|
||||
},
|
||||
}
|
||||
};
|
||||
ve.dm.Transaction.runBuilderTests( cases );
|
||||
} );
|
||||
|
||||
test( 'pushReplace', function() {
|
||||
var cases = {
|
||||
'insert': {
|
||||
'calls': [
|
||||
['pushReplace', [], [{ 'type': 'paragraph' }, 'a', 'b', 'c', { 'type': '/paragraph' }]]
|
||||
['pushReplace', [], [{ 'type': 'paragraph' }, 'a', 'b', 'c', { 'type': '/paragraph' }]]
|
||||
],
|
||||
'ops': [
|
||||
{
|
||||
|
@ -291,7 +471,13 @@ test( 'constructor', function() {
|
|||
}
|
||||
],
|
||||
'diff': 0
|
||||
},
|
||||
}
|
||||
};
|
||||
ve.dm.Transaction.runBuilderTests( cases );
|
||||
} );
|
||||
|
||||
test( 'pushReplaceElementAttribute', function() {
|
||||
var cases = {
|
||||
'replace element attribute': {
|
||||
'calls': [
|
||||
['pushReplaceElementAttribute', 'style', 'bullet', 'number']
|
||||
|
@ -326,7 +512,13 @@ test( 'constructor', function() {
|
|||
}
|
||||
],
|
||||
'diff': 0
|
||||
},
|
||||
}
|
||||
};
|
||||
ve.dm.Transaction.runBuilderTests( cases );
|
||||
} );
|
||||
|
||||
test( 'push*Annotating', function() {
|
||||
var cases = {
|
||||
'start annotating': {
|
||||
'calls': [
|
||||
['pushStartAnnotating', 'set', { 'type': 'textStyle/bold' }]
|
||||
|
@ -398,12 +590,5 @@ test( 'constructor', function() {
|
|||
'diff': 0
|
||||
}
|
||||
};
|
||||
for ( var msg in cases ) {
|
||||
var tx = new ve.dm.Transaction();
|
||||
for ( var i = 0; i < cases[msg].calls.length; i++ ) {
|
||||
tx[cases[msg].calls[i][0]].apply( tx, cases[msg].calls[i].slice( 1 ) );
|
||||
}
|
||||
deepEqual( tx.getOperations(), cases[msg].ops, msg + ': operations match' );
|
||||
deepEqual( tx.getLengthDifference(), cases[msg].diff, msg + ': length differences match' );
|
||||
}
|
||||
ve.dm.Transaction.runBuilderTests( cases );
|
||||
} );
|
||||
|
|
Loading…
Reference in a new issue