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

Reply via email to