jenkins-bot has submitted this change and it was merged.
Change subject: Refactor convert to use isolateAndUnwrap
......................................................................
Refactor convert to use isolateAndUnwrap
isolateAndUnwrap now unwraps to a level determined by a target type
i.e. the type you are going to convert to.
Also in this commit wrap/unwrap/rewrap have been refactored to use
getLengthDifference. unwrap now takes an inner/outer unwrap depth.
Change-Id: I3c6249de43232a9ef64f498a0aaf66b1c44973f2
---
M demos/ve/pages/isolation.html
M modules/ve/actions/ve.FormatAction.js
M modules/ve/dm/ve.dm.SurfaceFragment.js
M modules/ve/test/actions/ve.FormatAction.test.js
M modules/ve/test/dm/ve.dm.SurfaceFragment.test.js
M modules/ve/test/dm/ve.dm.example.js
6 files changed, 298 insertions(+), 199 deletions(-)
Approvals:
Catrope: Looks good to me, approved
jenkins-bot: Verified
diff --git a/demos/ve/pages/isolation.html b/demos/ve/pages/isolation.html
index 38869c6..620c670 100644
--- a/demos/ve/pages/isolation.html
+++ b/demos/ve/pages/isolation.html
@@ -1 +1 @@
-<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>Paragraph<ul><li>Item
4</li><li>Item 5</li><li>Item 6</li></ul><table><tbody><tr><td>Cell
1</td><td>Cell 2</td><td>Cell 3</td></tr><tr><td>Cell
4</td></tr></tbody></table>Not allowed by dm:<ul><li><h1>Title in
list</h1></li><li><pre>Preformatted in list</pre></li></ul>
\ No newline at end of file
+<ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>Paragraph<ul><li>Item
4</li><li>Item 5</li><li>Item 6</li></ul><table><tbody><tr><td>Cell
1</td><td>Cell 2</td><td>Cell 3</td></tr><tr><td>Cell
4</td></tr></tbody></table>Not allowed by dm:<ul><li><h1>Title in
list</h1></li><li><pre>Preformatted in
list</pre></li></ul><ul><li><ol><li>Nested 1</li><li>Nested 2</li><li>Nested
3</li></ol></li></ul><ul><li><p>P1</p><p>P2</p><p>P3</p></li></ul>
\ No newline at end of file
diff --git a/modules/ve/actions/ve.FormatAction.js
b/modules/ve/actions/ve.FormatAction.js
index 901aaf0..2db4952 100644
--- a/modules/ve/actions/ve.FormatAction.js
+++ b/modules/ve/actions/ve.FormatAction.js
@@ -45,86 +45,33 @@
* @param {Object} attributes
*/
ve.FormatAction.prototype.convert = function ( type, attributes ) {
- var selected, prevList, firstInList, lastInList, i, contentBranch,
listItem, txs,
+ var selected, i, length, contentBranch, txs,
surfaceView = this.surface.getView(),
surfaceModel = this.surface.getModel(),
selection = surfaceModel.getSelection(),
- doc = surfaceModel.getDocument();
- if ( type !== 'paragraph' ) {
- // We can't have headings or pre's in a list, so if we're
trying to convert
- // things that are in lists to a heading or a pre, split the
list
- selected = doc.selectNodes( selection, 'leaves' );
- for ( i = 0; i < selected.length; i++ ) {
- contentBranch = selected[i].node.isContent() ?
- selected[i].node.getParent() :
- selected[i].node;
- // Check if it's in a list
- listItem = contentBranch;
- // TODO: Add getMatchingAncestor to ve.dm.Node and use
it here
- while ( listItem && listItem.getType() !== 'listItem' )
{
- listItem = listItem.getParent();
- }
- if ( !listItem || listItem.getParent() !== prevList ) {
- // Not in a list or in a different list
- if ( prevList ) {
- // Split and unwrap prevList
- selection =
ve.FormatAction.splitAndUnwrap(
- surfaceModel, prevList,
firstInList, lastInList, selection
- );
- // Reset
- prevList = firstInList = lastInList =
undefined;
- }
- if ( listItem ) {
- prevList = listItem.getParent();
- firstInList = listItem;
- lastInList = firstInList;
- }
- } else {
- // This node is in the current list
- lastInList = listItem;
- }
- }
- if ( prevList ) {
- // Split and unwrap prevList
- selection = ve.FormatAction.splitAndUnwrap(
- surfaceModel, prevList, firstInList,
lastInList, selection
- );
- }
+ fragmentForSelection = new ve.dm.SurfaceFragment( surfaceModel,
selection, true ),
+ doc = surfaceModel.getDocument(),
+ fragments = [];
+
+ // We can't have headings or pre's in a list, so if we're trying to
convert
+ // things that are in lists to a heading or a pre, split the list
+ selected = doc.selectNodes( selection, 'leaves' );
+ for ( i = 0, length = selected.length; i < length; i++ ) {
+ contentBranch = selected[i].node.isContent() ?
+ selected[i].node.getParent() :
+ selected[i].node;
+
+ fragments.push( new ve.dm.SurfaceFragment( surfaceModel,
contentBranch.getOuterRange(), true ) );
}
+
+ for( i = 0, length = fragments.length; i < length; i++ ) {
+ fragments[i].isolateAndUnwrap( type );
+ }
+ selection = fragmentForSelection.getRange();
+
txs = ve.dm.Transaction.newFromContentBranchConversion( doc, selection,
type, attributes );
surfaceModel.change( txs, selection );
surfaceView.showSelection( selection );
-};
-
-/**
- * Split a list up by unwrapping some of its items.
- *
- * TODO: Refactor functionality into {ve.dm.SurfaceFragment}.
- *
- * @param {ve.dm.Surface} model
- * @param {ve.dm.ListNode} list
- * @param {ve.dm.ListItemNode} firstItem
- * @param {ve.dm.ListItemNode} lastItem
- * @returns {ve.Range} selection
- */
-ve.FormatAction.splitAndUnwrap = function ( model, list, firstItem, lastItem,
selection ) {
- var doc = model.getDocument(),
- start = firstItem.getOuterRange().start,
- end = lastItem.getOuterRange().end,
- fragment = new ve.dm.SurfaceFragment( model, new ve.Range(
start, end ), true ),
- fragmentForSelection = new ve.dm.SurfaceFragment( model,
selection, true ),
- tx;
-
- fragment.isolate();
- selection = fragmentForSelection.getRange();
-
- // Unwrap the list
- tx = ve.dm.Transaction.newFromWrap( doc, fragment.getRange(),
- [{ 'type': 'list' }], [], [{ 'type': 'listItem' }], []
- );
- selection = tx.translateRange( selection );
- model.change( tx, selection );
- return selection;
};
/* Registration */
diff --git a/modules/ve/dm/ve.dm.SurfaceFragment.js
b/modules/ve/dm/ve.dm.SurfaceFragment.js
index 5cb587c..87ada3f 100644
--- a/modules/ve/dm/ve.dm.SurfaceFragment.js
+++ b/modules/ve/dm/ve.dm.SurfaceFragment.js
@@ -574,25 +574,50 @@
if ( !ve.isArray( wrapper ) ) {
wrapper = [wrapper];
}
- var tx = ve.dm.Transaction.newFromWrap( this.document, this.range, [],
[], [], wrapper );
- this.range = tx.translateRange( this.range );
- this.surface.change( tx, !this.noAutoSelect && this.range );
+ var tx = ve.dm.Transaction.newFromWrap( this.document, this.range, [],
[], [], wrapper ),
+ newRange = new ve.Range( this.range.start, this.range.end +
tx.getLengthDifference() );
+ this.surface.change( tx, !this.noAutoSelect && newRange );
+ this.range = newRange;
return this;
};
/**
- * Unwrap each node in the fragment out of one or more elements.
+ * Unwrap nodes in the fragment out of one or more elements.
+ *
+ * Example:
+ * // fragment is a selection of:
<ul>「<li><p>a</p></li><li><p>b</p></li>」</ul>
+ * fragment.unwrapNodes( 1, 1 )
+ * // fragment is now a selection of: 「<p>a</p><p>b</p>」
*
* @method
- * @param {string|string[]} type Node types to unwrap, or array of node types
to unwrap
+ * @param {number} outerDepth Number of nodes outside the selection to unwrap
+ * @param {number} innerDepth Number of nodes inside the selection to unwrap
* @chainable
*/
-ve.dm.SurfaceFragment.prototype.unwrapNodes = function () {
+ve.dm.SurfaceFragment.prototype.unwrapNodes = function ( outerDepth,
innerDepth ) {
// Handle null fragment
if ( !this.surface ) {
return this;
}
- // TODO: Implement
+ var i, tx, newRange, innerUnwrapper = [], outerUnwrapper = [];
+
+ if ( this.range.end - this.range.start < innerDepth * 2 ) {
+ throw new Error( 'cannot unwrap by greater depth than maximum
theoretical depth of selection' );
+ }
+
+ for ( i = 0; i < innerDepth; i++ ) {
+ innerUnwrapper.push(
this.surface.getDocument().data[this.range.start + i] );
+ }
+ for ( i = outerDepth; i > 0; i-- ) {
+ outerUnwrapper.push(
this.surface.getDocument().data[this.range.start - i] );
+ }
+
+ tx = ve.dm.Transaction.newFromWrap( this.document, this.range,
outerUnwrapper, [], innerUnwrapper, [] );
+ newRange = new ve.Range( this.range.start - outerDepth, this.range.end
+ outerDepth + tx.getLengthDifference() );
+ this.surface.change( tx, !this.noAutoSelect && newRange );
+
+ this.range = newRange;
+
return this;
};
@@ -602,19 +627,46 @@
* A wrapper object is a linear model element; a plain object containing a
type property and an
* optional attributes property.
*
+ * Example:
+ * // fragment is a selection of:
<dl><dt><p>a</p></dt></dl><dl><dt><p>b</p></dt></dl>
+ * fragment.rewrapNodes(
+ * 2,
+ * [{ 'type': 'list', 'attributes': { 'style': 'bullet' } }, { 'type':
'listItem' }]
+ * )
+ * // fragment is now a selection of:
<ul><li><p>a</p></li></ul><ul><li><p>b</p></li></ul>
+ *
* @method
- * @param {string|string[]} type Node types to unwrap, or array of node types
to unwrap
+ * @param {number} depth Number of nodes to unwrap
* @param {Object|Object[]} wrapper Wrapper object, or array of wrapper
objects (see above)
* @param {string} wrapper.type Node type of wrapper
* @param {Object} [wrapper.attributes] Attributes of wrapper
* @chainable
*/
-ve.dm.SurfaceFragment.prototype.rewrapNodes = function () {
+ve.dm.SurfaceFragment.prototype.rewrapNodes = function ( depth, wrapper ) {
// Handle null fragment
if ( !this.surface ) {
return this;
}
- // TODO: Implement
+ var i, tx, newRange, unwrapper = [];
+
+ if ( !ve.isArray( wrapper ) ) {
+ wrapper = [wrapper];
+ }
+
+ if ( this.range.end - this.range.start < depth * 2 ) {
+ throw new Error( 'cannot unwrap by greater depth than maximum
theoretical depth of selection' );
+ }
+
+ for ( i = 0; i < depth; i++ ) {
+ unwrapper.push(
this.surface.getDocument().data[this.range.start + i] );
+ }
+
+ tx = ve.dm.Transaction.newFromWrap( this.document, this.range, [], [],
unwrapper, wrapper );
+ newRange = new ve.Range( this.range.start, this.range.end +
tx.getLengthDifference() );
+ this.surface.change( tx, !this.noAutoSelect && newRange );
+
+ this.range = newRange;
+
return this;
};
@@ -649,49 +701,8 @@
wrapper = [wrapper];
}
- newRange = new ve.Range( this.range.start, this.range.end + (
wrapper.length * 2 ) );
-
tx = ve.dm.Transaction.newFromWrap( this.document, this.range, [],
wrapper, [], [] );
- this.surface.change( tx, !this.noAutoSelect && newRange );
-
- this.range = newRange;
-
- return this;
-};
-
-/**
- * Unwrap nodes in the fragment out of one or more elements.
- *
- * Unwrap only removes elements from inside the fragment.
- *
- * Example:
- * // fragment is a selection of: <ul><li><p>text</p></li></ul>
- * fragment.unwrapAllNodes( 2 );
- * // fragment is now a selection of: <p>text</p>
- *
- * @method
- * @param {number} depth Number of nodes to unwrap
- * @chainable
- */
-ve.dm.SurfaceFragment.prototype.unwrapAllNodes = function ( depth ) {
- // Handle null fragment
- if ( !this.surface ) {
- return this;
- }
- var i, tx, newRange, unwrapper = [],
- innerRange = new ve.Range( this.range.start + depth,
this.range.end - depth );
-
- if ( this.range.end - this.range.start < depth * 2 ) {
- throw new Error( 'cannot unwrap by greater depth than maximum
theoretical depth of selection' );
- }
-
- for ( i = 0; i < depth; i++ ) {
- unwrapper.push(
this.surface.getDocument().data[this.range.start + i] );
- }
-
- newRange = new ve.Range( this.range.start, this.range.end - ( depth * 2
) );
-
- tx = ve.dm.Transaction.newFromWrap( this.document, innerRange,
unwrapper, [], [], [] );
+ newRange = new ve.Range( this.range.start, this.range.end +
tx.getLengthDifference() );
this.surface.change( tx, !this.noAutoSelect && newRange );
this.range = newRange;
@@ -705,8 +716,13 @@
* A wrapper object is a linear model element; a plain object containing a
type property and an
* optional attributes property.
*
+ * Example:
+ * // fragment is a selection of: <h1><p>a</p><p>b</p></h1>
+ * fragment.rewrapAllNodes( 1, { 'type': 'heading', 'attributes' : {
'level' : 2 } } );
+ * // fragment is now a selection of: <h2><p>a</p><p>b</p></h2>
+ *
* @method
- * @param {string|string[]} type Node types to unwrap, or array of node types
to unwrap
+ * @param {number} depth Number of nodes to unwrap
* @param {Object|Object[]} wrapper Wrapper object, or array of wrapper
objects (see above)
* @param {string} wrapper.type Node type of wrapper
* @param {Object} [wrapper.attributes] Attributes of wrapper
@@ -718,8 +734,11 @@
return this;
}
var i, tx, newRange, unwrapper = [],
- depthChange = wrapper.length - depth,
innerRange = new ve.Range( this.range.start + depth,
this.range.end - depth );
+
+ if ( !ve.isArray( wrapper ) ) {
+ wrapper = [wrapper];
+ }
if ( this.range.end - this.range.start < depth * 2 ) {
throw new Error( 'cannot unwrap by greater depth than maximum
theoretical depth of selection' );
@@ -729,9 +748,8 @@
unwrapper.push(
this.surface.getDocument().data[this.range.start + i] );
}
- newRange = new ve.Range( this.range.start, this.range.end + (
depthChange * 2 ) );
-
tx = ve.dm.Transaction.newFromWrap( this.document, innerRange,
unwrapper, wrapper, [], [] );
+ newRange = new ve.Range( this.range.start, this.range.end +
tx.getLengthDifference() );
this.surface.change( tx, !this.noAutoSelect && newRange );
this.range = newRange;
@@ -740,26 +758,33 @@
};
/**
- * Isolates the nodes in a fragment.
+ * Isolates the nodes in a fragment then unwraps them.
*
- * The node selection is expanded to siblings and then these are isolated such
that they are the
- * sole children of a parent element which can be placed anywhere.
+ * The node selection is expanded to siblings. These are isolated such that
they are the
+ * sole children of the nearest parent element which can 'type' can exist in.
+ *
+ * The new isolated selection is then safely unwrapped.
*
* @method
+ * @param {string} type Node type to isolate for
* @chainable
*/
-ve.dm.SurfaceFragment.prototype.isolate = function () {
+ve.dm.SurfaceFragment.prototype.isolateAndUnwrap = function ( isolateForType )
{
// Handle null fragment
if ( !this.surface ) {
return this;
}
var nodes, startSplitNode, endSplitNode, tx,
startOffset, endOffset,
+ outerDepth = 0,
+ factory = ve.dm.nodeFactory,
+ allowedParents = factory.getSuggestedParentNodeTypes(
isolateForType ),
startSplitRequired = false,
endSplitRequired = false,
startSplitNodes = [],
endSplitNodes = [],
- fragment = this;
+ fragment = this,
+ newRange = new ve.Range( this.range.start, this.range.end );
function createSplits( splitNodes, insertBefore ) {
var i, length,
@@ -774,38 +799,40 @@
}
}
- tx = ve.dm.Transaction.newFromInsertion( fragment.document,
insertBefore ? startOffset : endOffset, data );
- fragment.surface.change( tx, !fragment.noAutoSelect &&
tx.translateRange( fragment.range ) );
+ tx = ve.dm.Transaction.newFromInsertion(
fragment.getDocument(), insertBefore ? startOffset : endOffset, data );
+ newRange = new ve.Range( newRange.start, newRange.end +
tx.getLengthDifference() );
+ fragment.surface.change( tx, !fragment.noAutoSelect && newRange
);
startOffset += startOffsetChange;
endOffset += endOffsetChange;
}
- nodes = this.document.selectNodes( this.range, 'siblings' );
+ nodes = this.getDocument().selectNodes( this.range, 'siblings' );
// Find start split point, if required
startSplitNode = nodes[0].node;
startOffset = startSplitNode.getOuterRange().start;
- while ( startSplitNode.constructor.static.parentNodeTypes !== null ) {
- if ( startSplitNode.parent.indexOf( startSplitNode ) > 0 ) {
+ while( allowedParents !== null && ve.indexOf(
startSplitNode.getParent().type, allowedParents ) === -1 ) {
+ if ( startSplitNode.getParent().indexOf( startSplitNode ) > 0 )
{
startSplitRequired = true;
}
- startSplitNode = startSplitNode.parent;
+ startSplitNode = startSplitNode.getParent();
if ( startSplitRequired ) {
startSplitNodes.unshift(startSplitNode);
} else {
startOffset = startSplitNode.getOuterRange().start;
}
+ outerDepth++;
}
// Find end split point, if required
endSplitNode = nodes[nodes.length - 1].node;
endOffset = endSplitNode.getOuterRange().end;
- while ( endSplitNode.constructor.static.parentNodeTypes !== null ) {
- if ( endSplitNode.parent.indexOf( endSplitNode ) <
endSplitNode.parent.getChildren().length - 1 ) {
+ while( allowedParents !== null && ve.indexOf(
endSplitNode.getParent().type, allowedParents ) === -1 ) {
+ if ( endSplitNode.getParent().indexOf( endSplitNode ) <
endSplitNode.getParent().getChildren().length - 1 ) {
endSplitRequired = true;
}
- endSplitNode = endSplitNode.parent;
+ endSplitNode = endSplitNode.getParent();
if ( endSplitRequired ) {
endSplitNodes.unshift(endSplitNode);
} else {
@@ -821,5 +848,7 @@
createSplits( endSplitNodes, false );
}
+ this.unwrapNodes( outerDepth, 0 );
+
return this;
};
diff --git a/modules/ve/test/actions/ve.FormatAction.test.js
b/modules/ve/test/actions/ve.FormatAction.test.js
index a39fcae..09a55aa 100644
--- a/modules/ve/test/actions/ve.FormatAction.test.js
+++ b/modules/ve/test/actions/ve.FormatAction.test.js
@@ -31,36 +31,36 @@
created = { 'changed': { 'created': 1 } },
createdAndRebuilt = { 'changed': { 'created': 1, 'rebuilt': 1 }
};
- runConverterTest( assert, new ve.Range( 14, 16 ), 'heading', { level: 2
}, new ve.Range( 14, 16 ), function( data ) {
+ runConverterTest( assert, new ve.Range( 14, 16 ), 'MWheading', { level:
2 }, new ve.Range( 14, 16 ), function( data ) {
data[0].internal = rebuilt;
- data.splice( 11, 2, { 'type': '/list' }, { 'type': 'heading',
'attributes': { 'level': 2 }, 'internal': created } );
- data.splice( 19, 2, { 'type': '/heading' }, { 'type': 'list',
'attributes': { 'style': 'bullet' }, 'internal': createdAndRebuilt } );
- }, 'converting partial selection of list item "Item 2" to level 2
heading' );
+ data.splice( 11, 2, { 'type': '/list' }, { 'type': 'MWheading',
'attributes': { 'level': 2 }, 'internal': created } );
+ data.splice( 19, 2, { 'type': '/MWheading' }, { 'type': 'list',
'attributes': { 'style': 'bullet' }, 'internal': createdAndRebuilt } );
+ }, 'converting partial selection of list item "Item 2" to level 2
MWheading' );
- runConverterTest( assert, new ve.Range( 15, 50 ), 'heading', { level: 3
}, new ve.Range( 15, 44 ), function( data ) {
+ runConverterTest( assert, new ve.Range( 15, 50 ), 'MWheading', { level:
3 }, new ve.Range( 15, 44 ), function( data ) {
data[0].internal = rebuilt;
- data.splice( 11, 2, { 'type': '/list' }, { 'type': 'heading',
'attributes': { 'level': 3 }, 'internal': created } );
- data.splice( 19, 4, { 'type': '/heading' }, { 'type':
'heading', 'attributes': { 'level': 3 }, 'internal': created } );
- data.splice( 27, 4, { 'type': '/heading' }, { 'type':
'heading', 'attributes': { 'level': 3 }, 'internal': created } );
- data.splice( 38, 4, { 'type': '/heading' }, { 'type':
'heading', 'attributes': { 'level': 3 }, 'internal': created } );
- data.splice( 46, 2, { 'type': '/heading' }, { 'type': 'list',
'attributes': { 'style': 'bullet' }, 'internal': created } );
+ data.splice( 11, 2, { 'type': '/list' }, { 'type': 'MWheading',
'attributes': { 'level': 3 }, 'internal': created } );
+ data.splice( 19, 4, { 'type': '/MWheading' }, { 'type':
'MWheading', 'attributes': { 'level': 3 }, 'internal': created } );
+ data.splice( 27, 4, { 'type': '/MWheading' }, { 'type':
'MWheading', 'attributes': { 'level': 3 }, 'internal': created } );
+ data.splice( 38, 4, { 'type': '/MWheading' }, { 'type':
'MWheading', 'attributes': { 'level': 3 }, 'internal': created } );
+ data.splice( 46, 2, { 'type': '/MWheading' }, { 'type': 'list',
'attributes': { 'style': 'bullet' }, 'internal': created } );
}, 'converting partial selection across two lists surrounding a
paragraph' );
- runConverterTest( assert, new ve.Range( 4, 28 ), 'heading', { level: 1
}, new ve.Range( 2, 22 ), function( data ) {
+ runConverterTest( assert, new ve.Range( 4, 28 ), 'MWheading', { level:
1 }, new ve.Range( 2, 22 ), function( data ) {
data[0].internal = rebuilt;
- data.splice( 0, 3, { 'type': 'heading', 'attributes': {
'level': 1 }, 'internal': created } );
- data.splice( 7, 4, { 'type': '/heading' }, { 'type': 'heading',
'attributes': { 'level': 1 }, 'internal': created } );
- data.splice( 15, 4, { 'type': '/heading' }, { 'type':
'heading', 'attributes': { 'level': 1 }, 'internal': created } );
- data.splice( 23, 3, { 'type': '/heading' } );
- }, 'converting partial selection of all list items to level 1 headings'
);
+ data.splice( 0, 3, { 'type': 'MWheading', 'attributes': {
'level': 1 }, 'internal': created } );
+ data.splice( 7, 4, { 'type': '/MWheading' }, { 'type':
'MWheading', 'attributes': { 'level': 1 }, 'internal': created } );
+ data.splice( 15, 4, { 'type': '/MWheading' }, { 'type':
'MWheading', 'attributes': { 'level': 1 }, 'internal': created } );
+ data.splice( 23, 3, { 'type': '/MWheading' } );
+ }, 'converting partial selection of all list items to level 1
MWheadings' );
- runConverterTest( assert, new ve.Range( 5, 26 ), 'preformatted',
undefined, new ve.Range( 3, 20 ), function( data ) {
+ runConverterTest( assert, new ve.Range( 5, 26 ), 'MWpreformatted',
undefined, new ve.Range( 3, 20 ), function( data ) {
data[0].internal = rebuilt;
- data.splice( 0, 3, { 'type': 'preformatted', 'internal':
created } );
- data.splice( 7, 4, { 'type': '/preformatted' }, { 'type':
'preformatted', 'internal': created } );
- data.splice( 15, 4, { 'type': '/preformatted' }, { 'type':
'preformatted', 'internal': created } );
- data.splice( 23, 3, { 'type': '/preformatted' } );
- }, 'converting partial selection of some list items to preformatted
text' );
+ data.splice( 0, 3, { 'type': 'MWpreformatted', 'internal':
created } );
+ data.splice( 7, 4, { 'type': '/MWpreformatted' }, { 'type':
'MWpreformatted', 'internal': created } );
+ data.splice( 15, 4, { 'type': '/MWpreformatted' }, { 'type':
'MWpreformatted', 'internal': created } );
+ data.splice( 23, 3, { 'type': '/MWpreformatted' } );
+ }, 'converting partial selection of some list items to MWpreformatted
text' );
runConverterTest( assert, new ve.Range( 146, 159 ), 'paragraph',
undefined, new ve.Range( 146, 159 ), function( data ) {
data.splice( 145, 1, { 'type': 'paragraph', 'internal': created
} );
@@ -70,6 +70,6 @@
runConverterTest( assert, new ve.Range( 165, 180 ), 'paragraph',
undefined, new ve.Range( 165, 180 ), function( data ) {
data.splice( 162, 1, { 'type': 'paragraph', 'internal': created
} );
data.splice( 183, 1, { 'type': '/paragraph' } );
- }, 'converting preformatted in list item to paragraph' );
+ }, 'converting MWpreformatted in list item to paragraph' );
} );
diff --git a/modules/ve/test/dm/ve.dm.SurfaceFragment.test.js
b/modules/ve/test/dm/ve.dm.SurfaceFragment.test.js
index 83e0f6b..6e9e00c 100644
--- a/modules/ve/test/dm/ve.dm.SurfaceFragment.test.js
+++ b/modules/ve/test/dm/ve.dm.SurfaceFragment.test.js
@@ -114,10 +114,11 @@
);
} );
-QUnit.test( 'wrapNodes', 2, function ( assert ) {
+QUnit.test( 'wrapNodes/unwrapNodes', 10, function ( assert ) {
var doc = new ve.dm.Document( ve.copyArray( ve.dm.example.data ) ),
surface = new ve.dm.Surface( doc ),
fragment = new ve.dm.SurfaceFragment( surface, new ve.Range(
55, 61 ) );
+
// Make 2 paragraphs into 2 lists of 1 item each
fragment.wrapNodes(
[{ 'type': 'list', 'attributes': { 'style': 'bullet' } }, {
'type': 'listItem' }]
@@ -150,6 +151,12 @@
],
'wrapping nodes can add multiple levels of wrapping to multiple
elements'
);
+ assert.deepEqual( fragment.getRange(), new ve.Range( 55, 69 ), 'new
range contains wrapping elements' );
+
+ fragment.unwrapNodes( 0, 2 );
+ assert.deepEqual( doc.getData(), ve.dm.example.data, 'unwrapping 2
levels restores document to original state' );
+ assert.deepEqual( fragment.getRange(), new ve.Range( 55, 61 ), 'range
after unwrapping is same as original range' );
+
// Make a 1 paragraph into 1 list with 1 item
fragment = new ve.dm.SurfaceFragment( surface, new ve.Range( 9, 12 ) );
fragment.wrapNodes(
@@ -172,13 +179,78 @@
],
'wrapping nodes can add multiple levels of wrapping to a single
element'
);
+ assert.deepEqual( fragment.getRange(), new ve.Range( 9, 16 ), 'new
range contains wrapping elements' );
+
+ fragment.unwrapNodes( 0, 2 );
+ assert.deepEqual( doc.getData(), ve.dm.example.data, 'unwrapping 2
levels restores document to original state' );
+ assert.deepEqual( fragment.getRange(), new ve.Range( 9, 12 ), 'range
after unwrapping is same as original range' );
+
+ fragment = new ve.dm.SurfaceFragment( surface, new ve.Range( 8, 34 ) );
+ fragment.unwrapNodes( 3, 1 );
+ assert.deepEqual( fragment.getData(), doc.getData( new ve.Range( 5, 29
) ), 'unwrapping multiple outer nodes and an inner node' );
+ assert.deepEqual( fragment.getRange(), new ve.Range( 5, 29 ), 'new
range contains inner elements' );
+
} );
-QUnit.test( 'wrapAllNodes/unwrapAllNodes', 10, function ( assert ) {
+QUnit.test( 'rewrapNodes', 4, function ( assert ) {
+ var doc = new ve.dm.Document( ve.copyArray( ve.dm.example.data ) ),
+ surface = new ve.dm.Surface( doc ),
+ fragment = new ve.dm.SurfaceFragment( surface, new ve.Range(
43, 55 ) ),
+ expectedDoc = new ve.dm.Document( ve.copyArray(
ve.dm.example.data ) ),
+ expectedSurface = new ve.dm.Surface( expectedDoc ),
+ expectedFragment = new ve.dm.SurfaceFragment( expectedSurface,
new ve.Range( 43, 55 ) ),
+ created = { 'changed': { 'created': 1 } },
+ expectedData;
+
+ // set up wrapped nodes in example document
+ fragment.wrapNodes(
+ [{ 'type': 'list', 'attributes': { 'style': 'bullet' } }, {
'type': 'listItem' }]
+ );
+ expectedFragment.wrapNodes(
+ [{ 'type': 'list', 'attributes': { 'style': 'bullet' } }, {
'type': 'listItem' }]
+ );
+ // range is now 43, 59
+
+ // Compare a rewrap operation with its equivalent unwrap + wrap
+ // This type of test can only exist if the intermediate state is valid
+ fragment.rewrapNodes(
+ 2,
+ [{ 'type': 'definitionList' }, { 'type': 'definitionListItem',
'attributes': { 'style': 'term' } }]
+ );
+ expectedFragment.unwrapNodes( 0, 2 );
+ expectedFragment.wrapNodes(
+ [{ 'type': 'definitionList' }, { 'type': 'definitionListItem',
'attributes': { 'style': 'term' } }]
+ );
+
+ assert.deepEqual(
+ doc.getData(),
+ expectedDoc.getData(),
+ 'rewrapping multiple nodes via a valid intermediate state
produces the same document as unwrapping then wrapping'
+ );
+ assert.deepEqual( fragment.getRange(), expectedFragment.getRange(),
'new range contains rewrapping elements' );
+
+ // Rewrap paragrphs as headings
+ // The intermediate stage (plain text attached to the document) would
be invalid
+ // if performed as an unwrap and a wrap
+ expectedData = ve.copyArray( doc.getData() );
+
+ fragment = new ve.dm.SurfaceFragment( surface, new ve.Range( 59, 65 ) );
+ fragment.rewrapNodes( 1, [ { 'type': 'heading', 'attributes': {
'level': 1 } } ] );
+
+ expectedData.splice( 59, 1, { 'type': 'heading', 'attributes': {
'level': 1 }, 'internal': created } );
+ expectedData.splice( 61, 1, { 'type': '/heading' } );
+ expectedData.splice( 62, 1, { 'type': 'heading', 'attributes': {
'level': 1 }, 'internal': created } );
+ expectedData.splice( 64, 1, { 'type': '/heading' } );
+
+ assert.deepEqual( doc.getData(), expectedData, 'rewrapping paragraphs
as headings' );
+ assert.deepEqual( fragment.getRange(), new ve.Range( 59, 65 ), 'new
range contains rewrapping elements' );
+} );
+
+QUnit.test( 'wrapAllNodes', 10, function ( assert ) {
var doc = new ve.dm.Document( ve.copyArray( ve.dm.example.data ) ),
surface = new ve.dm.Surface( doc ),
fragment = new ve.dm.SurfaceFragment( surface, new ve.Range(
55, 61 ) ),
- expectedData;
+ expectedData = ve.copyArray( doc.getData() );
// Make 2 paragraphs into 1 lists of 1 item with 2 paragraphs
fragment.wrapAllNodes(
@@ -206,7 +278,7 @@
);
assert.deepEqual( fragment.getRange(), new ve.Range( 55, 65 ), 'new
range contains wrapping elements' );
- fragment.unwrapAllNodes( 2 );
+ fragment.unwrapNodes( 0, 2 );
assert.deepEqual( doc.getData(), ve.dm.example.data, 'unwrapping 2
levels restores document to original state' );
assert.deepEqual( fragment.getRange(), new ve.Range( 55, 61 ), 'range
after unwrapping is same as original range' );
@@ -234,26 +306,25 @@
);
assert.deepEqual( fragment.getRange(), new ve.Range( 9, 16 ), 'new
range contains wrapping elements' );
- fragment.unwrapAllNodes( 2 );
+ fragment.unwrapNodes( 0, 2 );
assert.deepEqual( doc.getData(), ve.dm.example.data, 'unwrapping 2
levels restores document to original state' );
assert.deepEqual( fragment.getRange(), new ve.Range( 9, 12 ), 'range
after unwrapping is same as original range' );
fragment = new ve.dm.SurfaceFragment( surface, new ve.Range( 5, 37 ) );
assert.throws( function() {
- fragment.unwrapAllNodes( 20 );
+ fragment.unwrapNodes( 0, 20 );
}, /cannot unwrap by greater depth/, 'error thrown trying to unwrap
more nodes that it is possible to contain' );
- expectedData = ve.copyArray( ve.dm.example.data );
expectedData.splice( 5, 4 );
expectedData.splice( 29, 4 );
- fragment.unwrapAllNodes( 4 );
+ fragment.unwrapNodes( 0, 4 );
assert.deepEqual(
doc.getData(),
expectedData,
'unwrapping 4 levels (table, tableSection, tableRow and
tableCell)'
);
-});
+} );
QUnit.test( 'rewrapAllNodes', 6, function ( assert ) {
var expectedData,
@@ -271,7 +342,7 @@
4,
[{ 'type': 'list', 'attributes': { 'style': 'bullet' } }, {
'type': 'listItem' }]
);
- expectedFragment.unwrapAllNodes( 4 );
+ expectedFragment.unwrapNodes( 0, 4 );
expectedFragment.wrapAllNodes(
[{ 'type': 'list', 'attributes': { 'style': 'bullet' } }, {
'type': 'listItem' }]
);
@@ -316,49 +387,65 @@
assert.deepEqual( doc.getData(), expectedData, 'rewrapping a heading as
a paragraph' );
assert.deepEqual( fragment.getRange(), new ve.Range( 0, 5 ), 'new range
contains rewrapping elements' );
-});
+} );
-function runIsolateTest( assert, range, expected, label ) {
+function runIsolateTest( assert, type, range, expected, label ) {
var doc = new ve.dm.Document( ve.copyArray( ve.dm.example.isolationData
) ),
surface = new ve.dm.Surface( doc ),
fragment = new ve.dm.SurfaceFragment( surface, range ),
data;
data = ve.copyArray( doc.getFullData() );
- fragment.isolate();
+ fragment.isolateAndUnwrap( type );
expected( data );
assert.deepEqual( doc.getFullData(), data, label );
}
-QUnit.test( 'isolate', 2, function ( assert ) {
+QUnit.test( 'isolateAndUnwrap', 4, function ( assert ) {
var rebuilt = { 'changed': { 'rebuilt': 1 } },
- created = { 'changed': { 'created': 1 } },
createdAndRebuilt = { 'changed': { 'created': 1, 'rebuilt': 1 }
};
- runIsolateTest( assert, new ve.Range( 11, 21 ), function( data ) {
+ runIsolateTest( assert, 'MWheading', new ve.Range( 12, 20 ), function(
data ) {
data[0].internal = rebuilt;
- data.splice( 11, 0, { 'type': '/list' }, { 'type': 'list',
'attributes': { 'style': 'bullet' }, 'internal': createdAndRebuilt });
- data.splice( 23, 0, { 'type': '/list' }, { 'type': 'list',
'attributes': { 'style': 'bullet' }, 'internal': createdAndRebuilt });
- }, 'isolating list item "Item 2"');
+ data.splice( 11, 0, { 'type': '/list' } );
+ data.splice( 12, 1 );
+ data.splice( 20, 1, { 'type': 'list', 'attributes': { 'style':
'bullet' }, 'internal': createdAndRebuilt });
+ }, 'isolating paragraph in list item "Item 2" for MWheading');
- runIsolateTest( assert, new ve.Range( 88, 108 ), function( data ) {
+ runIsolateTest( assert, 'heading', new ve.Range( 12, 20 ), function(
data ) {
+ data.splice( 11, 0, { 'type': 'listItem' } );
+ data.splice( 12, 1 );
+ data.splice( 20, 1, { 'type': '/listItem' });
+ }, 'isolating paragraph in list item "Item 2" for heading');
+
+ runIsolateTest( assert, 'MWheading', new ve.Range( 89, 97 ), function(
data ) {
data[75].internal = rebuilt;
data[76].internal = rebuilt;
data[77].internal = rebuilt;
- data.splice( 88, 0,
+ data.splice( 88, 1,
{ 'type': '/tableRow' },
{ 'type': '/tableSection' },
- { 'type': '/table' },
+ { 'type': '/table' }
+ );
+ data.splice( 99, 1,
{ 'type': 'table', 'internal': createdAndRebuilt },
{ 'type': 'tableSection', 'attributes': { 'style':
'body' }, 'internal': createdAndRebuilt },
- { 'type': 'tableRow', 'internal': created }
+ { 'type': 'tableRow', 'internal': createdAndRebuilt }
);
- data.splice( 115, 0,
- { 'type': '/tableSection' },
- { 'type': '/table' },
- { 'type': 'table', 'internal': createdAndRebuilt },
- { 'type': 'tableSection', 'attributes': { 'style':
'body' }, 'internal': createdAndRebuilt }
+ }, 'isolating "Cell 2" for MWheading');
+
+ runIsolateTest( assert, 'MWheading', new ve.Range( 202, 212 ),
function( data ) {
+ data[186].internal = rebuilt;
+ data[187].internal = rebuilt;
+ data[188].internal = rebuilt;
+ data.splice( 201, 1,
+ { 'type': '/list' }, { 'type': '/listItem' }, { 'type':
'/list' }
);
- }, 'isolating table cells "Cell 2" & "Cell 3"');
+ data.splice( 214, 1,
+ { 'type': 'list', 'attributes': { 'style': 'bullet' },
'internal': createdAndRebuilt },
+ { 'type': 'listItem', 'internal': createdAndRebuilt },
+ { 'type': 'list', 'attributes': { 'style': 'number' },
'internal': createdAndRebuilt }
+ );
+ }, 'isolating paragraph in list item "Nested 2" for MWheading');
} );
diff --git a/modules/ve/test/dm/ve.dm.example.js
b/modules/ve/test/dm/ve.dm.example.js
index ceb0ec7..d4bebcb 100644
--- a/modules/ve/test/dm/ve.dm.example.js
+++ b/modules/ve/test/dm/ve.dm.example.js
@@ -203,7 +203,7 @@
{ 'type': '/paragraph' },
// 31 - End of item
{ 'type': '/listItem' },
- // 32 - End of list
+ // 32 - End of lis t
{ 'type': '/list' },
// 33 - End of cell
{ 'type': '/tableCell' },
@@ -2010,7 +2010,9 @@
'<ul><li>Item 4</li><li>Item 5</li><li>Item 6</li></ul>' +
'<table><tbody><tr><td>Cell 1</td><td>Cell 2</td><td>Cell
3</td></tr><tr><td>Cell 4</td></tr></tbody></table>' +
'Not allowed by dm:' +
- '<ul><li><h1>Title in list</h1></li><li><pre>Preformatted in
list</pre></li></ul>';
+ '<ul><li><h1>Title in list</h1></li><li><pre>Preformatted in
list</pre></li></ul>' +
+ '<ul><li><ol><li>Nested 1</li><li>Nested 2</li><li>Nested
3</li></ol></li></ul>' +
+ '<ul><li><p>P1</p><p>P2</p><p>P3</p></li></ul>';
ve.dm.example.isolationData = [
{ 'type': 'list', 'attributes': { 'style': 'bullet' } },
@@ -2092,5 +2094,39 @@
'P', 'r', 'e', 'f', 'o', 'r', 'm', 'a', 't', 't', 'e', 'd', ' ', 'i',
'n', ' ', 'l', 'i', 's', 't',
{ 'type': '/preformatted' },
{ 'type': '/listItem' },
- { 'type': '/list' }
+ { 'type': '/list' },
+ { 'type': 'list', 'attributes': { 'style': 'bullet' } },
+ { 'type': 'listItem' },
+ { 'type': 'list', 'attributes': { 'style': 'number' } },
+ { 'type': 'listItem' },
+ { 'type': 'paragraph', 'internal': { 'generated': 'wrapper' } },
+ 'N', 'e', 's', 't', 'e', 'd', ' ', '1',
+ { 'type': '/paragraph' },
+ { 'type': '/listItem' },
+ { 'type': 'listItem' },
+ { 'type': 'paragraph', 'internal': { 'generated': 'wrapper' } },
+ 'N', 'e', 's', 't', 'e', 'd', ' ', '2',
+ { 'type': '/paragraph' },
+ { 'type': '/listItem' },
+ { 'type': 'listItem' },
+ { 'type': 'paragraph', 'internal': { 'generated': 'wrapper' } },
+ 'N', 'e', 's', 't', 'e', 'd', ' ', '3',
+ { 'type': '/paragraph' },
+ { 'type': '/listItem' },
+ { 'type': '/list' },
+ { 'type': '/listItem' },
+ { 'type': '/list' },
+ { 'type': 'list', 'attributes': { 'style': 'bullet' } },
+ { 'type': 'listItem' },
+ { 'type': 'paragraph' },
+ 'P', '1',
+ { 'type': '/paragraph' },
+ { 'type': 'paragraph' },
+ 'P', '2',
+ { 'type': '/paragraph' },
+ { 'type': 'paragraph' },
+ 'P', '3',
+ { 'type': '/paragraph' },
+ { 'type': '/listItem' },
+ { 'type': '/list' },
];
--
To view, visit https://gerrit.wikimedia.org/r/51622
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I3c6249de43232a9ef64f498a0aaf66b1c44973f2
Gerrit-PatchSet: 10
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Esanders <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Esanders <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits