%PDF- %PDF-
| Direktori : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/dm/ |
| Current File : //www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/dm/ve.dm.Converter.js |
/*!
* VisualEditor DataModel Converter class.
*
* @copyright 2011-2016 VisualEditor Team and others; see http://ve.mit-license.org
*/
/**
* DataModel converter.
*
* Converts between HTML DOM and VisualEditor linear data.
*
* @class
* @constructor
* @param {ve.dm.ModelRegistry} modelRegistry
* @param {ve.dm.NodeFactory} nodeFactory
* @param {ve.dm.AnnotationFactory} annotationFactory
* @param {ve.dm.MetaItemFactory} metaItemFactory
*/
ve.dm.Converter = function VeDmConverter( modelRegistry, nodeFactory, annotationFactory, metaItemFactory ) {
// Properties
this.modelRegistry = modelRegistry;
this.nodeFactory = nodeFactory;
this.annotationFactory = annotationFactory;
this.metaItemFactory = metaItemFactory;
this.doc = null;
this.documentData = null;
this.store = null;
this.internalList = null;
this.forClipboard = null;
this.fromClipboard = null;
this.contextStack = null;
};
/* Inheritance */
OO.initClass( ve.dm.Converter );
/* Static Properties */
/**
* List of HTML attribute names that {#renderHtmlAttributeList} should use computed values for.
* @type {string[]}
*/
ve.dm.Converter.static.computedAttributes = [ 'href', 'src' ];
/* Static Methods */
/**
* Get linear model data from a string optionally applying annotations
*
* @static
* @param {string} text Plain text to convert
* @param {ve.dm.AnnotationSet} [annotations] Annotations to apply
* @return {Array} Linear model data, one element per character
*/
ve.dm.Converter.static.getDataContentFromText = function ( text, annotations ) {
var i, len,
characters = text.split( '' );
if ( !annotations || annotations.isEmpty() ) {
return characters;
}
// Apply annotations to characters
for ( i = 0, len = characters.length; i < len; i++ ) {
// Just store the annotations' indexes from the index-value store
characters[ i ] = [ characters[ i ], annotations.getIndexes().slice() ];
}
return characters;
};
/**
* Utility function for annotation rendering. Transforms one set of annotations into another
* by opening and closing annotations. Each time an annotation is opened or closed, the associated
* callback is called with the annotation passed as a parameter.
*
* Note that currentSet will be modified, and will be equal to targetSet once this function returns.
*
* @static
* @param {ve.dm.AnnotationSet} currentSet The set of annotations currently opened. Will be modified.
* @param {ve.dm.AnnotationSet} targetSet The set of annotations we want to have.
* @param {Function} open Callback called when an annotation is opened. Passed a ve.dm.Annotation.
* @param {Function} close Callback called when an annotation is closed. Passed a ve.dm.Annotation.
*/
ve.dm.Converter.static.openAndCloseAnnotations = function ( currentSet, targetSet, open, close ) {
var i, len, index, startClosingAt, currentSetOpen, targetSetOpen;
// Close annotations as needed
// Go through annotationStack from bottom to top (low to high),
// and find the first annotation that's not in annotations.
targetSetOpen = targetSet.clone();
for ( i = 0, len = currentSet.getLength(); i < len; i++ ) {
index = currentSet.getIndex( i );
// containsComparableForSerialization is expensive,
// so do a simple contains check first
if (
targetSetOpen.containsIndex( index ) ||
targetSetOpen.containsComparableForSerialization( currentSet.get( i ) )
) {
targetSetOpen.removeIndex( index );
} else {
startClosingAt = i;
break;
}
}
if ( startClosingAt !== undefined ) {
// Close all annotations from top to bottom (high to low)
// until we reach startClosingAt
for ( i = currentSet.getLength() - 1; i >= startClosingAt; i-- ) {
close( currentSet.get( i ) );
// Remove from currentClone
currentSet.removeAt( i );
}
}
currentSetOpen = currentSet.clone();
// Open annotations as needed
for ( i = 0, len = targetSet.getLength(); i < len; i++ ) {
index = targetSet.getIndex( i );
// containsComparableForSerialization is expensive,
// so do a simple contains check first
if (
currentSetOpen.containsIndex( index ) ||
currentSetOpen.containsComparableForSerialization( targetSet.get( i ) )
) {
// If an annotation is already open remove it from the currentSetOpen list
// as it may exist multiple times in the targetSet, and so may need to be
// opened again
currentSetOpen.removeIndex( index );
} else {
open( targetSet.get( i ) );
// Add to currentClone
currentSet.pushIndex( index );
}
}
};
/**
* Copy attributes from one set of DOM elements to another.
*
* @static
* @param {HTMLElement[]} originalDomElements Array of DOM elements to render from
* @param {HTMLElement[]} targetDomElements Array of DOM elements to render onto
* @param {boolean|Function} [filter=true] Attribute filter
* @param {boolean} [computed=false] If true, use the computed values of attributes where available
* @param {boolean} [deep=false] Recurse into child nodes
*/
ve.dm.Converter.static.renderHtmlAttributeList = function ( originalDomElements, targetDomElements, filter, computed, deep ) {
var i, ilen, j, jlen, attrs, value;
if ( filter === undefined ) {
filter = true;
}
if ( filter === false ) {
return;
}
for ( i = 0, ilen = originalDomElements.length; i < ilen; i++ ) {
if ( !targetDomElements[ i ] ) {
continue;
}
attrs = originalDomElements[ i ].attributes;
if ( !attrs ) {
continue;
}
for ( j = 0, jlen = attrs.length; j < jlen; j++ ) {
if (
!targetDomElements[ i ].hasAttribute( attrs[ j ].name ) &&
( filter === true || filter( attrs[ j ].name ) )
) {
if ( computed && ve.dm.Converter.static.computedAttributes.indexOf( attrs[ j ].name ) !== -1 ) {
value = originalDomElements[ i ][ attrs[ j ].name ];
} else {
value = attrs[ j ].value;
}
targetDomElements[ i ].setAttribute( attrs[ j ].name, value );
}
if ( filter === true || filter( attrs[ j ].name ) ) {
value = computed && ve.dm.Converter.static.computedAttributes.indexOf( attrs[ j ].name ) !== -1 ?
originalDomElements[ i ][ attrs[ j ].name ] :
attrs[ j ].value;
}
}
// Descend into element children only (skipping text nodes and comment nodes)
if ( deep && originalDomElements[ i ].children.length > 0 ) {
ve.dm.Converter.static.renderHtmlAttributeList(
originalDomElements[ i ].children,
targetDomElements[ i ].children,
filter,
computed,
true
);
}
}
};
/* Methods */
/**
* Check whether this converter instance is currently inside a getModelFromDom() conversion.
*
* @method
* @return {boolean} Whether we're converting
*/
ve.dm.Converter.prototype.isConverting = function () {
return this.contextStack !== null;
};
/**
* Get the IndexValueStore used for the current conversion.
*
* @method
* @return {ve.dm.IndexValueStore|null} Current store, or null if not converting
*/
ve.dm.Converter.prototype.getStore = function () {
return this.store;
};
/**
* Get the HTML document currently being converted
*
* @method
* @return {HTMLDocument|null} HTML document being converted, or null if not converting
*/
ve.dm.Converter.prototype.getHtmlDocument = function () {
return this.doc;
};
/**
* Get the HTML document we are converting data for
*
* @method
* @return {HTMLDocument|null} HTML document being converted for, or null if not converting
*/
ve.dm.Converter.prototype.getTargetHtmlDocument = function () {
return this.targetDoc;
};
/**
* Is the current conversion for the clipboard
*
* @method
* @return {boolean|null} The conversion is for the clipboard, or null if not converting
*/
ve.dm.Converter.prototype.isForClipboard = function () {
return this.forClipboard;
};
/**
* Is the current conversion from the clipboard
*
* @method
* @return {boolean|null} The conversion is from the clipboard, or null if not converting
*/
ve.dm.Converter.prototype.isFromClipboard = function () {
return this.fromClipboard;
};
/**
* Get the current conversion context. This is the recursion state of getDataFromDomSubtree().
*
* @method
* @return {Object|null} Context object, or null if not converting
*/
ve.dm.Converter.prototype.getCurrentContext = function () {
return this.contextStack === null ? null : this.contextStack[ this.contextStack.length - 1 ];
};
/**
* Get the annotations currently being applied by the converter. Note that this is specific to
* the current recursion level.
*
* @method
* @return {ve.dm.AnnotationSet|null} Annotation set, or null if not converting
*/
ve.dm.Converter.prototype.getActiveAnnotations = function () {
var context = this.getCurrentContext();
return context ? context.annotations : null;
};
/**
* Whether the converter is currently expecting content. Note that this is specific to the current
* recursion level.
*
* @method
* @return {boolean|null} Boolean indicating whether content is expected, or null if not converting
*/
ve.dm.Converter.prototype.isExpectingContent = function () {
var context = this.getCurrentContext();
return context ? context.expectingContent : null;
};
/**
* Whether the converter can currently accept a child node with the given type.
*
* @method
* @param {string} nodeType
* @return {boolean|null} Whether the node type is valid, or null if not converting
*/
ve.dm.Converter.prototype.isValidChildNodeType = function ( nodeType ) {
var childTypes,
context = this.getCurrentContext();
if ( !context ) {
return null;
}
childTypes = this.nodeFactory.getChildNodeTypes( context.branchType );
return ( childTypes === null || childTypes.indexOf( nodeType ) !== -1 );
};
/**
* Whether the conversion is currently inside a wrapper paragraph generated by the converter.
* Note that this is specific to the current recursion level.
*
* @method
* @return {boolean|null} Boolean indicating whether we're wrapping, or null if not converting
*/
ve.dm.Converter.prototype.isInWrapper = function () {
var context = this.getCurrentContext();
return context ? context.inWrapper : null;
};
/**
* Whether the active wrapper can be closed. Note that this is specific to the current recursion
* level. If there is no active wrapper, this returns false.
*
* @method
* @return {boolean|null} Boolean indicating whether the wrapper can be closed, or null if not converting
*/
ve.dm.Converter.prototype.canCloseWrapper = function () {
var context = this.getCurrentContext();
return context ? context.canCloseWrapper : null;
};
/**
* Get the DOM element for a given linear model element.
*
* This invokes the toDomElements function registered for the element type.
*
* @method
* @param {Object|Array} dataElements Linear model element or data slice
* @param {HTMLDocument} doc Document to create DOM elements in
* @param {Node[]} [childDomElements] Array of child DOM elements to pass in (annotations only)
* @return {Node[]|boolean} DOM elements, or false if the element cannot be converted.
* If the first DOMelement has a 'handledOwnChildren' property set, the converter treats it as if it
* were a handlesOwnChildren node.
*/
ve.dm.Converter.prototype.getDomElementsFromDataElement = function ( dataElements, doc, childDomElements ) {
var domElements, originalDomElements,
dataElement = Array.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 );
}
if ( nodeClass.static.isInternal ) {
return false;
}
domElements = nodeClass.static.toDomElements( dataElements, doc, this, childDomElements );
if ( !Array.isArray( domElements ) && !( nodeClass.prototype instanceof ve.dm.Annotation ) ) {
throw new Error( 'toDomElements() failed to return an array when converting element of type ' + dataElement.type );
}
originalDomElements = this.store.value( dataElement.originalDomElementsIndex );
// Optimization: don't call renderHtmlAttributeList if returned domElements are equal to the originals
if ( originalDomElements && !ve.isEqualDomElements( domElements, originalDomElements ) ) {
ve.dm.Converter.static.renderHtmlAttributeList(
originalDomElements,
domElements,
nodeClass.static.preserveHtmlAttributes,
// computed
false,
// deep
!( nodeClass instanceof ve.dm.Node ) ||
!this.nodeFactory.canNodeHaveChildren( dataElement.type ) ||
this.nodeFactory.doesNodeHandleOwnChildren( dataElement.type )
);
}
return domElements;
};
/**
* Create a data element from a DOM element.
*
* @param {ve.dm.Model} modelClass Model class to use for conversion
* @param {Node[]} domElements DOM elements to convert
* @return {Object|Array|null} Data element or array of linear model data, or null to alienate
*/
ve.dm.Converter.prototype.createDataElements = function ( modelClass, domElements ) {
var dataElements = modelClass.static.toDataElement( domElements, this );
if ( !dataElements ) {
return null;
}
if ( !Array.isArray( dataElements ) ) {
dataElements = [ dataElements ];
}
if ( dataElements.length ) {
dataElements[ 0 ].originalDomElementsIndex = this.store.index( domElements, domElements.map( ve.getNodeHtml ).join( '' ) );
}
return dataElements;
};
/**
* Build an HTML DOM node for a linear model annotation.
*
* @method
* @param {Object} dataAnnotation Annotation object
* @return {HTMLElement} HTML DOM node
*/
ve.dm.Converter.prototype.getDomElementFromDataAnnotation = function ( dataAnnotation, doc ) {
var htmlData = dataAnnotation.toHtml(),
domElement = doc.createElement( htmlData.tag );
ve.setDomAttributes( domElement, htmlData.attributes );
return domElement;
};
/**
* Convert an HTML document to a document model.
*
* @param {HTMLDocument} doc HTML document to convert
* @param {Object} options Conversion options
* @param {HTMLDocument} [options.targetDoc=doc] Target HTML document we are converting for, if different from doc
* @param {boolean} [options.fromClipboard=false] Conversion is from clipboard
* @param {string} [options.lang] Document language code
* @param {string} [options.dir] Document directionality (ltr/rtl)
* @return {ve.dm.Document} Document model
*/
ve.dm.Converter.prototype.getModelFromDom = function ( doc, options ) {
var linearData, refData, innerWhitespace,
store = new ve.dm.IndexValueStore(),
internalList = new ve.dm.InternalList();
options = options || {};
// Set up the converter state
this.doc = doc;
this.targetDoc = options.targetDoc || doc;
this.fromClipboard = options.fromClipboard;
this.store = store;
this.internalList = internalList;
this.contextStack = [];
// Possibly do things with doc and the head in the future
// Generate data
linearData = new ve.dm.FlatLinearData(
store,
this.getDataFromDomSubtree( doc.body )
);
refData = this.internalList.convertToData( this, doc );
linearData.batchSplice( linearData.getLength(), 0, refData );
innerWhitespace = this.getInnerWhitespace( linearData );
// Clear the state
this.doc = null;
this.targetDoc = null;
this.fromClipboard = null;
this.store = null;
this.internalList = null;
this.contextStack = null;
return new ve.dm.Document( linearData, doc, undefined, internalList, innerWhitespace, options.lang, options.dir );
};
/**
* Wrapper for getDataFromDom which resets contextStack before the call
* and then set it back after the call.
*
* TODO: This is kind of a hack, better implementation would be more appropriate in near future.
*
* @method
* @param {HTMLElement} domElement HTML element to convert
* @param {Object} [wrapperElement] Data element to wrap the returned data in
* @param {ve.dm.AnnotationSet} [annotationSet] Override the set of annotations to use
* @return {Array} Linear model data
*/
ve.dm.Converter.prototype.getDataFromDomClean = function ( domElement, wrapperElement, annotationSet ) {
var result, contextStack = this.contextStack;
this.contextStack = [];
result = this.getDataFromDomSubtree( domElement, wrapperElement, annotationSet );
this.contextStack = contextStack;
return result;
};
/**
* Get linear model data from a DOM node. Called recursively. For internal use
* and ve.dm.Model.static.toDataElement() implementations.
*
* @method
* @param {HTMLElement} domElement HTML element to convert
* @param {Object} [wrapperElement] Data element to wrap the returned data in
* @param {ve.dm.AnnotationSet} [annotationSet] Override the set of annotations to use
* @return {Array} Linear model data
*/
ve.dm.Converter.prototype.getDataFromDomSubtree = function ( domElement, wrapperElement, annotationSet ) {
var i, childNode, childNodes, childDataElements, text, matches,
wrappingParagraph, prevElement, childAnnotations, modelName, modelClass,
annotation, childIsContent, aboutGroup, emptyParagraph,
modelRegistry = this.modelRegistry,
data = [],
nextWhitespace = '',
wrappedWhitespace = '',
wrappedWhitespaceIndex,
wrappedMetaItems = [],
context = {},
prevContext = this.contextStack.length ?
this.contextStack[ this.contextStack.length - 1 ] : null;
/**
* Add whitespace to an element at a specific offset.
*
* @private
* @param {Array} element Data element
* @param {number} index Whitespace index, 0-3
* @param {string} whitespace Whitespace content
*/
function addWhitespace( element, index, whitespace ) {
if ( !whitespace ) {
return;
}
if ( !element.internal ) {
element.internal = {};
}
// whitespace = [ outerPre, innerPre, innerPost, outerPost ]
// <tag> text </tag> <nextTag>
// ^^^^^^^^ ^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^
// outerPre innerPre innerPost outerPost
if ( !element.internal.whitespace ) {
element.internal.whitespace = [];
}
element.internal.whitespace[ index ] = whitespace;
}
function processNextWhitespace( element ) {
// This function uses and changes nextWhitespace in the outer function's scope,
// which means it's not really a function but more of a shortcut.
if ( nextWhitespace !== '' ) {
addWhitespace( element, 0, nextWhitespace );
nextWhitespace = '';
}
}
// FIXME rewrite this horrible meta item / whitespace queueing/wrapping business
function outputWrappedMetaItems( whitespaceTreatment ) {
var i, len,
toInsert = [],
prev = wrappingParagraph;
for ( i = 0, len = wrappedMetaItems.length; i < len; i++ ) {
if ( wrappedMetaItems[ i ].type && wrappedMetaItems[ i ].type.charAt( 0 ) !== '/' ) {
if ( wrappedMetaItems[ i ].internal && wrappedMetaItems[ i ].internal.whitespace ) {
if ( whitespaceTreatment === 'restore' ) {
toInsert = toInsert.concat( ve.dm.Converter.static.getDataContentFromText(
wrappedMetaItems[ i ].internal.whitespace[ 0 ], context.annotations
) );
delete wrappedMetaItems[ i ].internal;
} else if ( whitespaceTreatment === 'fixup' ) {
addWhitespace( prev, 3, wrappedMetaItems[ i ].internal.whitespace[ 0 ] );
}
}
prev = wrappedMetaItems[ i ];
}
toInsert.push( wrappedMetaItems[ i ] );
}
if ( wrappedWhitespace !== '' && whitespaceTreatment === 'restore' ) {
// If we have wrapped whitespace, insert the wrapped meta items before it
// This is horrible and this whole system desperately needs to be rewritten
ve.batchSplice( data, wrappedWhitespaceIndex, 0, toInsert );
} else {
data = data.concat( toInsert );
}
wrappedMetaItems = [];
}
function startWrapping() {
// Mark this paragraph as having been generated by
// us, so we can strip it on the way out
wrappingParagraph = {
type: 'paragraph',
internal: { generated: 'wrapper' }
};
data.push( wrappingParagraph );
context.inWrapper = true;
context.canCloseWrapper = true;
context.expectingContent = true;
processNextWhitespace( wrappingParagraph );
}
function stopWrapping() {
if ( wrappedWhitespace !== '' ) {
// Remove wrappedWhitespace from data
data.splice( wrappedWhitespaceIndex, wrappedWhitespace.length );
// Add whitespace to the last sibling: either the last meta item or the wrapper paragraph
addWhitespace( wrappedMetaItems.length > 0 ? wrappedMetaItems[ wrappedMetaItems.length - 2 ] : wrappingParagraph, 3, wrappedWhitespace );
nextWhitespace = wrappedWhitespace;
}
data.push( { type: '/paragraph' } );
outputWrappedMetaItems( 'fixup' );
wrappingParagraph = undefined;
context.inWrapper = false;
context.canCloseWrapper = false;
context.expectingContent = context.originallyExpectingContent;
}
function getAboutGroup( node ) {
var about,
aboutGroup = [ node ];
if ( node.nodeType !== Node.ELEMENT_NODE || node.getAttribute( 'about' ) === null ) {
return aboutGroup;
}
about = node.getAttribute( 'about' );
while ( ( node = node.nextSibling ) !== null ) {
if ( node.nodeType === Node.ELEMENT_NODE && node.getAttribute( 'about' ) === about ) {
aboutGroup.push( node );
} else {
break;
}
}
return aboutGroup;
}
function isAllInstanceOf( data, targetClass ) {
var i, type, itemClass;
for ( i = data.length - 1; i >= 0; i-- ) {
type = ve.dm.LinearData.static.getType( data[ i ] );
if ( type ) {
itemClass = modelRegistry.lookup( type ) || ve.dm.AlienNode;
if ( !( itemClass === targetClass || itemClass.prototype instanceof targetClass ) ) {
return false;
}
} else {
return false;
}
}
return true;
}
context.annotations = annotationSet || (
prevContext ? prevContext.annotations.clone() : new ve.dm.AnnotationSet( this.store )
);
context.branchType = wrapperElement ? wrapperElement.type : (
prevContext ? prevContext.branchType : 'document'
);
context.branchHasContent = this.nodeFactory.canNodeContainContent( context.branchType );
context.originallyExpectingContent = context.branchHasContent || !context.annotations.isEmpty();
context.expectingContent = context.originallyExpectingContent;
context.inWrapper = prevContext ? prevContext.inWrapper : false;
context.canCloseWrapper = false;
this.contextStack.push( context );
// Open element
if ( wrapperElement ) {
data.push( wrapperElement );
}
// Add contents
for ( i = 0; i < domElement.childNodes.length; i++ ) {
childNode = domElement.childNodes[ i ];
switch ( childNode.nodeType ) {
case Node.ELEMENT_NODE:
case Node.COMMENT_NODE:
if (
childNode.getAttribute &&
childNode.getAttribute( 'data-ve-ignore' )
) {
continue;
}
aboutGroup = getAboutGroup( childNode );
modelName = this.modelRegistry.matchElement( childNode, aboutGroup.length > 1 );
modelClass = this.modelRegistry.lookup( modelName ) || ve.dm.AlienNode;
if ( modelClass.prototype instanceof ve.dm.Annotation ) {
childNodes = [ childNode ];
} else {
// Node or meta item
childNodes = modelClass.static.enableAboutGrouping ?
aboutGroup : [ childNode ];
}
childDataElements = this.createDataElements( modelClass, childNodes );
if ( !childDataElements ) {
// Alienate
modelClass = ve.dm.AlienNode;
childNodes = modelClass.static.enableAboutGrouping ?
aboutGroup : [ childNode ];
childDataElements = this.createDataElements( modelClass, childNodes );
} else if ( childDataElements.length ) {
// Update modelClass to reflect the type we got back
modelClass = this.modelRegistry.lookup( childDataElements[ 0 ].type );
} else {
continue;
}
// Now take the appropriate action based on that
if ( modelClass.prototype instanceof ve.dm.Annotation ) {
annotation = this.annotationFactory.createFromElement( childDataElements[ 0 ], this.store );
// Start wrapping if needed
if ( !context.inWrapper && !context.expectingContent ) {
startWrapping();
prevElement = wrappingParagraph;
}
// Append child element data
childAnnotations = context.annotations.clone();
childAnnotations.push( annotation );
childDataElements = this.getDataFromDomSubtree( childNode, undefined, childAnnotations );
if ( !childDataElements.length || isAllInstanceOf( childDataElements, ve.dm.AlienMetaItem ) ) {
// Empty annotation, create a meta item
childDataElements = this.createDataElements( ve.dm.AlienMetaItem, childNodes );
childDataElements.push( { type: '/' + childDataElements[ 0 ].type } );
// Annotate meta item
if ( !context.annotations.isEmpty() ) {
childDataElements[ 0 ].annotations = context.annotations.getIndexes().slice();
}
}
outputWrappedMetaItems( 'restore' );
data = data.concat( childDataElements );
// Clear wrapped whitespace
wrappedWhitespace = '';
} else {
// Node or meta item
if ( modelClass.prototype instanceof ve.dm.MetaItem ) {
// No additional processing needed
// Write to data and continue
if ( childDataElements.length === 1 ) {
childDataElements.push( { type: '/' + childDataElements[ 0 ].type } );
}
// Annotate meta item
if ( !context.annotations.isEmpty() ) {
childDataElements[ 0 ].annotations = context.annotations.getIndexes().slice();
}
// Queue wrapped meta items only if it's actually possible for us to move them out
// of the wrapper
if ( context.inWrapper && context.canCloseWrapper ) {
wrappedMetaItems = wrappedMetaItems.concat( childDataElements );
if ( wrappedWhitespace !== '' ) {
data.splice( wrappedWhitespaceIndex, wrappedWhitespace.length );
addWhitespace( childDataElements[ 0 ], 0, wrappedWhitespace );
nextWhitespace = wrappedWhitespace;
wrappedWhitespace = '';
}
} else {
outputWrappedMetaItems( 'restore' );
data = data.concat( childDataElements );
processNextWhitespace( childDataElements[ 0 ] );
prevElement = childDataElements[ 0 ];
}
// In case we consumed multiple childNodes, adjust i accordingly
i += childNodes.length - 1;
break;
}
childIsContent = this.nodeFactory.isNodeContent( childDataElements[ 0 ].type );
// 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 {
// Alienate
modelClass = ve.dm.AlienNode;
childNodes = modelClass.static.enableAboutGrouping ?
aboutGroup : [ childNode ];
childDataElements = this.createDataElements( modelClass, childNodes );
childIsContent = this.nodeFactory.isNodeContent( childDataElements[ 0 ].type );
}
}
// If we're inserting content into a wrapper, any wrapped whitespace and meta
// items up until this point are here to stay
if ( context.inWrapper && childIsContent ) {
outputWrappedMetaItems( 'restore' );
wrappedWhitespace = '';
// Don't record the wrapped whitespace as the child node's outer whitespace
nextWhitespace = '';
}
// Annotate child
if ( childIsContent && !context.annotations.isEmpty() ) {
childDataElements[ 0 ].annotations = context.annotations.getIndexes().slice();
}
// Output child and process children if needed
if (
childDataElements.length === 1 &&
childNodes.length === 1 &&
this.nodeFactory.canNodeHaveChildren( childDataElements[ 0 ].type ) &&
!this.nodeFactory.doesNodeHandleOwnChildren( childDataElements[ 0 ].type )
) {
// Recursion
// Opening and closing elements are added by the recursion too
outputWrappedMetaItems( 'restore' );
data = data.concat(
this.getDataFromDomSubtree( childNode, childDataElements[ 0 ],
new ve.dm.AnnotationSet( this.store )
)
);
} else {
if ( childDataElements.length === 1 ) {
childDataElements.push( { type: '/' + childDataElements[ 0 ].type } );
}
// Write childDataElements directly
outputWrappedMetaItems( 'restore' );
data = data.concat( childDataElements );
}
processNextWhitespace( childDataElements[ 0 ] );
prevElement = childDataElements[ 0 ];
// In case we consumed multiple childNodes, adjust i accordingly
i += childNodes.length - 1;
}
break;
case Node.TEXT_NODE:
text = childNode.data;
if ( text === '' ) {
// Empty text node?!?
break;
}
if ( !context.originallyExpectingContent ) {
// Strip and store outer whitespace
if ( text.match( /^\s+$/ ) ) {
// This text node is whitespace only
if ( context.inWrapper ) {
// We're already wrapping, so output this whitespace
// and store it in wrappedWhitespace (see
// comment about wrappedWhitespace below)
wrappedWhitespace = text;
wrappedWhitespaceIndex = data.length;
data = data.concat(
ve.dm.Converter.static.getDataContentFromText( wrappedWhitespace, context.annotations )
);
} else {
// We're not in wrapping mode, store this whitespace
if ( !prevElement ) {
if ( wrapperElement ) {
// First child, store as inner
// whitespace in the parent
addWhitespace( wrapperElement, 1, text );
}
// Else, WTF?!? This is not supposed to
// happen, but it's not worth
// throwing an exception over.
} else {
addWhitespace( prevElement, 3, text );
}
nextWhitespace = text;
wrappedWhitespace = '';
outputWrappedMetaItems( 'restore' );
}
// We're done, no actual text left to process
break;
} else {
// This text node contains actual text
// Separate the real text from the whitespace
// HACK: . doesn't match newlines in JS, so use
// [\s\S] to match any character
matches = text.match( /^(\s*)([\s\S]*?)(\s*)$/ );
if ( !context.inWrapper ) {
// Wrap the text in a paragraph and output it
startWrapping();
// Only store leading whitespace if we just
// started wrapping
if ( matches[ 1 ] !== '' ) {
if ( !prevElement ) {
if ( wrapperElement ) {
// First child, store as inner
// whitespace in the parent
addWhitespace( wrapperElement, 1, matches[ 1 ] );
}
// Else, WTF?!? This is not supposed to
// happen, but it's not worth
// throwing an exception over.
} else {
addWhitespace( prevElement, 3, matches[ 1 ] );
}
addWhitespace( wrappingParagraph, 0, matches[ 1 ] );
}
} else {
outputWrappedMetaItems( 'restore' );
// We were already wrapping in a paragraph,
// so the leading whitespace must be output
data = data.concat(
ve.dm.Converter.static.getDataContentFromText( matches[ 1 ], context.annotations )
);
}
// Output the text sans whitespace
data = data.concat(
ve.dm.Converter.static.getDataContentFromText( matches[ 2 ], context.annotations )
);
// Don't store this in wrappingParagraph.internal.whitespace[3]
// and nextWhitespace just yet. Instead, store it
// in wrappedWhitespace. There might be more text
// nodes after this one, so we output wrappedWhitespace
// for now and undo that if it turns out this was
// the last text node. We can't output it later
// because we have to apply the correct annotations.
wrappedWhitespace = matches[ 3 ];
wrappedWhitespaceIndex = data.length;
data = data.concat(
ve.dm.Converter.static.getDataContentFromText( wrappedWhitespace, context.annotations )
);
prevElement = wrappingParagraph;
break;
}
}
// Strip leading and trailing inner whitespace
// (but only in non-annotation nodes)
// and store it so it can be restored later.
if (
context.annotations.isEmpty() && i === 0 && wrapperElement &&
!this.nodeFactory.doesNodeHaveSignificantWhitespace( wrapperElement.type )
) {
// Strip leading whitespace from the first child
matches = text.match( /^\s+/ );
if ( matches && matches[ 0 ] !== '' ) {
addWhitespace( wrapperElement, 1, matches[ 0 ] );
text = text.slice( matches[ 0 ].length );
}
}
if (
context.annotations.isEmpty() &&
i === domElement.childNodes.length - 1 &&
wrapperElement &&
!this.nodeFactory.doesNodeHaveSignificantWhitespace( wrapperElement.type )
) {
// Strip trailing whitespace from the last child
matches = text.match( /\s+$/ );
if ( matches && matches[ 0 ] !== '' ) {
addWhitespace( wrapperElement, 2, matches[ 0 ] );
text = text.slice( 0, text.length - matches[ 0 ].length );
}
}
// Annotate the text and output it
data = data.concat(
ve.dm.Converter.static.getDataContentFromText( text, context.annotations )
);
break;
}
}
// End auto-wrapping of bare content
if ( context.inWrapper && context.canCloseWrapper ) {
stopWrapping();
// HACK: don't set context.inWrapper = false here because it's checked below
context.inWrapper = true;
}
// If we're closing a node that doesn't have any children, but could contain a paragraph,
// add a paragraph. This prevents things like empty list items
if ( context.branchType !== 'paragraph' && wrapperElement && data[ data.length - 1 ] === wrapperElement &&
!context.inWrapper && !this.nodeFactory.canNodeContainContent( context.branchType ) &&
!this.nodeFactory.isNodeContent( context.branchType ) &&
this.isValidChildNodeType( 'paragraph' )
) {
emptyParagraph = { type: 'paragraph', internal: { generated: 'empty' } };
processNextWhitespace( emptyParagraph );
data.push( emptyParagraph );
data.push( { type: '/paragraph' } );
}
// Close element
if ( wrapperElement ) {
// Add the whitespace after the last child to the parent as innerPost
// But don't do this if the parent is empty, because in that case we've already put that
// whitespace in innerPre
if ( nextWhitespace !== '' && data[ data.length - 1 ] !== wrapperElement ) {
addWhitespace( wrapperElement, 2, nextWhitespace );
nextWhitespace = '';
}
data.push( { type: '/' + wrapperElement.type } );
}
// Don't return an empty document
if ( context.branchType === 'document' && isAllInstanceOf( data, ve.dm.MetaItem ) && !annotationSet ) {
emptyParagraph = { type: 'paragraph', internal: { generated: 'empty' } };
processNextWhitespace( emptyParagraph );
data.push( emptyParagraph );
data.push( { type: '/paragraph' } );
}
this.contextStack.pop();
return data;
};
/**
* Get inner whitespace from linear data
*
* @param {ve.dm.FlatLinearData} data Linear model data
* @return {Array} innerWhitespace Inner whitespace
*/
ve.dm.Converter.prototype.getInnerWhitespace = function ( data ) {
var whitespace,
innerWhitespace = new Array( 2 ),
stack = 0,
last = data.getLength() - 1;
if ( data.isOpenElementData( 0 ) ) {
whitespace = ve.getProp( data.getData( 0 ), 'internal', 'whitespace' );
innerWhitespace[ 0 ] = whitespace ? whitespace[ 0 ] : undefined;
}
if ( data.isCloseElementData( last ) ) {
// Find matching opening tag of the last close tag
stack++;
while ( --last ) {
if ( data.isCloseElementData( last ) ) {
stack++;
} else if ( data.isOpenElementData( last ) ) {
stack--;
if ( stack === 0 && data.getType( last ) !== 'internalList' ) {
break;
}
}
}
whitespace = ve.getProp( data.getData( last ), 'internal', 'whitespace' );
innerWhitespace[ 1 ] = whitespace ? whitespace[ 3 ] : undefined;
}
return innerWhitespace;
};
/**
* Convert document model to an HTML DOM
*
* @method
* @param {ve.dm.Document} model Document model
* @param {boolean} [forClipboard=false] Conversion is for clipboard
* @return {HTMLDocument} Document containing the resulting HTML
*/
ve.dm.Converter.prototype.getDomFromModel = function ( model, forClipboard ) {
var doc = ve.createDocumentFromHtml( '' );
this.getDomSubtreeFromModel( model, doc.body, forClipboard );
return doc;
};
/**
* Convert model node to an HTML DOM
*
* @method
* @param {ve.dm.Node} node Model node
* @param {boolean} [forClipboard=false] Conversion is for clipboard
* @return {HTMLDocument} Document containing the resulting HTML
*/
ve.dm.Converter.prototype.getDomFromNode = function ( node, forClipboard ) {
return this.getDomFromModel(
node.getDocument().shallowCloneFromRange( node.isInternal() ? node.getRange() : node.getOuterRange() ),
forClipboard
);
};
/**
* Convert document model to an HTML DOM subtree and add it to a container element.
*
* @method
* @param {ve.dm.Document} model Document model
* @param {HTMLElement} container DOM element to add the generated elements to. Should be empty.
* @param {boolean} [forClipboard=false] Conversion is for clipboard
*/
ve.dm.Converter.prototype.getDomSubtreeFromModel = function ( model, container, forClipboard ) {
// Set up the converter state
this.documentData = model.getFullData();
this.store = model.getStore();
this.internalList = model.getInternalList();
this.forClipboard = !!forClipboard;
this.getDomSubtreeFromData( this.documentData, container, model.getInnerWhitespace() );
// Clear the state
this.documentData = null;
this.store = null;
this.internalList = null;
this.forClipboard = null;
};
/**
* Convert linear model data to an HTML DOM subtree and add it to a container element.
*
* @param {Array} data Linear model data
* @param {HTMLElement} container DOM element to add the generated elements to. Should be empty.
* @param {Array} [innerWhitespace] Inner whitespace if the container is the body
* @throws Unbalanced data: looking for closing /type
*/
ve.dm.Converter.prototype.getDomSubtreeFromData = function ( data, container, innerWhitespace ) {
var text, i, j, isStart, annotations, dataElement, dataElementOrSlice, oldLastOuterPost,
childDomElements, pre, ours, theirs, parentDomElement, lastChild, isContentNode, sibling,
previousSiblings, doUnwrap, textNode, type, annotatedDomElementStack, annotatedDomElements,
dataLen = data.length,
canContainContentStack = [],
converter = this,
doc = container.ownerDocument,
domElement = container,
annotationStack = new ve.dm.AnnotationSet( this.store );
// TODO this whole function should be rewritten with a domElementStack and ascend() and
// descend() functions, to build the whole DOM bottom-up rather than top-down. That would make
// unwrapping easier and will hopefully result in fewer DOM operations.
function openAnnotation() {
// Add text if needed
if ( text.length > 0 ) {
annotatedDomElements.push( doc.createTextNode( text ) );
text = '';
}
annotatedDomElements = [];
annotatedDomElementStack.push( annotatedDomElements );
}
function closeAnnotation( annotation ) {
var i, len, annotationElement, annotatedChildDomElements,
matches, first, last,
leading = '',
trailing = '',
originalDomElements = annotation.getOriginalDomElements( converter.store ),
origElementText = originalDomElements[ 0 ] &&
originalDomElements[ 0 ].textContent ||
'';
// Add text if needed
if ( text.length > 0 ) {
annotatedDomElements.push( doc.createTextNode( text ) );
text = '';
}
annotatedChildDomElements = annotatedDomElementStack.pop();
annotatedDomElements = annotatedDomElementStack[ annotatedDomElementStack.length - 1 ];
// HACK: Move any leading and trailing whitespace out of the annotation, but only if the
// annotation didn't originally have leading/trailing whitespace
first = annotatedChildDomElements[ 0 ];
while (
first &&
first.nodeType === Node.TEXT_NODE &&
( matches = first.data.match( /^\s+/ ) ) &&
!origElementText.match( /^\s/ )
) {
leading += matches[ 0 ];
first.deleteData( 0, matches[ 0 ].length );
if ( first.data.length !== 0 ) {
break;
}
// Remove empty text node
annotatedChildDomElements.shift();
// Process next text node to see if it also has whitespace
first = annotatedChildDomElements[ 0 ];
}
last = annotatedChildDomElements[ annotatedChildDomElements.length - 1 ];
while (
last &&
last.nodeType === Node.TEXT_NODE &&
( matches = last.data.match( /\s+$/ ) ) &&
!origElementText.match( /\s$/ )
) {
trailing = matches[ 0 ] + trailing;
last.deleteData( last.data.length - matches[ 0 ].length, matches[ 0 ].length );
if ( last.data.length !== 0 ) {
break;
}
// Remove empty text node
annotatedChildDomElements.pop();
// Process next text node to see if it also has whitespace
last = annotatedChildDomElements[ annotatedChildDomElements.length - 1 ];
}
if ( annotatedChildDomElements.length ) {
annotationElement = converter.getDomElementsFromDataElement(
annotation.getElement(), doc, annotatedChildDomElements
)[ 0 ];
}
if ( leading ) {
annotatedDomElements.push( doc.createTextNode( leading ) );
}
if ( annotationElement ) {
for ( i = 0, len = annotatedChildDomElements.length; i < len; i++ ) {
annotationElement.appendChild( annotatedChildDomElements[ i ] );
}
annotatedDomElements.push( annotationElement );
} else {
for ( i = 0, len = annotatedChildDomElements.length; i < len; i++ ) {
annotatedDomElements.push( annotatedChildDomElements[ i ] );
}
}
if ( trailing ) {
annotatedDomElements.push( doc.createTextNode( trailing ) );
}
}
function findEndOfNode( i ) {
var j, depth;
for ( j = i + 1, depth = 1; j < dataLen && depth > 0; j++ ) {
if ( data[ j ].type ) {
depth += data[ j ].type.charAt( 0 ) === '/' ? -1 : 1;
}
}
if ( depth !== 0 ) {
throw new Error( 'Unbalanced data: ' + depth + ' element(s) left open.' );
}
return j;
}
function getDataElementOrSlice() {
var dataSlice;
if (
ve.dm.nodeFactory.lookup( data[ i ].type ) &&
ve.dm.nodeFactory.doesNodeHandleOwnChildren( data[ i ].type )
) {
dataSlice = data.slice( i, findEndOfNode( i ) );
} else {
dataSlice = data[ i ];
}
return dataSlice;
}
function removeInternalNodes() {
var dataCopy, endOffset;
// See if there is an internalList in the data, and if there is one, remove it
// Removing it here prevents unwanted interactions with whitespace preservation
for ( i = 0; i < dataLen; i++ ) {
if (
data[ i ].type && data[ i ].type.charAt( 0 ) !== '/' &&
ve.dm.nodeFactory.lookup( data[ i ].type ) &&
ve.dm.nodeFactory.isNodeInternal( data[ i ].type )
) {
// Copy data if we haven't already done so
if ( !dataCopy ) {
dataCopy = data.slice();
}
endOffset = findEndOfNode( i );
// Remove this node's data from dataCopy
dataCopy.splice( i - ( dataLen - dataCopy.length ), endOffset - i );
// Move i such that it will be at endOffset in the next iteration
i = endOffset - 1;
}
}
if ( dataCopy ) {
data = dataCopy;
dataLen = data.length;
}
}
removeInternalNodes();
for ( i = 0; i < dataLen; i++ ) {
if ( typeof data[ i ] === 'string' ) {
// Text
text = '';
isStart = i > 0 &&
ve.dm.LinearData.static.isOpenElementData( data[ i - 1 ] ) &&
!ve.dm.nodeFactory.doesNodeHaveSignificantWhitespace(
ve.dm.LinearData.static.getType( data[ i - 1 ] )
);
// Continue forward as far as the plain text goes
while ( typeof data[ i ] === 'string' ) {
// HACK: Skip over leading whitespace (T53462/T142132) in non-whitespace-preserving tags
// This should possibly be handled by Parsoid or in the UI.
if ( !( isStart && data[ i ].match( /\s/ ) && !this.forClipboard ) ) {
text += data[ i ];
isStart = false;
}
i++;
}
// i points to the first non-text thing, go back one so we don't skip this later
i--;
// Add text
if ( text.length > 0 ) {
domElement.appendChild( doc.createTextNode( text ) );
}
} else if (
Array.isArray( data[ i ] ) ||
(
data[ i ].annotations !== undefined && (
this.metaItemFactory.lookup( data[ i ].type ) ||
this.nodeFactory.isNodeContent( data[ i ].type )
)
)
) {
// Annotated text, nodes or meta
text = '';
annotatedDomElements = [];
annotatedDomElementStack = [ annotatedDomElements ];
while (
data[ i ] !== undefined && (
Array.isArray( data[ i ] ) ||
(
data[ i ].annotations !== undefined && (
this.metaItemFactory.lookup( data[ i ].type ) ||
this.nodeFactory.isNodeContent( data[ i ].type )
)
)
)
) {
annotations = new ve.dm.AnnotationSet(
this.store, data[ i ].annotations || data[ i ][ 1 ]
);
ve.dm.Converter.static.openAndCloseAnnotations( annotationStack, annotations,
openAnnotation, closeAnnotation
);
if ( data[ i ].annotations === undefined ) {
// Annotated text
text += data[ i ][ 0 ];
} else {
// Annotated node
// Add text if needed
if ( text.length > 0 ) {
annotatedDomElements.push( doc.createTextNode( text ) );
text = '';
}
// Insert the elements
dataElementOrSlice = getDataElementOrSlice();
childDomElements = this.getDomElementsFromDataElement( dataElementOrSlice, doc );
for ( j = 0; j < childDomElements.length; j++ ) {
annotatedDomElements.push( childDomElements[ j ] );
}
if ( Array.isArray( dataElementOrSlice ) ) {
i += dataElementOrSlice.length - 1;
} else {
i++; // Skip the closing
}
}
i++;
}
// We're now at the first non-annotated thing, go back one so we don't skip this later
i--;
// Add any gathered text
if ( text.length > 0 ) {
annotatedDomElements.push( doc.createTextNode( text ) );
text = '';
}
// Close any remaining annotations
ve.dm.Converter.static.openAndCloseAnnotations( annotationStack, new ve.dm.AnnotationSet( this.store ),
openAnnotation, closeAnnotation
);
// Put the annotated nodes in the DOM
for ( j = 0; j < annotatedDomElements.length; j++ ) {
domElement.appendChild( annotatedDomElements[ j ] );
}
} else if ( data[ i ].type !== undefined ) {
dataElement = data[ i ];
// Element
if ( dataElement.type.charAt( 0 ) === '/' ) {
// Close element
parentDomElement = domElement.parentNode;
type = data[ i ].type.slice( 1 );
if ( this.metaItemFactory.lookup( type ) ) {
isContentNode = canContainContentStack[ canContainContentStack.length - 1 ];
} else {
isContentNode = this.nodeFactory.isNodeContent( type );
canContainContentStack.pop();
}
// Process whitespace
// whitespace = [ outerPre, innerPre, innerPost, outerPost ]
oldLastOuterPost = parentDomElement.lastOuterPost;
if (
!isContentNode &&
domElement.veInternal &&
domElement.veInternal.whitespace
) {
// Process inner whitespace. innerPre is for sure legitimate
// whitespace that should be inserted; if it was a duplicate
// of our child's outerPre, we would have cleared it.
pre = domElement.veInternal.whitespace[ 1 ];
if ( pre ) {
if (
domElement.firstChild &&
domElement.firstChild.nodeType === Node.TEXT_NODE
) {
// First child is a TextNode, prepend to it
domElement.firstChild.insertData( 0, pre );
} else {
// Prepend a TextNode
textNode = doc.createTextNode( pre );
textNode.veIsWhitespace = true;
domElement.insertBefore(
textNode,
domElement.firstChild
);
}
}
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
// (i.e. it's a content-containing node), so there's
// nothing to check innerPost against
theirs = ours;
} else {
theirs = domElement.lastOuterPost;
}
if ( ours && ours === theirs ) {
if ( lastChild && lastChild.nodeType === Node.TEXT_NODE ) {
// Last child is a TextNode, append to it
domElement.lastChild.appendData( ours );
} else {
// Append a TextNode
textNode = doc.createTextNode( ours );
textNode.veIsWhitespace = true;
domElement.appendChild(
textNode
);
}
}
// Tell the parent about our outerPost
parentDomElement.lastOuterPost = domElement.veInternal.whitespace[ 3 ] || '';
} else if ( !isContentNode ) {
// Use empty string, because undefined means there were no
// structural children
parentDomElement.lastOuterPost = '';
}
// else don't touch lastOuterPost
// Logic to unwrap empty & wrapper nodes.
// It would be nicer if we could avoid generating in the first
// place, but then remembering where we have to skip ascending
// to the parent would be tricky.
doUnwrap = false;
if ( domElement.veInternal ) {
switch ( domElement.veInternal.generated ) {
case 'slug':
// 'slug' elements - remove if they are still empty
if ( domElement.childNodes.length === 0 ) {
doUnwrap = true;
}
break;
case 'empty':
// 'empty' elements - first ensure they are actually empty
if ( domElement.childNodes.length === 0 && (
// then check that we are the last child
// before unwrapping (and therefore destroying)
i === data.length - 1 ||
data[ i + 1 ].type.charAt( 0 ) === '/'
)
) {
doUnwrap = true;
}
break;
case 'wrapper':
// 'wrapper' elements - ensure there is a block level
// element between this element and the previous sibling
// wrapper or parent node
doUnwrap = true;
previousSiblings = domElement.parentElement.childNodes;
// Note: previousSiblings includes the current element
// so we only go up to length - 2
for ( j = previousSiblings.length - 2; j >= 0; j-- ) {
sibling = previousSiblings[ j ];
if ( sibling.nodeType === Node.TEXT_NODE && !sibling.veIsWhitespace ) {
// we've found an unwrapped paragraph so don't unwrap
doUnwrap = false;
break;
}
if ( ve.isBlockElement( sibling ) ) {
// there is a block element before the next unwrapped node
// so it's safe to unwrap
break;
}
}
break;
}
}
if ( doUnwrap ) {
if ( domElement.childNodes.length ) {
// If domElement has children, append them to parentDomElement
while ( domElement.firstChild ) {
parentDomElement.insertBefore(
domElement.firstChild,
domElement
);
}
} else {
// If domElement has no children, it's as if it was never there at all,
// so set lastOuterPost back to what it was, except that we need to
// change undefined to '' , since undefined means there were no children.
parentDomElement.lastOuterPost = oldLastOuterPost || '';
}
parentDomElement.removeChild( domElement );
}
delete domElement.veInternal;
delete domElement.lastOuterPost;
// Ascend to parent node, except if this is an internal node
// TODO: It's not covered with unit tests.
if ( !ve.dm.nodeFactory.lookup( type ) || !ve.dm.nodeFactory.isNodeInternal( type ) ) {
domElement = parentDomElement;
}
} else {
// Create node from data
if ( this.metaItemFactory.lookup( data[ i ].type ) ) {
isContentNode = canContainContentStack[ canContainContentStack.length - 1 ];
} else {
canContainContentStack.push(
// if the last item was true then this item must inherit it
canContainContentStack[ canContainContentStack.length - 1 ] ||
this.nodeFactory.canNodeContainContent( data[ i ].type )
);
isContentNode = this.nodeFactory.isNodeContent( data[ i ].type );
}
dataElementOrSlice = getDataElementOrSlice();
childDomElements = this.getDomElementsFromDataElement( dataElementOrSlice, doc );
if ( childDomElements && !childDomElements.length ) {
// Support toDomElements returning an empty array
i = findEndOfNode( i ) - 1;
continue;
} else if ( childDomElements ) {
// Add clone of internal data; we use a clone rather than a reference because
// we modify .veInternal.whitespace[1] in some cases
childDomElements[ 0 ].veInternal = ve.extendObject(
{ childDomElements: childDomElements },
dataElement.internal ? ve.copy( dataElement.internal ) : {}
);
// Add elements
for ( j = 0; j < childDomElements.length; j++ ) {
domElement.appendChild( childDomElements[ j ] );
}
// Descend into the first child node
parentDomElement = domElement;
domElement = childDomElements[ 0 ];
// Process outer whitespace
// Every piece of outer whitespace is duplicated somewhere:
// each node's outerPost is duplicated as the next node's
// outerPre, the first node's outerPre is the parent's
// innerPre, and the last node's outerPost is the parent's
// innerPost. For each piece of whitespace, we verify that
// the duplicate matches. If it doesn't, we take that to
// mean the user has messed with it and don't output any
// whitespace.
if ( domElement.veInternal && domElement.veInternal.whitespace ) {
// Process this node's outerPre
ours = domElement.veInternal.whitespace[ 0 ];
theirs = undefined;
if ( domElement.previousSibling ) {
// Get previous sibling's outerPost
theirs = parentDomElement.lastOuterPost;
} else if ( parentDomElement === container ) {
// outerPre of the very first node in the document, check against body innerWhitespace
theirs = innerWhitespace ? innerWhitespace[ 0 ] : ours;
} else {
// First child, get parent's innerPre
if (
parentDomElement.veInternal &&
parentDomElement.veInternal.whitespace
) {
theirs = parentDomElement.veInternal.whitespace[ 1 ];
// Clear parent's innerPre so it's not used again
parentDomElement.veInternal.whitespace[ 1 ] = undefined;
}
// else theirs=undefined
}
if ( ours && ours === theirs ) {
// Matches the duplicate, insert a TextNode
textNode = doc.createTextNode( ours );
textNode.veIsWhitespace = true;
parentDomElement.insertBefore(
textNode,
domElement
);
}
} else if (
!isContentNode &&
!domElement.previousSibling &&
parentDomElement.veInternal &&
parentDomElement.veInternal.whitespace
) {
// The parent's innerPre should not be used, because it doesn't match
// outerPre (since we didn't have any whitespace set at all).
// Except if this is a content node, because content nodes
// don't have whitespace annotated on them *sigh*
parentDomElement.veInternal.whitespace[ 1 ] = undefined;
}
}
if ( Array.isArray( dataElementOrSlice ) ) {
i += dataElementOrSlice.length - 2;
} else if ( childDomElements && childDomElements.length && childDomElements[ 0 ].handledOwnChildren ) {
i = findEndOfNode( i ) - 2;
}
}
}
}
// Check outerPost whitespace of the very last node against body innerWhitespace
if (
container.lastOuterPost !== undefined &&
( !innerWhitespace || container.lastOuterPost === innerWhitespace[ 1 ] )
) {
if ( container.lastChild && container.lastChild.nodeType === Node.TEXT_NODE ) {
// Last child is a TextNode, append to it
container.lastChild.appendData( container.lastOuterPost );
} else if ( container.lastOuterPost.length > 0 ) {
// Append a TextNode
container.appendChild( doc.createTextNode( container.lastOuterPost ) );
}
delete container.lastOuterPost;
}
// Get rid of excess text nodes
ve.normalizeNode( container );
};
/* Initialization */
ve.dm.converter = new ve.dm.Converter( ve.dm.modelRegistry, ve.dm.nodeFactory, ve.dm.annotationFactory, ve.dm.metaItemFactory );