Merge "(bug 45062) Implement the new node API in the converter"

This commit is contained in:
jenkins-bot 2013-02-22 23:26:18 +00:00 committed by Gerrit Code Review
commit fb11cc8a8c
22 changed files with 275 additions and 460 deletions

View file

@ -35,20 +35,29 @@ ve.dm.AlienNode.static.enableAboutGrouping = true;
ve.dm.AlienNode.static.storeHtmlAttributes = false;
ve.dm.AlienNode.static.toDataElement = function ( domElement, context ) {
ve.dm.AlienNode.static.toDataElement = function ( domElements, context ) {
var i, isInline, allTagsInline, type, html;
// Check whether all elements are inline elements
allTagsInline = true;
for ( i = 0; i < domElements.length; i++ ) {
if ( ve.isBlockElement( domElements[i] ) ) {
allTagsInline = false;
break;
}
}
// We generate alienBlock elements for block tags and alienInline elements for
// inline tags; unless we're in a content location, in which case we have no choice
// but to generate an alienInline element.
var isInline =
// Force inline in content locations (but not wrappers)
( context.expectingContent && !context.inWrapper ) ||
// Also force inline in wrappers that we can't close
( context.inWrapper && !context.canCloseWrapper ) ||
// Look at the tag name otherwise
!ve.isBlockElement( domElement ),
type = isInline ? 'alienInline' : 'alienBlock',
// TODO handle about groups somehow
html = $( '<div>' ).append( $( domElement ).clone() ).html();
isInline =
// Force inline in content locations (but not wrappers)
( context.expectingContent && !context.inWrapper ) ||
// Also force inline in wrappers that we can't close
( context.inWrapper && !context.canCloseWrapper ) ||
// Look at the tag names otherwise
allTagsInline;
type = isInline ? 'alienInline' : 'alienBlock';
html = $( '<div>', domElements[0].ownerDocument ).append( $( domElements ).clone() ).html();
return {
'type': type,
'attributes': {
@ -57,11 +66,11 @@ ve.dm.AlienNode.static.toDataElement = function ( domElement, context ) {
};
};
ve.dm.AlienNode.static.toDomElement = function ( dataElement ) {
ve.dm.AlienNode.static.toDomElements = function ( dataElement ) {
var wrapper = document.createElement( 'div' );
wrapper.innerHTML = dataElement.attributes.html;
// TODO handle multiple nodes (from about groups) somehow
return wrapper.firstChild;
// Convert wrapper.children to an array
return Array.prototype.slice.call( wrapper.childNodes, 0 );
};
/* Concrete subclasses */

View file

@ -35,8 +35,8 @@ ve.dm.BreakNode.static.toDataElement = function () {
return { 'type': 'break' };
};
ve.dm.BreakNode.static.toDomElement = function () {
return document.createElement( 'br' );
ve.dm.BreakNode.static.toDomElements = function () {
return [ document.createElement( 'br' ) ];
};
/* Registration */

View file

@ -33,8 +33,8 @@ ve.dm.CenterNode.static.toDataElement = function () {
return { 'type': 'center' };
};
ve.dm.CenterNode.static.toDomElement = function () {
return document.createElement( 'center' );
ve.dm.CenterNode.static.toDomElements = function () {
return [ document.createElement( 'center' ) ];
};
/* Registration */

View file

@ -35,14 +35,14 @@ ve.dm.DefinitionListItemNode.static.defaultAttributes = {
ve.dm.DefinitionListItemNode.static.matchTagNames = [ 'dt', 'dd' ];
ve.dm.DefinitionListItemNode.static.toDataElement = function ( domElement ) {
var style = domElement.nodeName.toLowerCase() === 'dt' ? 'term' : 'definition';
ve.dm.DefinitionListItemNode.static.toDataElement = function ( domElements ) {
var style = domElements[0].nodeName.toLowerCase() === 'dt' ? 'term' : 'definition';
return { 'type': 'definitionListItem', 'attributes': { 'style': style } };
};
ve.dm.DefinitionListItemNode.static.toDomElement = function ( dataElement ) {
ve.dm.DefinitionListItemNode.static.toDomElements = function ( dataElement ) {
var tag = dataElement.attributes && dataElement.attributes.style === 'term' ? 'dt' : 'dd';
return document.createElement( tag );
return [ document.createElement( tag ) ];
};
/* Registration */

View file

@ -35,8 +35,8 @@ ve.dm.DefinitionListNode.static.toDataElement = function () {
return { 'type': 'definitionList' };
};
ve.dm.DefinitionListNode.static.toDomElement = function () {
return document.createElement( 'dl' );
ve.dm.DefinitionListNode.static.toDomElements = function () {
return [ document.createElement( 'dl' ) ];
};
/* Registration */

View file

@ -35,7 +35,7 @@ ve.dm.HeadingNode.static.defaultAttributes = {
ve.dm.HeadingNode.static.matchTagNames = [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ];
ve.dm.HeadingNode.static.toDataElement = function ( domElement ) {
ve.dm.HeadingNode.static.toDataElement = function ( domElements ) {
var levels = {
'h1': 1,
'h2': 2,
@ -44,13 +44,13 @@ ve.dm.HeadingNode.static.toDataElement = function ( domElement ) {
'h5': 5,
'h6': 6
},
level = levels[domElement.nodeName.toLowerCase()];
level = levels[domElements[0].nodeName.toLowerCase()];
return { 'type': 'heading', 'attributes': { 'level': level } };
};
ve.dm.HeadingNode.static.toDomElement = function ( dataElement ) {
ve.dm.HeadingNode.static.toDomElements = function ( dataElement ) {
var level = dataElement.attributes && dataElement.attributes.level || 1;
return document.createElement( 'h' + level );
return [ document.createElement( 'h' + level ) ];
};
/* Registration */

View file

@ -35,8 +35,8 @@ ve.dm.ImageNode.static.toDataElement = function () {
return { 'type': 'image' };
};
ve.dm.ImageNode.static.toDomElement = function () {
return document.createElement( 'img' );
ve.dm.ImageNode.static.toDomElements = function () {
return [ document.createElement( 'img' ) ];
};
/* Registration */

View file

@ -35,8 +35,8 @@ ve.dm.ListItemNode.static.toDataElement = function () {
return { 'type': 'listItem' };
};
ve.dm.ListItemNode.static.toDomElement = function () {
return document.createElement( 'li' );
ve.dm.ListItemNode.static.toDomElements = function () {
return [ document.createElement( 'li' ) ];
};
/* Registration */

View file

@ -35,14 +35,14 @@ ve.dm.ListNode.static.defaultAttributes = {
ve.dm.ListNode.static.matchTagNames = [ 'ul', 'ol' ];
ve.dm.ListNode.static.toDataElement = function ( domElement ) {
var style = domElement.nodeName.toLowerCase() === 'ol' ? 'number' : 'bullet';
ve.dm.ListNode.static.toDataElement = function ( domElements ) {
var style = domElements[0].nodeName.toLowerCase() === 'ol' ? 'number' : 'bullet';
return { 'type': 'list', 'attributes': { 'style': style } };
};
ve.dm.ListNode.static.toDomElement = function ( dataElement ) {
ve.dm.ListNode.static.toDomElements = function ( dataElement ) {
var tag = dataElement.attributes && dataElement.attributes.style === 'number' ? 'ol' : 'ul';
return document.createElement( tag );
return [ document.createElement( tag ) ];
};

View file

@ -31,18 +31,18 @@ ve.dm.MWEntityNode.static.isContent = true;
ve.dm.MWEntityNode.static.matchTagNames = [ 'span' ];
ve.dm.MWEntityNode.static.matchRdfaTypes = [ 'mw:Entity' ]; // TODO ignored, still using a converter hack
ve.dm.MWEntityNode.static.matchRdfaTypes = [ 'mw:Entity' ];
ve.dm.MWEntityNode.static.toDataElement = function ( domElement ) {
return { 'type': 'MWentity', 'attributes': { 'character': domElement.textContent } };
ve.dm.MWEntityNode.static.toDataElement = function ( domElements ) {
return { 'type': 'MWentity', 'attributes': { 'character': domElements[0].textContent } };
};
ve.dm.MWEntityNode.static.toDomElement = function ( dataElement ) {
ve.dm.MWEntityNode.static.toDomElements = function ( dataElement ) {
var domElement = document.createElement( 'span' ),
textNode = document.createTextNode( dataElement.attributes.character );
domElement.setAttribute( 'typeof', 'mw:Entity' );
domElement.appendChild( textNode );
return domElement;
return [ domElement ];
};
/* Registration */

View file

@ -33,24 +33,25 @@ ve.dm.MetaNode.static.isMeta = true;
ve.dm.MetaNode.static.matchTagNames = [ 'meta', 'link' ];
ve.dm.MetaNode.static.toDataElement = function ( domElement, context ) {
var isLink = domElement.nodeName.toLowerCase() === 'link',
ve.dm.MetaNode.static.toDataElement = function ( domElements, context ) {
var firstDomElement = domElements[0],
isLink = firstDomElement.nodeName.toLowerCase() === 'link',
keyAttr = isLink ? 'rel' : 'property',
valueAttr = isLink ? 'href' : 'content',
dataElement = {
'type': context.expectingContent ? 'metaInline' : 'metaBlock',
'attributes': {
'style': isLink ? 'link' : 'meta',
'key': domElement.getAttribute( keyAttr )
'key': firstDomElement.getAttribute( keyAttr )
}
};
if ( domElement.hasAttribute( valueAttr ) ) {
dataElement.attributes.value = domElement.getAttribute( valueAttr );
if ( firstDomElement.hasAttribute( valueAttr ) ) {
dataElement.attributes.value = firstDomElement.getAttribute( valueAttr );
}
return dataElement;
};
ve.dm.MetaNode.static.toDomElement = function ( dataElement ) {
ve.dm.MetaNode.static.toDomElements = function ( dataElement ) {
var style = dataElement.attributes && dataElement.attributes.style || 'meta',
isLink = style === 'link',
tag = isLink ? 'link' : 'meta',
@ -58,7 +59,7 @@ ve.dm.MetaNode.static.toDomElement = function ( dataElement ) {
valueAttr = isLink ? 'href' : 'content',
domElement;
if ( style === 'comment' ) {
return document.createComment( dataElement.attributes && dataElement.attributes.text || '' );
return [ document.createComment( dataElement.attributes && dataElement.attributes.text || '' ) ];
}
domElement = document.createElement( tag );
if ( dataElement.attributes && dataElement.attributes.key !== null ) {
@ -67,7 +68,7 @@ ve.dm.MetaNode.static.toDomElement = function ( dataElement ) {
if ( dataElement.attributes && dataElement.attributes.value ) {
domElement.setAttribute( valueAttr, dataElement.attributes.value );
}
return domElement;
return [ domElement ];
};
/* Concrete subclasses */

View file

@ -35,8 +35,8 @@ ve.dm.ParagraphNode.static.toDataElement = function () {
return { 'type': 'paragraph' };
};
ve.dm.ParagraphNode.static.toDomElement = function () {
return document.createElement( 'p' );
ve.dm.ParagraphNode.static.toDomElements = function () {
return [ document.createElement( 'p' ) ];
};
/* Registration */

View file

@ -37,8 +37,8 @@ ve.dm.PreformattedNode.static.toDataElement = function () {
return { 'type': 'preformatted' };
};
ve.dm.PreformattedNode.static.toDomElement = function () {
return document.createElement( 'pre' );
ve.dm.PreformattedNode.static.toDomElements = function () {
return [ document.createElement( 'pre' ) ];
};
/* Registration */

View file

@ -35,14 +35,14 @@ ve.dm.TableCellNode.static.defaultAttributes = {
ve.dm.TableCellNode.static.matchTagNames = [ 'td', 'th' ];
ve.dm.TableCellNode.static.toDataElement = function ( domElement ) {
var style = domElement.nodeName.toLowerCase() === 'th' ? 'header' : 'data';
ve.dm.TableCellNode.static.toDataElement = function ( domElements ) {
var style = domElements[0].nodeName.toLowerCase() === 'th' ? 'header' : 'data';
return { 'type': 'tableCell', 'attributes': { 'style': style } };
};
ve.dm.TableCellNode.static.toDomElement = function ( dataElement ) {
ve.dm.TableCellNode.static.toDomElements = function ( dataElement ) {
var tag = dataElement.attributes && dataElement.attributes.style === 'header' ? 'th' : 'td';
return document.createElement( tag );
return [ document.createElement( tag ) ];
};
/* Registration */

View file

@ -35,8 +35,8 @@ ve.dm.TableNode.static.toDataElement = function () {
return { 'type': 'table' };
};
ve.dm.TableNode.static.toDomElement = function () {
return document.createElement( 'table' );
ve.dm.TableNode.static.toDomElements = function () {
return [ document.createElement( 'table' ) ];
};
/* Registration */

View file

@ -37,8 +37,8 @@ ve.dm.TableRowNode.static.toDataElement = function () {
return { 'type': 'tableRow' };
};
ve.dm.TableRowNode.static.toDomElement = function () {
return document.createElement( 'tr' );
ve.dm.TableRowNode.static.toDomElements = function () {
return [ document.createElement( 'tr' ) ];
};
/* Registration */

View file

@ -37,24 +37,24 @@ ve.dm.TableSectionNode.static.defaultAttributes = {
ve.dm.TableSectionNode.static.matchTagNames = [ 'thead', 'tbody', 'tfoot' ];
ve.dm.TableSectionNode.static.toDataElement = function ( domElement ) {
ve.dm.TableSectionNode.static.toDataElement = function ( domElements ) {
var styles = {
'thead': 'header',
'tbody': 'body',
'tfoot': 'footer'
},
style = styles[domElement.nodeName.toLowerCase()] || 'body';
style = styles[domElements[0].nodeName.toLowerCase()] || 'body';
return { 'type': 'tableSection', 'attributes': { 'style': style } };
};
ve.dm.TableSectionNode.static.toDomElement = function ( dataElement ) {
ve.dm.TableSectionNode.static.toDomElements = function ( dataElement ) {
var tags = {
'header': 'thead',
'body': 'tbody',
'footer': 'tfoot'
},
tag = tags[dataElement.attributes && dataElement.attributes.style || 'body'];
return document.createElement( tag );
return [ document.createElement( tag ) ];
};
/* Registration */

View file

@ -12,21 +12,15 @@
*
* @class
* @constructor
* @param {ve.dm.ModelRegistry} modelRegistry
* @param {ve.dm.NodeFactory} nodeFactory
* @param {ve.dm.AnnotationFactory} annotationFactory
*/
ve.dm.Converter = function VeDmConverter( nodeFactory, annotationFactory ) {
ve.dm.Converter = function VeDmConverter( modelRegistry, nodeFactory, annotationFactory ) {
// Properties
this.modelRegistry = modelRegistry;
this.nodeFactory = nodeFactory;
this.annotationFactory = annotationFactory;
this.elements = {
'toDomElement': {},
'toDataElement': {},
'dataElementTypes': {}
};
// Events
this.nodeFactory.addListenerMethod( this, 'register', 'onNodeRegister' );
};
/* Static Methods */
@ -58,80 +52,36 @@ ve.dm.Converter.getDataContentFromText = function ( text, annotations ) {
/* Methods */
/**
* Handle register events from the node factory.
*
* FIXME
* If a node is special; such as document, alienInline, alienBlock and text; its {converters}
* property should be set to null, as to distinguish it from a new node type that someone has simply
* forgotten to implement converters for.
*
* @method
* @param {string} type Node type
* @param {Function} constructor Node constructor
* @throws {Error} Missing conversion data in node implementation
*/
ve.dm.Converter.prototype.onNodeRegister = function ( dataElementType, constructor ) {
if ( !constructor.static.toDomElement || !constructor.static.toDataElement ) {
throw new Error( 'Missing static properties in node implementation of ' + dataElementType );
} else {
var i,
domElementTypes = constructor.static.matchTagNames || [],
toDomElement = constructor.static.toDomElement,
toDataElement = constructor.static.toDataElement;
// Registration
this.elements.toDomElement[dataElementType] = toDomElement;
for ( i = 0; i < domElementTypes.length; i++ ) {
this.elements.toDataElement[domElementTypes[i]] = toDataElement;
this.elements.dataElementTypes[domElementTypes[i]] = dataElementType;
}
}
};
/**
* Get the DOM element for a given linear model element.
*
* This invokes the toDomElement function registered for the element type.
* This invokes the toDomElements function registered for the element type.
*
* @method
* @param {Object} dataElement Linear model element
* @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.getDomElementFromDataElement = function ( dataElement, doc ) {
var key, domElement, dataElementAttributes, wrapper,
dataElementType = dataElement.type;
if ( dataElementType === 'alienInline' || dataElementType === 'alienBlock' ) {
// Alien
// Create nodes from source
wrapper = doc.createElement( 'div' );
wrapper.innerHTML = dataElement.attributes.html;
if ( wrapper.childNodes.length > 1 ) {
// Wrap the HTML in a single element, this makes
// it much easier to deal with. It'll be unwrapped
// at the end of getDomFromData().
domElement = doc.createElement( 'div' );
domElement.setAttribute( 'data-ve-multi-child-alien-wrapper', 'true' );
while ( wrapper.firstChild ) {
domElement.appendChild( wrapper.firstChild );
}
} else {
domElement = wrapper.firstChild;
}
return domElement;
ve.dm.Converter.prototype.getDomElementsFromDataElement = function ( dataElement, doc ) {
var domElements, dataElementAttributes, key, matches,
nodeClass = this.nodeFactory.lookup( dataElement.type );
if ( !nodeClass ) {
throw new Error( 'Attempting to convert unknown data element type ' + dataElement.type );
}
if ( !( dataElementType in this.elements.toDomElement ) ) {
// Unsupported element
return false;
domElements = nodeClass.static.toDomElements( dataElement, doc );
if ( !domElements || !domElements.length ) {
throw new Error( 'toDomElements() failed to return an array when converting element of type ' + dataElement.type );
}
domElement = this.elements.toDomElement[dataElementType]( dataElement );
dataElementAttributes = dataElement.attributes;
if ( dataElementAttributes ) {
for ( key in dataElementAttributes ) {
// Only include 'html/0/*' attributes and strip the 'html/0/' from the beginning of the name
if ( key.indexOf( 'html/0/' ) === 0 ) {
domElement.setAttribute( key.substr( 7 ), dataElementAttributes[key] );
// Only include 'html/i/*' attributes and strip the 'html/i/' from the beginning of the name
/*jshint regexp:false */
matches = key.match( /^html\/(\d+)\/(.*)$/ );
if ( matches ) {
if ( domElements[matches[1]] && !domElements[matches[1]].hasAttribute( matches[2] ) ) {
domElements[matches[1]].setAttribute( matches[2], dataElementAttributes[key] );
}
}
}
}
@ -141,66 +91,34 @@ ve.dm.Converter.prototype.getDomElementFromDataElement = function ( dataElement,
!ve.isEmptyObject( dataElement.internal.changed ) &&
ve.init.platform.useChangeMarkers()
) {
domElement.setAttribute( 'data-ve-changed',
domElements[0].setAttribute( 'data-ve-changed',
JSON.stringify( dataElement.internal.changed )
);
}
return domElement;
return domElements;
};
/**
* Get the linear model data element for a given DOM element.
*
* This invokes the toDataElement function registered for the element type
*
* @method
* @param {HTMLElement} domElement DOM element
* @param {ve.AnnotationSet} annotations Annotations to apply if the node is a content node
* @returns {Object|boolean} Linear model element, or false if the node cannot be converted
*/
ve.dm.Converter.prototype.getDataElementFromDomElement = function ( domElement, annotations ) {
var dataElement, domElementAttributes, dataElementAttributes, domElementAttribute, i,
domElementType = domElement.nodeName.toLowerCase();
annotations = annotations || new ve.AnnotationSet();
if (
// Unsupported elements
!( domElementType in this.elements.toDataElement )
// TODO check for generated elements
) {
return false;
}
dataElement = this.elements.toDataElement[domElementType]( domElement );
domElementAttributes = domElement.attributes;
if (
dataElement && ve.dm.nodeFactory.doesNodeStoreHtmlAttributes( dataElement.type ) &&
domElementAttributes.length
) {
dataElementAttributes = dataElement.attributes = dataElement.attributes || {};
// Include all attributes and prepend 'html/0/' to each attribute name
for ( i = 0; i < domElementAttributes.length; i++ ) {
domElementAttribute = domElementAttributes[i];
dataElementAttributes['html/0/' + domElementAttribute.name] = domElementAttribute.value;
ve.dm.Converter.prototype.createDataElement = function ( modelClass, domElements, context ) {
var i, j, dataElement, dataElementAttributes, domElementAttributes, domElementAttribute;
dataElement = modelClass.static.toDataElement( domElements, ve.copyObject( context ) );
if ( modelClass.static.storeHTMLAttributes && dataElement ) {
for ( i = 0; i < domElements.length; i++ ) {
domElementAttributes = domElements[i].attributes;
if ( domElementAttributes && domElementAttributes.length ) {
dataElementAttributes = dataElement.attributes = dataElement.attributes || {};
// Include all attributes and prepend 'html/i/' to each attribute name
for ( j = 0; j < domElementAttributes.length; j++ ) {
domElementAttribute = domElementAttributes[j];
dataElementAttributes['html/' + i + '/' + domElementAttribute.name] =
domElementAttribute.value;
}
}
}
}
if ( this.nodeFactory.isNodeContent( dataElement.type ) && !annotations.isEmpty() ) {
dataElement.annotations = annotations.clone();
}
return dataElement;
};
/**
* Check if an HTML DOM node represents an annotation, and if so, build an annotation object for it.
*
* Annotation Object:
* { 'type': 'type', data: { 'key': 'value', ... } }
*
* @param {HTMLElement} domElement HTML DOM node
* @returns {Object|boolean} Annotation object, or false if the node is not an annotation
*/
ve.dm.Converter.prototype.getDataAnnotationFromDomElement = function ( domElement ) {
return this.annotationFactory.createFromElement( domElement ) || false;
};
/**
* Build an HTML DOM node for a linear model annotation.
*
@ -238,38 +156,6 @@ ve.dm.Converter.prototype.getDataFromDom = function ( doc ) {
*/
ve.dm.Converter.prototype.getDataFromDomRecursion = function ( domElement, annotations,
dataElement, path, alreadyWrapped ) {
function createAlien( domElement, context, isWrapper ) {
// We generate alienBlock elements for block tags and alienInline elements for
// inline tags; unless we're in a content location, in which case we have no choice
// but to generate an alienInline element.
var isInline =
// Force inline in content locations (but not wrappers)
( !context.inWrapper && context.expectingContent ) ||
// Also force inline in wrappers that we can't close
( context.inWrapper && !context.canCloseWrapper ) ||
// Look at the tag name otherwise
!ve.isBlockElement( domElement ),
type = isInline ? 'alienInline' : 'alienBlock',
html, alien;
if ( isWrapper ) {
html = $( domElement ).html();
} else {
html = $( '<div>', doc ).append( $( domElement ).clone() ).html();
}
alien = [
{
'type': type,
'attributes': {
'html': html
}
},
{ 'type': '/' + type }
];
if ( !annotations.isEmpty() ) {
alien[0].annotations = annotations.clone();
}
return alien;
}
function addWhitespace( element, index, whitespace ) {
if ( !element.internal ) {
element.internal = {};
@ -321,76 +207,40 @@ ve.dm.Converter.prototype.getDataFromDomRecursion = function ( domElement, annot
context.expectingContent = originallyExpectingContent;
}
/**
* Helper function to group adjacent child elements with the same about attribute together.
* If there are multiple adjacent child nodes with the same about attribute, they are
* wrapped in a `<div>` with the data-ve-aboutgroup attribute set.
*
* This function does not wrap single-element about groups, and does not descend into the
* child elements.
*
* @private
* @param element {HTMLElement} Element to process
*/
function doAboutGrouping( element ) {
var child = element.firstChild, textNodes = [],
prevChild, aboutGroup, aboutWrapper, childAbout, nextChild, i;
while ( child ) {
nextChild = child.nextSibling;
if ( !child.getAttribute ) {
function getAboutGroup( el ) {
var textNodes = [], aboutGroup = [ el ], elAbout, node;
if ( !el.getAttribute || el.getAttribute( 'about' ) === null ) {
return aboutGroup;
}
elAbout = el.getAttribute( 'about' );
for ( node = el.nextSibling; node; node = node.nextSibling ) {
if ( !node.getAttribute ) {
// Text nodes don't have a getAttribute() method. Thanks HTML DOM,
// that's really helpful ^^
textNodes.push( child );
child = nextChild;
textNodes.push( node );
continue;
}
childAbout = child.getAttribute( 'about' );
if ( childAbout && !aboutGroup ) {
// Start of a new about group
aboutGroup = childAbout;
} else if ( childAbout && childAbout === aboutGroup ) {
// Continuation of the current about group
if ( !aboutWrapper ) {
// This is the second child in this group, so the
// previous child is the first child in this group.
// Wrap the previous child
aboutWrapper = doc.createElement( 'div' );
aboutWrapper.setAttribute( 'data-ve-aboutgroup', aboutGroup );
element.insertBefore( aboutWrapper, prevChild );
aboutWrapper.appendChild( prevChild );
}
// Append any outstanding text nodes to the wrapper
for ( i = 0; i < textNodes.length; i++ ) {
aboutWrapper.appendChild( textNodes[i] );
}
// Append this child to the wrapper
aboutWrapper.appendChild( child );
} else if ( aboutGroup ) {
// This child isn't in the current about group
aboutGroup = undefined;
aboutWrapper = undefined;
if ( childAbout ) {
// Start of a new about group
aboutGroup = childAbout;
}
if ( node.getAttribute( 'about' ) === elAbout ) {
aboutGroup = aboutGroup.concat( textNodes );
textNodes = [];
aboutGroup.push( node );
} else {
break;
}
prevChild = child;
child = nextChild;
textNodes = [];
}
return aboutGroup;
}
// Fallback to defaults
annotations = annotations || new ve.AnnotationSet();
path = path || ['document'];
var i, j, childDomElement, annotation, childDataElement, text, childTypes, matches,
wrappingParagraph, prevElement, alien, rdfaType, isLink, childAnnotations,
doc = domElement.ownerDocument,
var i, childDomElement, childDomElements, childDataElement, text, childTypes, matches,
wrappingParagraph, prevElement, childAnnotations, modelName, modelClass,
annotation, childIsContent, aboutGroup,
data = [],
branchType = path[path.length - 1],
branchHasContent = this.nodeFactory.canNodeContainContent( branchType ),
originallyExpectingContent = branchHasContent || !annotations.isEmpty(),
childIsContent,
nextWhitespace = '',
wrappedWhitespace = '',
wrappedWhitespaceIndex,
@ -403,101 +253,25 @@ ve.dm.Converter.prototype.getDataFromDomRecursion = function ( domElement, annot
if ( dataElement ) {
data.push( dataElement );
}
// Do about grouping
// FIXME this assumes every about group is an alien
doAboutGrouping( domElement );
// Add contents
for ( i = 0; i < domElement.childNodes.length; i++ ) {
childDomElement = domElement.childNodes[i];
switch ( childDomElement.nodeType ) {
case Node.ELEMENT_NODE:
// Alienate about groups
if ( childDomElement.hasAttribute( 'data-ve-aboutgroup' ) ) {
alien = createAlien( childDomElement, context, true );
if ( context.inWrapper && alien[0].type === 'alienBlock' ) {
stopWrapping();
} else if (
!context.inWrapper && !context.expectingContent &&
alien[0].type === 'alienInline'
) {
startWrapping();
}
data = data.concat( alien );
processNextWhitespace( alien[0] );
prevElement = alien[0];
break;
}
// HACK handle <meta>/<link> separately because of the
// metaInline/metaBlock distinction
modelName = this.modelRegistry.matchElement( childDomElement );
modelClass = this.modelRegistry.lookup( modelName ) || ve.dm.AlienNode;
// HACK: force MetaNode for <meta>/<link> even if they have an mw: type
// FIXME EWWWWWW find a better way to handle this
if (
childDomElement.nodeName.toLowerCase() === 'meta' ||
childDomElement.nodeName.toLowerCase() === 'link'
( childDomElement.nodeName.toLowerCase() === 'meta' || childDomElement.nodeName.toLowerCase() === 'link' ) &&
( modelClass.prototype instanceof ve.dm.AlienNode || modelClass === ve.dm.AlienNode )
) {
isLink = childDomElement.nodeName.toLowerCase() === 'link';
childDataElement = {
'type': context.expectingContent ? 'metaInline' : 'metaBlock',
'attributes': {
'style': isLink ? 'link' : 'meta',
'key': childDomElement.getAttribute( isLink ? 'rel' : 'property' )
}
};
if ( childDomElement.hasAttribute( isLink ? 'href' : 'content' ) ) {
childDataElement.attributes.value = childDomElement.getAttribute( isLink ? 'href' : 'content' );
}
// Preserve HTML attributes
// FIXME the following is duplicated from getDataElementFromDomElement()
// Include all attributes and prepend 'html/0/' to each attribute name
for ( j = 0; j < childDomElement.attributes.length; j++ ) {
// ..but exclude attributes we've already processed,
// because they'll be overwritten otherwise *sigh*
// FIXME this sucks, we need a new node type API so bad
if (
childDomElement.attributes[j].name !== ( isLink ? 'rel' : 'property' ) &&
childDomElement.attributes[j].name !== ( isLink ? 'href' : 'content' )
) {
childDataElement.attributes['html/0/' + childDomElement.attributes[j].name] = childDomElement.attributes[j].value;
}
}
data.push( childDataElement );
data.push( { 'type': context.expectingContent ? '/metaInline' : '/metaBlock' } );
processNextWhitespace( childDataElement );
prevElement = childDataElement;
break;
}
// Alienate anything with a mw: type that isn't registered
// HACK because we don't actually have an RDFa type registry yet,
// this hardcodes the set of recognized types
rdfaType = childDomElement.getAttribute( 'rel' ) ||
childDomElement.getAttribute( 'typeof' ) ||
childDomElement.getAttribute( 'property' );
if (
rdfaType &&
rdfaType.match( /^mw:/ ) &&
!rdfaType.match( /^mw:WikiLink/ ) &&
!rdfaType.match( /^mw:ExtLink/ ) &&
!rdfaType.match( /^mw:Entity/ )
) {
alien = createAlien( childDomElement, context );
if ( context.inWrapper && alien[0].type === 'alienBlock' ) {
stopWrapping();
} else if (
!context.inWrapper && !context.expectingContent &&
alien[0].type === 'alienInline'
) {
startWrapping();
}
data = data.concat( alien );
processNextWhitespace( alien[0] );
prevElement = alien[0];
break;
modelClass = ve.dm.MetaNode;
}
if ( modelClass.prototype instanceof ve.dm.Annotation ) {
annotation = this.annotationFactory.create( modelName, childDomElement );
// Detect and handle annotated content
// HACK except for mw:Entity. We need a node API rewrite, badly
annotation = this.getDataAnnotationFromDomElement( childDomElement );
if ( annotation && rdfaType !== 'mw:Entity' ) {
// Start auto-wrapping of bare content
// Start wrapping if needed
if ( !context.inWrapper && !context.expectingContent ) {
startWrapping();
prevElement = wrappingParagraph;
@ -511,65 +285,62 @@ ve.dm.Converter.prototype.getDataFromDomRecursion = function ( domElement, annot
undefined, path, context.inWrapper
)
);
break;
}
// Look up child element type
childDataElement = this.getDataElementFromDomElement( childDomElement, annotations );
if ( childDataElement ) {
} else {
aboutGroup = getAboutGroup( childDomElement );
childDomElements = modelClass.static.enableAboutGrouping ?
aboutGroup : [ childDomElement ];
childDataElement = this.createDataElement( modelClass, childDomElements, context );
childIsContent = this.nodeFactory.isNodeContent( childDataElement.type );
// Check that something isn't terribly wrong
if ( !(
// Non-content child in a content container
( originallyExpectingContent && !childIsContent ) ||
// Non-content child trying to break wrapping at
// the wrong level
( context.inWrapper && !context.canCloseWrapper && !childIsContent )
) ) {
// End auto-wrapping of bare content from a previously processed node
// but only if childDataElement is a non-content element
if ( context.inWrapper && context.canCloseWrapper && !childIsContent ) {
// If childIsContent isn't what we expect, adjust
if ( !context.expectingContent && childIsContent ) {
startWrapping();
prevElement = wrappingParagraph;
} else if ( context.expectingContent && !childIsContent ) {
if ( context.inWrapper && context.canCloseWrapper ) {
stopWrapping();
} else if ( !context.inWrapper && !context.expectingContent && childIsContent ) {
startWrapping();
prevElement = wrappingParagraph;
}
if ( this.nodeFactory.canNodeHaveChildren( childDataElement.type ) ) {
// Append child element data
data = data.concat(
this.getDataFromDomRecursion(
childDomElement,
new ve.AnnotationSet(),
childDataElement,
path.concat( childDataElement.type ),
context.inWrapper
)
);
} else {
// Append empty node
data.push( childDataElement );
data.push( { 'type': '/' + childDataElement.type } );
// Alienate
modelClass = ve.dm.AlienNode;
childDomElements = modelClass.static.enableAboutGrouping ?
aboutGroup : [ childDomElement ];
childDataElement = this.createDataElement( modelClass, childDomElements, context );
childIsContent = this.nodeFactory.isNodeContent( childDataElement.type );
}
processNextWhitespace( childDataElement );
prevElement = childDataElement;
break;
}
// If something is wrong, fall through, and the bad child
// will be alienated below.
// Annotate child
if ( childIsContent && !annotations.isEmpty() ) {
childDataElement.annotations = annotations.clone();
}
// Output child and its children, if any
if (
childDomElements.length === 1 &&
this.nodeFactory.canNodeHaveChildren( childDataElement.type )
) {
// Recursion
// Opening and closing elements are added by the recursion too
data = data.concat(
this.getDataFromDomRecursion(
childDomElement,
new ve.AnnotationSet(),
childDataElement,
path.concat( childDataElement.type ),
context.inWrapper
)
);
} else {
// Write an opening and closing
data.push( childDataElement );
data.push( { 'type': '/' + childDataElement.type } );
}
processNextWhitespace( childDataElement );
prevElement = childDataElement;
// In case we consumed multiple childDomElements, adjust i accordingly
i += childDomElements.length - 1;
}
// We don't know what this is, fall back to alien.
alien = createAlien( childDomElement, context );
if ( context.inWrapper && alien[0].type === 'alienBlock' ) {
stopWrapping();
} else if (
!context.inWrapper && !context.expectingContent &&
alien[0].type === 'alienInline'
) {
startWrapping();
}
data = data.concat( alien );
processNextWhitespace( alien[0] );
prevElement = alien[0];
break;
case Node.TEXT_NODE:
text = childDomElement.data;
@ -700,6 +471,7 @@ ve.dm.Converter.prototype.getDataFromDomRecursion = function ( domElement, annot
);
break;
case Node.COMMENT_NODE:
// TODO treat this as a node with nodeName #comment
childDataElement = {
'type': context.expectingContent ? 'metaInline' : 'metaBlock',
'attributes': {
@ -761,7 +533,7 @@ ve.dm.Converter.prototype.getDataFromDomRecursion = function ( domElement, annot
*/
ve.dm.Converter.prototype.getDomFromData = function ( data ) {
var text, i, j, k, annotations, annotation, annotationElement, dataElement, arr,
childDomElement, pre, ours, theirs, parentDomElement, startClosingAt,
childDomElements, pre, ours, theirs, parentDomElement, lastChild, startClosingAt,
isContentNode, changed, parentChanged,
doc = ve.createDocumentFromHTML( '' ),
container = doc.body,
@ -855,8 +627,11 @@ ve.dm.Converter.prototype.getDomFromData = function ( data ) {
domElement.appendChild( doc.createTextNode( text ) );
text = '';
}
// Insert the element
domElement.appendChild( this.getDomElementFromDataElement( data[i], doc ) );
// Insert the elements
childDomElements = this.getDomElementsFromDataElement( data[i], 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++;
}
@ -909,6 +684,11 @@ ve.dm.Converter.prototype.getDomFromData = function ( data ) {
);
}
}
lastChild = domElement.veInternal.childDomElements ?
domElement.veInternal
.childDomElements[domElement.veInternal.childDomElements.length - 1]
.lastChild :
domElement.lastChild;
ours = domElement.veInternal.whitespace[2];
if ( domElement.lastOuterPost === undefined ) {
// This node didn't have any structural children
@ -919,10 +699,7 @@ ve.dm.Converter.prototype.getDomFromData = function ( data ) {
theirs = domElement.lastOuterPost;
}
if ( ours && ours === theirs ) {
if (
domElement.lastChild &&
domElement.lastChild.nodeType === 3
) {
if ( lastChild && lastChild.nodeType === 3 ) {
// Last child is a TextNode, append to it
domElement.lastChild.appendData( ours );
} else {
@ -991,16 +768,19 @@ ve.dm.Converter.prototype.getDomFromData = function ( data ) {
domElement = parentDomElement;
} else {
// Create node from data
childDomElement = this.getDomElementFromDataElement( dataElement, doc );
childDomElements = this.getDomElementsFromDataElement( dataElement, doc );
// Add reference to internal data
if ( dataElement.internal ) {
childDomElement.veInternal = dataElement.internal;
childDomElements[0].veInternal = ve.extendObject(
{ 'childDomElements': childDomElements },
dataElement.internal || {}
);
// Add elements
for ( j = 0; j < childDomElements.length; j++ ) {
domElement.appendChild( childDomElements[j] );
}
// Add element
domElement.appendChild( childDomElement );
// Descend into child node
// Descend into the first child node
parentDomElement = domElement;
domElement = childDomElement;
domElement = childDomElements[0];
// Process outer whitespace
// Every piece of outer whitespace is duplicated somewhere:
@ -1057,11 +837,6 @@ ve.dm.Converter.prototype.getDomFromData = function ( data ) {
delete container.lastOuterPost;
}
// Unwrap multi-child alien wrappers
$( container ).find( '[data-ve-multi-child-alien-wrapper]' ).each( function() {
$( this ).replaceWith( $( this ).contents() );
} );
// Workaround for bug 42469: if a <pre> starts with a newline, that means .innerHTML will
// screw up and stringify it with one fewer newline. Work around this by adding a newline.
// If we don't see a leading newline, we still don't know if the original HTML was
@ -1082,4 +857,4 @@ ve.dm.Converter.prototype.getDomFromData = function ( data ) {
/* Initialization */
ve.dm.converter = new ve.dm.Converter( ve.dm.nodeFactory, ve.dm.annotationFactory );
ve.dm.converter = new ve.dm.Converter( ve.dm.modelRegistry, ve.dm.nodeFactory, ve.dm.annotationFactory );

View file

@ -87,10 +87,14 @@ ve.dm.Node.static.matchRdfaTypes = null;
ve.dm.Node.static.matchFunction = null;
/**
* Static function to convert a DOM element to a linear model data element for this node type.
* Static function to convert a DOM element or set of sibling DOM elements to a linear model data
* element for this node type.
*
* This function is only called if this node "won" the matching for the DOM element, so domElement
* will match this node's matching rule.
* This function is only called if this node "won" the matching for the first DOM element, so
* domElements[0] will match this node's matching rule. There is usually only one node in
* domElements[]. Multiple nodes will only be passed if this node supports about groups.
* If there are multiple nodes, the nodes are all adjacent siblings in the same about group
* (i.e. they are grouped together because they have the same value for the about attribute).
*
* This function is allowed to return a content element when context indicates that a non-content
* element is expected or vice versa. If that happens, the converter deals with it in the following way:
@ -114,7 +118,7 @@ ve.dm.Node.static.matchFunction = null;
*
* @static
* @method
* @param {HTMLElement} domElement DOM element to convert
* @param {HTMLElement[]} domElements DOM elements to convert. Usually only one element
* @param {Object} context Object describing the current state of the converter
* @param {boolean} context.expectingContent Whether this function is expected to return a content element
* @param {boolean} context.inWrapper Whether this element is in a wrapper paragraph generated by the converter;
@ -123,20 +127,24 @@ ve.dm.Node.static.matchFunction = null;
* can only be true if context.inWrapper is also true
* @returns {Object|null} Linear model element, or null to alienate
*/
ve.dm.Node.static.toDataElement = function ( /*domElement, context*/ ) {
ve.dm.Node.static.toDataElement = function ( /*domElements, context*/ ) {
throw new Error( 've.dm.Node subclass must implement toDataElement' );
};
/**
* Static function to convert a linear model data element for this node type back to a DOM element.
* Static function to convert a linear model data element for this node type back to one or more
* DOM elements.
*
* NOTE: If this function returns multiple DOM elements, the DOM elements produced by the children
* of this node (if any) will be attached to the first DOM element in the array.
*
* @static
* @method
* @param {Object} Linear model element with a type property and optionally an attributes property
* @returns {HTMLElement} DOM element
* @returns {HTMLElement[]} DOM elements
*/
ve.dm.Node.static.toDomElement = function ( /*dataElement*/ ) {
throw new Error( 've.dm.Node subclass must implement toDomElement' );
ve.dm.Node.static.toDomElements = function ( /*dataElement*/ ) {
throw new Error( 've.dm.Node subclass must implement toDomElements' );
};
/**
@ -169,7 +177,8 @@ ve.dm.Node.static.enableAboutGrouping = false;
/**
* Whether HTML attributes should be preserved for this node type. If true, the HTML attributes
* of the DOM elements will be stored as linear model attributes. The attribute names be
* html/0/attrName, where attrName is the name of the attribute.
* html/i/attrName, where i is the index of the DOM element in the domElements array, and attrName
* is the name of the attribute.
*
* This should generally be enabled, except for node types that store their entire HTML in an
* attribute.

View file

@ -9,6 +9,8 @@ QUnit.module( 've.dm.Converter' );
/* Tests */
// TODO rewrite to test getDataElementOrAnnotationFromDomElement
/*
QUnit.test( 'getDataElementFromDomElement', 20, function ( assert ) {
var msg, conversion;
@ -21,14 +23,15 @@ QUnit.test( 'getDataElementFromDomElement', 20, function ( assert ) {
);
}
} );
*/
QUnit.test( 'getDomElementFromDataElement', 20, function ( assert ) {
QUnit.test( 'getDomElementsFromDataElement', 20, function ( assert ) {
var msg, conversion;
for ( msg in ve.dm.example.conversions ) {
conversion = ve.dm.example.conversions[msg];
assert.equalDomElement(
ve.dm.converter.getDomElementFromDataElement( conversion.dataElement ),
ve.dm.converter.getDomElementsFromDataElement( conversion.dataElement )[0],
conversion.domElement,
msg
);

View file

@ -81,7 +81,7 @@ ve.dm.StubBarNode.static.matchRdfaTypes = ['bar'];
// HACK keep ve.dm.Converter happy for now
// TODO once ve.dm.Converter is rewritten, this can be removed
ve.dm.StubBarNode.static.toDataElement = function () {};
ve.dm.StubBarNode.static.toDomElement = function () {};
ve.dm.StubBarNode.static.toDomElements = function () {};
/* Tests */

View file

@ -302,7 +302,8 @@ ve.dm.example.withMeta = [
'type': 'metaBlock',
'attributes': {
'style': 'meta',
'key': 'mw:PageProp/nocc'
'key': 'mw:PageProp/nocc',
'html/0/property': 'mw:PageProp/nocc'
}
},
{ 'type': '/metaBlock' },
@ -315,7 +316,9 @@ ve.dm.example.withMeta = [
'attributes': {
'style': 'link',
'key': 'mw:WikiLink/Category',
'value': './Category:Bar'
'value': './Category:Bar',
'html/0/href': './Category:Bar',
'html/0/rel': 'mw:WikiLink/Category'
}
},
{ 'type': '/metaInline' },
@ -327,7 +330,9 @@ ve.dm.example.withMeta = [
'attributes': {
'style': 'meta',
'key': 'mw:foo',
'value': 'bar'
'value': 'bar',
'html/0/content': 'bar',
'html/0/property': 'mw:foo'
}
},
{ 'type': '/metaInline' },
@ -348,7 +353,9 @@ ve.dm.example.withMeta = [
'attributes': {
'style': 'meta',
'key': 'mw:bar',
'value': 'baz'
'value': 'baz',
'html/0/content': 'baz',
'html/0/property': 'mw:bar'
}
},
{ 'type': '/metaBlock' },
@ -365,7 +372,9 @@ ve.dm.example.withMeta = [
'attributes': {
'style': 'link',
'key': 'mw:WikiLink/Category',
'value': './Category:Foo#Bar baz%23quux'
'value': './Category:Foo#Bar baz%23quux',
'html/0/href': './Category:Foo#Bar baz%23quux',
'html/0/rel': 'mw:WikiLink/Category'
}
},
{ 'type': '/metaBlock' },
@ -408,7 +417,8 @@ ve.dm.example.withMetaMetaData = [
'type': 'metaBlock',
'attributes': {
'style': 'meta',
'key': 'mw:PageProp/nocc'
'key': 'mw:PageProp/nocc',
'html/0/property': 'mw:PageProp/nocc'
}
}
],
@ -421,7 +431,9 @@ ve.dm.example.withMetaMetaData = [
'attributes': {
'style': 'link',
'key': 'mw:WikiLink/Category',
'value': './Category:Bar'
'value': './Category:Bar',
'html/0/href': './Category:Bar',
'html/0/rel': 'mw:WikiLink/Category'
}
}
],
@ -433,7 +445,9 @@ ve.dm.example.withMetaMetaData = [
'attributes': {
'style': 'meta',
'key': 'mw:foo',
'value': 'bar'
'value': 'bar',
'html/0/content': 'bar',
'html/0/property': 'mw:foo'
}
}
],
@ -454,7 +468,9 @@ ve.dm.example.withMetaMetaData = [
'attributes': {
'style': 'meta',
'key': 'mw:bar',
'value': 'baz'
'value': 'baz',
'html/0/content': 'baz',
'html/0/property': 'mw:bar'
}
},
{
@ -469,7 +485,9 @@ ve.dm.example.withMetaMetaData = [
'attributes': {
'style': 'link',
'key': 'mw:WikiLink/Category',
'value': './Category:Foo#Bar baz%23quux'
'value': './Category:Foo#Bar baz%23quux',
'html/0/href': './Category:Foo#Bar baz%23quux',
'html/0/rel': 'mw:WikiLink/Category'
}
},
{
@ -478,7 +496,7 @@ ve.dm.example.withMetaMetaData = [
'style': 'meta',
'key': null,
'html/0/typeof': 'mw:Placeholder',
'html/0/data-parsoid': 'foobar',
'html/0/data-parsoid': 'foobar'
}
}
]
@ -865,13 +883,11 @@ ve.dm.example.domToDataCases = {
'data': [
{ 'type': 'paragraph', 'internal': { 'generated': 'wrapper' } },
'1',
{ 'type': '/paragraph' },
{
'type': 'alienBlock',
'type': 'alienInline',
'attributes': { 'html': '<tt about="#mwt1">foo</tt><tt about="#mwt1">bar</tt>' }
},
{ 'type': '/alienBlock' },
{ 'type': 'paragraph', 'internal': { 'generated': 'wrapper' } },
{ 'type': '/alienInline' },
'2',
{ 'type': '/paragraph' }
]
@ -1973,7 +1989,9 @@ ve.dm.example.domToDataCases = {
'attributes': {
'style': 'meta',
'key': 'mw:foo',
'value': 'bar'
'value': 'bar',
'html/0/content': 'bar',
'html/0/property': 'mw:foo'
}
},
{ 'type': '/metaInline' },
@ -2075,4 +2093,4 @@ ve.dm.example.isolationData = [
{ 'type': '/preformatted' },
{ 'type': '/listItem' },
{ 'type': '/list' }
];
];