Catrope has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/58645


Change subject: Allow nodes to handle their own children
......................................................................

Allow nodes to handle their own children

For data->DOM, this is easy: .toDataElements() can optionally return an
array instead of an object, and that will be treated as the data to
insert. If this happens, the converter won't descend. The node handler
can recursively invoke the converter if it needs to (although I suspect
the current implementation is broken when converting block content in an
inline context).

For DOM->data, this is a bit more complex. The node sets
.static.handlesOwnChildren = true; , which triggers the converter to
pass a data slice rather than a single data element, and not to
descend. The node handler can invoke the converter to recursively
convert DOM subtrees to data.

ve.dm.Converter (data->DOM):
* Renamed createDataElement() to createDataElements()
** .toDataElement() may return element or array, handle this
* Renamed childDataElement to childDataElements, is now an array
* Actually alienate if .toDataElement() returns null
** Shockingly, this claimed to be supported before but wasn't
* Rather than pushing to data, concat to it
** Add closing if needed
* Don't descend if .toDataElement() returned an array of length >1, or
  if the node has .handlesOwnChildren = true

ve.dm.Converter (DOM->data):
* Split getDomSubtreeFromData() and getDomFromData()
* When converting a node that handles its own children, pass in a data
  slice and skip over that data

Change-Id: I196cb4c0895cbf0b428a189adb61b56565573ab3
---
M modules/ve/dm/ve.dm.Converter.js
M modules/ve/dm/ve.dm.Model.js
M modules/ve/dm/ve.dm.Node.js
M modules/ve/dm/ve.dm.NodeFactory.js
4 files changed, 167 insertions(+), 54 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/VisualEditor 
refs/changes/45/58645/1

diff --git a/modules/ve/dm/ve.dm.Converter.js b/modules/ve/dm/ve.dm.Converter.js
index 7210117..ebe1990 100644
--- a/modules/ve/dm/ve.dm.Converter.js
+++ b/modules/ve/dm/ve.dm.Converter.js
@@ -177,17 +177,18 @@
  * This invokes the toDomElements function registered for the element type.
  *
  * @method
- * @param {Object} dataElement Linear model element
+ * @param {Object|Array} dataElement Linear model element or data slice
  * @param {HTMLDocument} doc Document to create DOM elements in
  * @returns {HTMLElement|boolean} DOM element, or false if the element cannot 
be converted
  */
