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