-ve.dm.Converter.prototype.getDomElementsFromDataElement = function ( 
dataElement, doc ) {
+ve.dm.Converter.prototype.getDomElementsFromDataElement = function ( 
dataElements, doc ) {
        var domElements, dataElementAttributes, key, matches,
+               dataElement = ve.isArray( dataElements ) ? dataElements[0] : 
dataElements,
                nodeClass = this.modelRegistry.lookup( dataElement.type );
        if ( !nodeClass ) {
                throw new Error( 'Attempting to convert unknown data element 
type ' + dataElement.type );
        }
-       domElements = nodeClass.static.toDomElements( dataElement, doc );
+       domElements = nodeClass.static.toDomElements( dataElements, doc, this );
        if ( !domElements || !domElements.length ) {
                throw new Error( 'toDomElements() failed to return an array 
when converting element of type ' + dataElement.type );
        }
@@ -222,16 +223,23 @@
  * Create a data element from a DOM element.
  * @param {ve.dm.Model} modelClass Model class to use for conversion
  * @param {HTMLElement[]} domElements DOM elements to convert
- * @returns {Object} Data element
+ * @returns {Object|Array|null} Data element or array of linear model data, or 
null to alienate
  */
-ve.dm.Converter.prototype.createDataElement = function ( modelClass, 
domElements ) {
-       var i, j, dataElement, dataElementAttributes, domElementAttributes, 
domElementAttribute;
-       dataElement = modelClass.static.toDataElement( domElements, this );
-       if ( modelClass.static.storeHtmlAttributes && dataElement ) {
+ve.dm.Converter.prototype.createDataElements = function ( modelClass, 
domElements ) {
+       var i, j, dataElements, dataElementAttributes, domElementAttributes,
+               domElementAttribute;
+       dataElements = modelClass.static.toDataElement( domElements, this );
+       if ( !dataElements ) {
+               return null;
+       }
+       if ( !ve.isArray( dataElements ) ) {
+               dataElements = [ dataElements ];
+       }
+       if ( dataElements[0] && modelClass.static.storeHtmlAttributes ) {
                for ( i = 0; i < domElements.length; i++ ) {
                        domElementAttributes = domElements[i].attributes;
                        if ( domElementAttributes && 
domElementAttributes.length ) {
-                               dataElementAttributes = dataElement.attributes 
= dataElement.attributes || {};
+                               dataElementAttributes = 
dataElements[0].attributes = dataElements[0].attributes || {};
                                // Include all attributes and prepend 'html/i/' 
to each attribute name
                                for ( j = 0; j < domElementAttributes.length; 
j++ ) {
                                        domElementAttribute = 
domElementAttributes[j];
@@ -241,7 +249,7 @@
                        }
                }
        }
-       return dataElement;
+       return dataElements;
 };
 
 /**
@@ -368,7 +376,7 @@
                return aboutGroup;
        }
 
-       var i, childDomElement, childDomElements, childDataElement, text, 
childTypes, matches,
+       var i, childDomElement, childDomElements, childDataElements, text, 
childTypes, matches,
                wrappingParagraph, prevElement, childAnnotations, modelName, 
modelClass,
                annotation, childIsContent, aboutGroup,
                data = [],
@@ -403,20 +411,29 @@
                                modelName = this.modelRegistry.matchElement( 
childDomElement );
                                modelClass = this.modelRegistry.lookup( 
modelName ) || ve.dm.AlienNode;
                                if ( modelClass.prototype instanceof 
ve.dm.Annotation ) {
-                                       childDataElement = 
this.createDataElement( modelClass, [ childDomElement ] );
+                                       childDataElements = 
this.createDataElements( modelClass, [ childDomElement ] );
                                } else {
                                        // Node or meta item
                                        aboutGroup = getAboutGroup( 
childDomElement );
                                        childDomElements = 
modelClass.static.enableAboutGrouping ?
                                                aboutGroup : [ childDomElement 
];
-                                       childDataElement = 
this.createDataElement( modelClass, childDomElements );
+                                       childDataElements = 
this.createDataElements( modelClass, childDomElements );
                                }
 
-                               // Update modelClass to reflect the type we got 
back
-                               modelClass = this.modelRegistry.lookup( 
childDataElement.type );
+                               if ( !childDataElements ) {
+                                       // Alienate
+                                       modelClass = ve.dm.AlienNode;
+                                       childDomElements = 
modelClass.static.enableAboutGrouping ?
+                                               aboutGroup : [ childDomElement 
];
+                                       childDataElements = 
this.createDataElement( modelClass, childDomElements );
+                               } else {
+                                       // Update modelClass to reflect the 
type we got back
+                                       modelClass = this.modelRegistry.lookup( 
childDataElements[0].type );
+                               }
+
                                // Now take the appropriate action based on that
                                if ( modelClass.prototype instanceof 
ve.dm.Annotation ) {
-                                       annotation = 
this.annotationFactory.create( modelName, childDataElement );
+                                       annotation = 
this.annotationFactory.create( modelName, childDataElements[0] );
                                        // Start wrapping if needed
                                        if ( !context.inWrapper && 
!context.expectingContent ) {
                                                startWrapping();
@@ -433,14 +450,16 @@
                                        if ( modelClass.prototype instanceof 
ve.dm.MetaItem ) {
                                                // No additional processing 
needed
                                                // Write to data and continue
-                                               data.push( childDataElement );
-                                               data.push( { 'type': '/' + 
childDataElement.type } );
-                                               processNextWhitespace( 
childDataElement );
-                                               prevElement = childDataElement;
+                                               if ( childDataElements.length 
=== 1 ) {
+                                                       childDataElements.push( 
{ 'type': '/' + childDataElements[0].type } );
+                                               }
+                                               data = data.concat( 
childDataElements );
+                                               processNextWhitespace( 
childDataElements[0] );
+                                               prevElement = 
childDataElements[0];
                                                break;
                                        }
 
-                                       childIsContent = 
this.nodeFactory.isNodeContent( childDataElement.type );
+                                       childIsContent = 
this.nodeFactory.isNodeContent( childDataElements[0].type );
 
                                        // If childIsContent isn't what we 
expect, adjust
                                        if ( !context.expectingContent && 
childIsContent ) {
@@ -454,35 +473,39 @@
                                                        modelClass = 
ve.dm.AlienNode;
                                                        childDomElements = 
modelClass.static.enableAboutGrouping ?
                                                                aboutGroup : [ 
childDomElement ];
-                                                       childDataElement = 
this.createDataElement( modelClass, childDomElements );
-                                                       childIsContent = 
this.nodeFactory.isNodeContent( childDataElement.type );
+                                                       childDataElements = 
this.createDataElements( modelClass, childDomElements );
+                                                       childIsContent = 
this.nodeFactory.isNodeContent( childDataElements[0].type );
                                                }
                                        }
 
                                        // Annotate child
                                        if ( childIsContent && 
!context.annotations.isEmpty() ) {
-                                               childDataElement.annotations = 
context.annotations.getIndexes().slice();
+                                               
childDataElements[0].annotations = context.annotations.getIndexes().slice();
                                        }
 
-                                       // Output child and its children, if any
+                                       // Output child and process children if 
needed
                                        if (
+                                               childDataElements.length === 1 
&&
                                                childDomElements.length === 1 &&
-                                               
this.nodeFactory.canNodeHaveChildren( childDataElement.type )
+                                               
this.nodeFactory.canNodeHaveChildren( childDataElements[0].type ) &&
+                                               
!this.nodeFactory.doesNodeHandleOwnChildren( childDataElements[0].type )
                                        ) {
                                                // Recursion
                                                // Opening and closing elements 
are added by the recursion too
                                                data = data.concat(
-                                                       
this.getDataFromDomRecursion( childDomElement, childDataElement,
+                                                       
this.getDataFromDomRecursion( childDomElement, childDataElements[0],
                                                                new 
ve.dm.AnnotationSet( this.store )
                                                        )
                                                );
                                        } else {
-                                               // Write an opening and closing
-                                               data.push( childDataElement );
-                                               data.push( { 'type': '/' + 
childDataElement.type } );
+                                               if ( childDataElements.length 
=== 1 ) {
+                                                       childDataElements.push( 
{ 'type': '/' + childDataElements[0].type } );
+                                               }
+                                               // Write childDataElements 
directly
+                                               data = data.concat( 
childDataElements );
                                        }
-                                       processNextWhitespace( childDataElement 
);
-                                       prevElement = childDataElement;
+                                       processNextWhitespace( 
childDataElements[0] );
+                                       prevElement = childDataElements[0];
 
                                        // In case we consumed multiple 
childDomElements, adjust i accordingly
                                        i += childDomElements.length - 1;
@@ -618,17 +641,19 @@
                                break;
                        case Node.COMMENT_NODE:
                                // TODO treat this as a node with nodeName 
#comment
-                               childDataElement = {
-                                       'type': 'alienMeta',
-                                       'attributes': {
-                                               'style': 'comment',
-                                               'text': childDomElement.data
-                                       }
-                               };
-                               data.push( childDataElement );
-                               data.push( { 'type': '/alienMeta' } );
-                               processNextWhitespace( childDataElement );
-                               prevElement = childDataElement;
+                               childDataElements = [
+                                       {
+                                               'type': 'alienMeta',
+                                               'attributes': {
+                                                       'style': 'comment',
+                                                       'text': 
childDomElement.data
+                                               }
+                                       },
+                                       { 'type': '/alienMeta' }
+                               ];
+                               data = data.concat( childDataElements );
+                               processNextWhitespace( childDataElements[0] );
+                               prevElement = childDataElements[0];
                                break;
                }
        }
@@ -681,12 +706,24 @@
  * @returns {HTMLDocument} Document containing the resulting HTML
  */
 ve.dm.Converter.prototype.getDomFromData = function ( store, data ) {
-       var text, i, j, k, annotations, annotationElement, dataElement,
+       var doc = ve.createDocumentFromHTML( '' );
+       this.getDomSubtreeFromData( store, data, doc.body );
+       return doc;
+};
+
+/**
+ * Convert linear model data to an HTML DOM subtree and add it to a container 
element.
+ *
+ * @param {ve.dm.IndexValueStore} store Index-value store
+ * @param {Array} data Linear model data
+ * @param {HTMLElement} container DOM element to add the generated elements 
to. Should be empty.
+ */
+ve.dm.Converter.prototype.getDomSubtreeFromData = function ( store, data, 
container ) {
+       var text, i, j, k, annotations, annotationElement, dataElement, 
dataSlice,
                childDomElements, pre, ours, theirs, parentDomElement, 
lastChild,
                isContentNode, changed, parentChanged, sibling, 
previousSiblings, doUnwrap, textNode,
                conv = this,
-               doc = ve.createDocumentFromHTML( '' ),
-               container = doc.body,
+               doc = container.ownerDocument,
                domElement = container,
                annotationStack = new ve.dm.AnnotationSet( store );
 
@@ -712,6 +749,28 @@
                }
                // Traverse up
                domElement = domElement.parentNode;
+       }
+
+       function maybeGetDataSlice() {
+               var dataSlice, j, handlesOwn = false;
+               try {
+                       handlesOwn = 
ve.dm.nodeFactory.doesNodeHandleOwnChildren( data[i].type );
+               } catch ( e ) {}
+
+               if ( handlesOwn ) {
+                       j = i;
+                       while ( j < data.length && data[j].type !== '/' + 
data[i].type ) {
+                               j++;
+                       }
+                       if ( j >= data.length ) {
+                               throw new Error( 'Unbalanced data: never found 
closing /' +
+                                       dataElement.type );
+                       }
+                       dataSlice = data.slice( i, j + 1 );
+               } else {
+                       dataSlice = data[i];
+               }
+               return dataSlice;
        }
 
        for ( i = 0; i < data.length; i++ ) {
@@ -763,12 +822,16 @@
                                                text = '';
                                        }
                                        // Insert the elements
-                                       childDomElements = 
this.getDomElementsFromDataElement( data[i], doc );
+                                       dataSlice = maybeGetDataSlice();
+                                       childDomElements = 
this.getDomElementsFromDataElement( dataSlice, doc );
                                        for ( j = 0; j < 
childDomElements.length; j++ ) {
                                                domElement.appendChild( 
childDomElements[j] );
                                        }
-                                       // Increment i once more so we skip 
over the closing as well
-                                       i++;
+                                       if ( ve.isArray( dataSlice ) ) {
+                                               i += dataSlice.length - 1;
+                                       } else {
+                                               i++; // Skip the closing
+                                       }
                                }
                                i++;
                        }
@@ -938,7 +1001,8 @@
                                domElement = parentDomElement;
                        } else {
                                // Create node from data
-                               childDomElements = 
this.getDomElementsFromDataElement( dataElement, doc );
+                               dataSlice = maybeGetDataSlice();
+                               childDomElements = 
this.getDomElementsFromDataElement( dataSlice, doc );
                                // Add reference to internal data
                                childDomElements[0].veInternal = 
ve.extendObject(
                                        { 'childDomElements': childDomElements 
},
@@ -994,6 +1058,10 @@
                                                );
                                        }
                                }
+
+                               if ( ve.isArray( dataSlice ) ) {
+                                       i += dataSlice.length - 2;
+                               }
                        }
                }
        }
@@ -1024,7 +1092,6 @@
                        }
                }
        } );
-       return doc;
 };
 
 /* Initialization */
diff --git a/modules/ve/dm/ve.dm.Model.js b/modules/ve/dm/ve.dm.Model.js
index 9990b59..cb06357 100644
--- a/modules/ve/dm/ve.dm.Model.js
+++ b/modules/ve/dm/ve.dm.Model.js
@@ -106,12 +106,18 @@
  * (usually the model's own .static.name, but that's not required). It may 
optionally have an attributes
  * property set to an object with key-value pairs. Any other properties are 
not allowed.
  *
+ * This function may return a single linear model element, or an array of 
balanced linear model
+ * data. If this function needs to recursively convert a DOM node (e.g. a 
child of one of the
+ * DOM elements passed in), it can call converter.getDataFromDomRecursion( 
domElement ). Note that
+ * if an array is returned, the converter will not descend into the DOM node's 
children; the model
+ * will be assumed to have handled those children.
+ *
  * @static
  * @inheritable
  * @method
  * @param {HTMLElement[]} domElements DOM elements to convert. Usually only 
one element
  * @param {ve.dm.Converter} converter Converter object
- * @returns {Object|null} Linear model element, or null to alienate
+ * @returns {Object|Array|null} Linear model element, or array with linear 
model data, or null to alienate
  */
 ve.dm.Model.static.toDataElement = function ( /*domElements, converter*/ ) {
        throw new Error( 've.dm.Model subclass must implement toDataElement' );
@@ -121,6 +127,11 @@
  * Static function to convert a linear model data element for this model type 
back to one or more
  * DOM elements.
  *
+ * If this model is a node with .handlesOwnChildren set to true, dataElement 
will be an array of
+ * the linear model data of this node and all of its children, rather than a 
single element.
+ * In this case, this function way want to recursively convert linear model 
data to DOM, which can
+ * be done with converter.getDomSubtreeFromData( store, data, containerElement 
);
+ *
  * NOTE: If this function returns multiple DOM elements, the DOM elements 
produced by the children
  * of this model (if it's a node and has children) will be attached to the 
first DOM element in the array.
  * For annotations, only the first element is used, and any additional 
elements are ignored.
@@ -128,11 +139,12 @@
  * @static
  * @inheritable
  * @method
- * @param {Object} dataElement Linear model element with a type property and 
optionally an attributes property
+ * @param {Object|Array} dataElement Linear model element or array of linear 
model data
  * @param {HTMLDocument} doc HTML document for creating elements
+ * @param {ve.dm.Converter} converter Converter object to optionally call 
.getDomSubtreeFromData() on
  * @returns {HTMLElement[]} DOM elements
  */
-ve.dm.Model.static.toDomElements = function ( /*dataElement, doc*/ ) {
+ve.dm.Model.static.toDomElements = function ( /*dataElement, doc, converter*/ 
) {
        throw new Error( 've.dm.Model subclass must implement toDomElements' );
 };
 
diff --git a/modules/ve/dm/ve.dm.Node.js b/modules/ve/dm/ve.dm.Node.js
index 0cf9063..92d2b11 100644
--- a/modules/ve/dm/ve.dm.Node.js
+++ b/modules/ve/dm/ve.dm.Node.js
@@ -46,6 +46,26 @@
 /* Static Properties */
 
 /**
+ * Whether this node handles its own children. After converting a DOM node to 
a linear model
+ * node of this type, the converter checks this property. If it's false, the 
converter will descend
+ * into the DOM node's children, recursively convert them, and attach the 
resulting nodes as
+ * children of the linear model node. If it's true, the converter will not 
descend, and will
+ * expect the node's toDataElement() to have handled the entire DOM subtree.
+ *
+ * The same is true when converting from linear model data to DOM: if this 
property is true,
+ * toDomElements() will be passed the node's data element and all of its 
children and will be
+ * expected to convert the entire subtree. If it's false, the converter will 
descend into the
+ * child nodes and convert each one individually.
+ *
+ * If .static.childNodeTypes is set to [], this property is ignored and will 
be assumed to be true.
+ *
+ * @static
+ * @type {Boolean} static.handlesOwnChildren
+ * @inheritable
+ */
+ve.dm.Node.static.handlesOwnChildren = false;
+
+/**
  * Whether this node type has a wrapping element in the linear model. Most 
node types are wrapped,
  * only special node types are not wrapped.
  *
diff --git a/modules/ve/dm/ve.dm.NodeFactory.js 
b/modules/ve/dm/ve.dm.NodeFactory.js
index e2a09e9..7781ca7 100644
--- a/modules/ve/dm/ve.dm.NodeFactory.js
+++ b/modules/ve/dm/ve.dm.NodeFactory.js
@@ -200,6 +200,20 @@
        throw new Error( 'Unknown node type: ' + type );
 };
 
+/**
+ * Check if the node handles its own children.
+ *
+ * @method
+ * @param {string} type Node type
+ * @returns {boolean} Whether the node handles its own children
+ */
+ve.dm.NodeFactory.prototype.doesNodeHandleOwnChildren = function ( type ) {
+       if ( type in this.registry ) {
+               return this.registry[type].static.handlesOwnChildren;
+       }
+       throw new Error( 'Unknown node type: ' + type );
+};
+
 /* Initialization */
 
 ve.dm.nodeFactory = new ve.dm.NodeFactory();

-- 
To view, visit https://gerrit.wikimedia.org/r/58645
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I196cb4c0895cbf0b428a189adb61b56565573ab3
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Catrope <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to