Файловый менеджер - Редактировать - /var/www/html/mediawiki-1.43.1/extensions/VisualEditor/lib/ve/src/dm/ve.dm.Converter.js
Ðазад
/*! * VisualEditor DataModel Converter class. * * @copyright See AUTHORS.txt */ /** * 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 */ ve.dm.Converter = function VeDmConverter( modelRegistry, nodeFactory, annotationFactory ) { // Properties this.modelRegistry = modelRegistry; this.nodeFactory = nodeFactory; this.annotationFactory = annotationFactory; this.doc = null; this.documentData = null; this.store = null; this.internalList = null; this.mode = null; this.fromClipboard = null; this.contextStack = null; // Whitespace regexes const whitespaceList = this.constructor.static.whitespaceList; this.leadingWhitespaceRegex = new RegExp( '^[' + whitespaceList + ']' ); this.leadingWhitespacesRegex = new RegExp( '^[' + whitespaceList + ']+' ); this.trailingWhitespaceRegex = new RegExp( '[' + whitespaceList + ']$' ); this.trailingWhitespacesRegex = new RegExp( '[' + whitespaceList + ']+$' ); this.onlyWhitespaceRegex = new RegExp( '^[' + whitespaceList + ']+$' ); this.trimWhitespaceRegex = new RegExp( '^([' + whitespaceList + ']*)([\\s\\S]*?)([' + whitespaceList + ']*)$' ); }; /* 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' ]; /** * Pattern matching 'white space characters' as defined by the HTML spec only. * * All other whitespace should be treated as text, e.g. non-breaking spaces. * * See https://www.w3.org/TR/html4/struct/text.html#h-9.1 * * @type {RegExp} */ ve.dm.Converter.static.whitespaceList = ' \\t\\f\\u200b\\r\\n'; ve.dm.Converter.static.PARSER_MODE = 0; ve.dm.Converter.static.CLIPBOARD_MODE = 1; ve.dm.Converter.static.PREVIEW_MODE = 2; /* 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 ) { const characters = text.split( '' ); if ( !annotations || annotations.isEmpty() ) { return characters; } // Apply annotations to characters for ( let i = 0, len = characters.length; i < len; i++ ) { // Just store the annotations' hashes from the hash-value store characters[ i ] = [ characters[ i ], annotations.getHashes().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 ) { let hash, i, len; // Close annotations as needed // Go through annotationStack from bottom to top (low to high), // and find the first annotation that's not in annotations. if ( currentSet.getLength() ) { const targetSetOpen = targetSet.clone(); let startClosingAt; for ( i = 0, len = currentSet.getLength(); i < len; i++ ) { hash = currentSet.getHash( i ); // containsComparableForSerialization is expensive, // so do a simple contains check first if ( targetSetOpen.containsHash( hash ) || targetSetOpen.containsComparableForSerialization( currentSet.get( i ) ) ) { targetSetOpen.removeHash( hash ); } 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 ); } } } if ( targetSet.getLength() ) { const currentSetOpen = currentSet.clone(); // Open annotations as needed for ( i = 0, len = targetSet.getLength(); i < len; i++ ) { hash = targetSet.getHash( i ); // containsComparableForSerialization is expensive, // so do a simple contains check first if ( currentSetOpen.containsHash( hash ) || 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.removeHash( hash ); } else { open( targetSet.get( i ) ); // Add to currentClone currentSet.pushHash( hash ); } } } }; /** * 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 ) { if ( filter === undefined ) { filter = true; } if ( filter === false ) { return; } for ( let i = 0, ilen = originalDomElements.length; i < ilen; i++ ) { if ( !targetDomElements[ i ] ) { continue; } const attrs = originalDomElements[ i ].attributes; if ( !attrs ) { continue; } for ( let j = 0, jlen = attrs.length; j < jlen; j++ ) { if ( targetDomElements[ i ].nodeType === Node.ELEMENT_NODE && !targetDomElements[ i ].hasAttribute( attrs[ j ].name ) && ( filter === true || filter( attrs[ j ].name ) ) ) { let value; if ( computed && this.computedAttributes.indexOf( attrs[ j ].name ) !== -1 ) { value = originalDomElements[ i ][ attrs[ j ].name ]; } else { value = attrs[ j ].value; } targetDomElements[ i ].setAttribute( attrs[ j ].name, value ); } } // Descend into element children only (skipping text nodes, comment nodes and nodes we just created) if ( deep && !targetDomElements[ i ].veFromDataElement && originalDomElements[ i ].children.length > 0 ) { this.renderHtmlAttributeList( originalDomElements[ i ].children, targetDomElements[ i ].children, filter, computed, true ); } } }; /** * Modify linear model data in-place to move inline meta items out of content context * * All branch node start items must have item.internal.metaItems = [] * All inline meta items must have item.internal.isInlineMeta set to true * * After the method completes, each inline meta item will be moved downward to the nearest legal * block position (i.e. just after the close meta parent item), and has these properties: * item.internal.loadMetaParentHash - corresponding meta parent's item.originalDomElementsHash * item.internal.loadMetaParentOffset - offset at load time within the meta parent (0 for start). * Each meta item is appended to the corresponding meta parent's item.internal.metaItems . * * @param {Array} data Linear model data to modify in place */ ve.dm.Converter.static.moveInlineMetaItems = function ( data ) { const ancestors = [], pendingMetaItems = []; function closestMetaParent() { for ( let n = ancestors.length - 1; n >= 0; n-- ) { const ancestor = ancestors[ n ]; if ( ancestor.isMetaParent ) { return ancestor; } } return null; } let metaParent; for ( let i = 0; i < data.length; i++ ) { let item = data[ i ]; if ( Array.isArray( item ) ) { // Ignore annotations item = item[ 0 ]; } if ( !item.type ) { // Item is not a node continue; } if ( item.type[ 0 ] !== '/' ) { // Item is a node start if ( ve.getProp( item, 'internal', 'isInlineMeta' ) ) { // This is an inline meta item: move it delete item.internal.isInlineMeta; metaParent = closestMetaParent(); if ( metaParent ) { metaParent.item.internal.metaItems.push( item ); pendingMetaItems.push( { item: item, closeItem: data[ i + 1 ], metaParent: metaParent, offset: i - metaParent.offset - 1 } ); // Remove this item and the immediately following close item data.splice( i, 2 ); // Prepare to rescan this index i--; } else { // Inline meta outside meta parent. This can happen if, say, // the document starts with a comment then a meta item. // Skip this item and the immediately following close item i++; } } else { ancestors.push( { item: item, offset: i, isMetaParent: !!ve.getProp( item, 'internal', 'metaItems' ) } ); } } else { // Item is a node end metaParent = ancestors.pop(); if ( metaParent.isMetaParent ) { for ( let j = 0; j < pendingMetaItems.length; j++ ) { const pending = pendingMetaItems[ j ]; if ( pending.metaParent.item !== metaParent.item ) { continue; } pending.item.internal.loadMetaParentHash = metaParent.item.originalDomElementsHash; pending.item.internal.loadMetaParentOffset = pending.offset; pendingMetaItems.splice( j, 1 ); j--; // This will drop annotations on meta items; fine data.splice( i + 1, 0, pending.item, pending.closeItem ); i += 2; } } } } }; /* Methods */ /** * Check whether this converter instance is currently inside a getModelFromDom() conversion. * * @return {boolean} Whether we're converting */ ve.dm.Converter.prototype.isConverting = function () { return this.contextStack !== null; }; /** * Get the HashValueStore used for the current conversion. * * @return {ve.dm.HashValueStore|null} Current store, or null if not converting */ ve.dm.Converter.prototype.getStore = function () { return this.store; }; /** * Get the HTML document currently being converted * * @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 * * @return {HTMLDocument|null} HTML document being converted for, or null if not converting */ ve.dm.Converter.prototype.getTargetHtmlDocument = function () { return this.targetDoc; }; /** * Get the converter mode, one of PARSER_MODE, CLIPBOARD_MODE or PREVIEW_MODE * * @return {number} Converter mode */ ve.dm.Converter.prototype.getMode = function () { return this.mode; }; /** * Checks if the current mode needs a full view rendering in the HTML * * @return {boolean} Mode needs a rendering */ ve.dm.Converter.prototype.doesModeNeedRendering = function () { return this.getMode() !== this.constructor.static.PARSER_MODE; }; /** * Is the current conversion for the parser * * @return {boolean} The conversion is for the paser */ ve.dm.Converter.prototype.isForParser = function () { return this.getMode() === this.constructor.static.PARSER_MODE; }; /** * Is the current conversion for the clipboard * * @return {boolean} The conversion is for the clipboard */ ve.dm.Converter.prototype.isForClipboard = function () { return this.getMode() === this.constructor.static.CLIPBOARD_MODE; }; /** * Is the current conversion for the preview * * @return {boolean} The conversion is for the preview */ ve.dm.Converter.prototype.isForPreview = function () { return this.getMode() === this.constructor.static.PREVIEW_MODE; }; /** * Is the current conversion from the clipboard * * @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(). * * @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. * * @return {ve.dm.AnnotationSet|null} Annotation set, or null if not converting */ ve.dm.Converter.prototype.getActiveAnnotations = function () { const 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. * * @return {boolean|null} Boolean indicating whether content is expected, or null if not converting */ ve.dm.Converter.prototype.isExpectingContent = function () { const context = this.getCurrentContext(); return context ? context.expectingContent : null; }; /** * Whether the converter can currently accept a child node with the given type. * * @param {string} nodeType * @return {boolean|null} Whether the node type is valid, or null if not converting */ ve.dm.Converter.prototype.isValidChildNodeType = function ( nodeType ) { const context = this.getCurrentContext(); if ( !context ) { return null; } const 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. * * @return {boolean|null} Boolean indicating whether we're wrapping, or null if not converting */ ve.dm.Converter.prototype.isInWrapper = function () { const 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. * * @return {boolean|null} Boolean indicating whether the wrapper can be closed, or null if not converting */ ve.dm.Converter.prototype.canCloseWrapper = function () { const 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. * * @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 ) { const 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; } const 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 ); } const originalDomElements = this.store.value( dataElement.originalDomElementsHash ); // Optimization: don't call renderHtmlAttributeList if returned domElements are equal to the originals if ( originalDomElements && !ve.isEqualDomElements( domElements, originalDomElements ) ) { this.constructor.static.renderHtmlAttributeList( originalDomElements, domElements, nodeClass.static.preserveHtmlAttributes, // computed false, // deep ( !this.nodeFactory.lookup( dataElement.type ) || !this.nodeFactory.canNodeHaveChildren( dataElement.type ) || this.nodeFactory.doesNodeHandleOwnChildren( dataElement.type ) ) ); } // TODO: This is only for the diff. Eventually should make a DiffConverter subclass if ( dataElement.internal && dataElement.internal.diff ) { Array.prototype.forEach.call( domElements, ( domElement ) => { for ( const key in dataElement.internal.diff ) { // toDomElements is a misnomer, it can actually return other nodes, // such as comment nodes or text nodes. if ( domElement.setAttribute ) { domElement.setAttribute( key, dataElement.internal.diff[ key ] ); } } } ); } // Mark branch nodes as generated from dataElement, so we don't try and descend into them in a deep renderHtmlAttributeList call if ( this.nodeFactory.lookup( dataElement.type ) && this.nodeFactory.canNodeHaveChildren( dataElement.type ) ) { const hasSignificantWhitespace = this.nodeFactory.doesNodeHaveSignificantWhitespace( dataElement.type ); domElements.forEach( ( domElement ) => { domElement.veFromDataElement = true; if ( hasSignificantWhitespace ) { domElement.veHasSignificantWhitespace = true; } } ); } 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 ) { let dataElements = modelClass.static.toDataElement( domElements, this ); if ( !dataElements ) { return null; } if ( !Array.isArray( dataElements ) ) { dataElements = [ dataElements ]; } if ( dataElements.length ) { let serializer; if ( modelClass.prototype instanceof ve.dm.Annotation ) { serializer = function ( node ) { // Do not include childNodes; see T160839 return node.cloneNode( false ).outerHTML; }; } else { serializer = ve.getNodeHtml; } dataElements[ 0 ].originalDomElementsHash = this.store.hash( domElements, domElements.map( serializer ).join( '' ) ); if ( modelClass.prototype instanceof ve.dm.BranchNode && modelClass.static.childNodeTypes === null ) { // Set this item up as a meta parent ve.setProp( dataElements[ 0 ], 'internal', 'metaItems', [] ); ve.setProp( dataElements[ 0 ], 'internal', 'changesSinceLoad', 0 ); } } return dataElements; }; /** * Build an HTML DOM node for a linear model annotation. * * @param {Object} dataAnnotation * @param {HTMLDocument} doc HTML document to create element with * @return {HTMLElement} HTML DOM node */ ve.dm.Converter.prototype.getDomElementFromDataAnnotation = function ( dataAnnotation, doc ) { const 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) * @param {ve.dm.HashValueStore} [store] Hash value store * @return {ve.dm.Document} Document model */ ve.dm.Converter.prototype.getModelFromDom = function ( doc, options, store ) { const tmpDoc = new ve.dm.Document(); const internalList = new ve.dm.InternalList( tmpDoc ); store = store || new ve.dm.HashValueStore(); 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 const data = this.getDataFromDomSubtree( doc.body ); this.constructor.static.moveInlineMetaItems( data ); const linearData = new ve.dm.ElementLinearData( store, data ); const refData = this.internalList.convertToData( this, doc ); linearData.batchSplice( linearData.getLength(), 0, refData ); const 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, null, null, tmpDoc.getStorage() ); }; /** * 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. * * @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 ) { const contextStack = this.contextStack; this.contextStack = []; const 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. * * @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 ) { const modelRegistry = this.modelRegistry, data = [], context = {}, prevContext = this.contextStack.length ? this.contextStack[ this.contextStack.length - 1 ] : null; let wrappingParagraph, nextWhitespace = '', wrappedWhitespace = '', wrappedWhitespaceIndex, wrappedMetaItems = []; /** * 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 */ const 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; }; const 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 const outputWrappedMetaItems = ( whitespaceTreatment ) => { const toInsert = []; let prev = wrappingParagraph; for ( let j = 0, len = wrappedMetaItems.length; j < len; j++ ) { if ( wrappedMetaItems[ j ].type && wrappedMetaItems[ j ].type.charAt( 0 ) !== '/' ) { if ( wrappedMetaItems[ j ].internal && wrappedMetaItems[ j ].internal.whitespace ) { if ( whitespaceTreatment === 'restore' ) { ve.batchPush( toInsert, this.constructor.static.getDataContentFromText( wrappedMetaItems[ j ].internal.whitespace[ 0 ], context.annotations ) ); delete wrappedMetaItems[ j ].internal; } else if ( whitespaceTreatment === 'fixup' ) { addWhitespace( prev, 3, wrappedMetaItems[ j ].internal.whitespace[ 0 ] ); } } prev = wrappedMetaItems[ j ]; } toInsert.push( wrappedMetaItems[ j ] ); } 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 { ve.batchPush( data, toInsert ); } wrappedMetaItems = []; }; const 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', metaItems: [] } }; data.push( wrappingParagraph ); context.inWrapper = true; context.canCloseWrapper = true; context.expectingContent = true; processNextWhitespace( wrappingParagraph ); }; const 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; }; const getAboutGroup = ( node ) => { const group = [ node ]; if ( node.nodeType !== Node.ELEMENT_NODE || node.getAttribute( 'about' ) === null ) { return group; } const about = node.getAttribute( 'about' ); while ( ( node = node.nextSibling ) !== null ) { if ( node.nodeType === Node.ELEMENT_NODE && node.getAttribute( 'about' ) === about ) { group.push( node ); } else { break; } } return group; }; const isAllInstanceOf = ( linearData, targetClass ) => { for ( let j = linearData.length - 1; j >= 0; j-- ) { const type = ve.dm.LinearData.static.getType( linearData[ j ] ); if ( type ) { const 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 function setInlineMeta( element ) { ve.setProp( element, 'internal', 'isInlineMeta', true ); } let prevElement; for ( let i = 0; i < domElement.childNodes.length; i++ ) { const childNode = domElement.childNodes[ i ]; switch ( childNode.nodeType ) { case Node.ELEMENT_NODE: case Node.COMMENT_NODE: { if ( childNode.getAttribute && childNode.getAttribute( 'data-ve-ignore' ) ) { continue; } const aboutGroup = getAboutGroup( childNode ); const modelName = this.modelRegistry.matchElement( childNode, aboutGroup.length > 1 ); let modelClass = this.modelRegistry.lookup( modelName ) || ve.dm.AlienNode; let childNodes; if ( modelClass.prototype instanceof ve.dm.Annotation ) { childNodes = [ childNode ]; } else { // Node or meta item childNodes = modelClass.static.enableAboutGrouping ? aboutGroup : [ childNode ]; } let 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; } // If we're about to start wrapping for an annotation, // check paragraphs are actually allowed here. if ( !context.inWrapper && !context.expectingContent && modelClass.prototype instanceof ve.dm.Annotation && !this.isValidChildNodeType( 'paragraph' ) ) { // Alienate (force block mode as we are replacing a wrapper) modelClass = ve.dm.AlienBlockNode; childNodes = modelClass.static.enableAboutGrouping ? aboutGroup : [ childNode ]; childDataElements = this.createDataElements( modelClass, childNodes ); } // Now take the appropriate action based on that if ( modelClass.prototype instanceof ve.dm.Annotation ) { const annotation = this.annotationFactory.createFromElement( childDataElements[ 0 ], this.store ); // Start wrapping if needed if ( !context.inWrapper && !context.expectingContent ) { startWrapping(); prevElement = wrappingParagraph; } // Append child element data const 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 if ( !childDataElements.length || isAllInstanceOf( childDataElements, ve.dm.RemovableAlienMetaItem ) ) { childDataElements = this.createDataElements( ve.dm.RemovableAlienMetaItem, childNodes ); } else { 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.getHashes().slice(); } // Mark meta items to be moved outside of content context, as we can't handle them here // (context.expectingContent is always true at this point) setInlineMeta( childDataElements[ 0 ] ); } outputWrappedMetaItems( 'restore' ); ve.batchPush( data, childDataElements ); // Clear wrapped whitespace wrappedWhitespace = ''; } else { // Node or meta item if ( modelClass.prototype instanceof ve.dm.MetaItem ) { if ( context.expectingContent ) { // Mark meta items to be moved outside of content context, as we can't handle them here childDataElements.forEach( setInlineMeta ); } // 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.getHashes().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 ) { ve.batchPush( wrappedMetaItems, childDataElements ); if ( wrappedWhitespace !== '' ) { data.splice( wrappedWhitespaceIndex, wrappedWhitespace.length ); addWhitespace( childDataElements[ 0 ], 0, wrappedWhitespace ); nextWhitespace = wrappedWhitespace; wrappedWhitespace = ''; } } else { outputWrappedMetaItems( 'restore' ); ve.batchPush( data, childDataElements ); processNextWhitespace( childDataElements[ 0 ] ); prevElement = childDataElements[ 0 ]; } // In case we consumed multiple childNodes, adjust i accordingly i += childNodes.length - 1; break; } let childIsContent = this.nodeFactory.canNodeSerializeAsContent( 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.canNodeSerializeAsContent( 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.getHashes().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' ); ve.batchPush( data, 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' ); ve.batchPush( data, 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: { let text = childNode.data; if ( text === '' ) { // Empty text node?!? break; } if ( !context.originallyExpectingContent ) { // Strip and store outer whitespace if ( this.onlyWhitespaceRegex.test( text ) ) { // 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; ve.batchPush( data, this.constructor.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 const matches = text.match( this.trimWhitespaceRegex ); 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 ve.batchPush( data, this.constructor.static.getDataContentFromText( matches[ 1 ], context.annotations ) ); } // Output the text sans whitespace ve.batchPush( data, this.constructor.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; ve.batchPush( data, this.constructor.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 const matches = text.match( this.leadingWhitespacesRegex ); 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 const matches = text.match( this.trailingWhitespacesRegex ); if ( matches && matches[ 0 ] !== '' ) { addWhitespace( wrapperElement, 2, matches[ 0 ] ); text = text.slice( 0, text.length - matches[ 0 ].length ); } } // Annotate the text and output it ve.batchPush( data, this.constructor.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' ) ) { const wrapperParagraph = { type: 'paragraph', internal: { generated: 'wrapper' } }; processNextWhitespace( wrapperParagraph ); data.push( wrapperParagraph ); 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 ) { const 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.ElementLinearData} data Linear model data * @return {Array.<string|undefined>} Sparse array of whitespace strings: [ innerLeft, innerRight ] */ ve.dm.Converter.prototype.getInnerWhitespace = function ( data ) { const innerWhitespace = new Array( 2 ); let stack = 0, last = data.getLength() - 1; let whitespace; 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 * * @param {ve.dm.Document} model Document model * @param {number} [mode=PARSER_MODE] Conversion mode, defaults to PARSER_MODE * @return {HTMLDocument} Document containing the resulting HTML */ ve.dm.Converter.prototype.getDomFromModel = function ( model, mode ) { // Backwards compatibility with 'forClipboard' argument if ( typeof mode === 'boolean' ) { mode = mode ? this.constructor.static.CLIPBOARD_MODE : this.constructor.static.PARSER_MODE; } mode = mode || this.constructor.static.PARSER_MODE; const doc = ve.createDocumentFromHtml( '' ); this.getDomSubtreeFromModel( model, doc.body, mode ); return doc; }; /** * Convert model node to an HTML DOM * * @param {ve.dm.Node} node Model node * @param {number} [mode=PARSER_MODE] Conversion mode, defaults to PARSER_MODE * @return {HTMLDocument} Document containing the resulting HTML */ ve.dm.Converter.prototype.getDomFromNode = function ( node, mode ) { // Backwards compatibility with 'forClipboard' argument if ( typeof mode === 'boolean' ) { mode = mode ? this.constructor.static.CLIPBOARD_MODE : this.constructor.static.PARSER_MODE; } mode = mode || this.constructor.static.PARSER_MODE; return this.getDomFromModel( node.getDocument().shallowCloneFromRange( node.isInternal() ? node.getRange() : node.getOuterRange() ), mode ); }; /** * Convert document model to an HTML DOM subtree and add it to a container element. * * @param {ve.dm.Document} model Document model * @param {HTMLElement} container DOM element to add the generated elements to. Should be empty. * @param {number} [mode=PARSER_MODE] Conversion mode, defaults to PARSER_MODE */ ve.dm.Converter.prototype.getDomSubtreeFromModel = function ( model, container, mode ) { if ( typeof mode === 'boolean' ) { mode = mode ? this.constructor.static.CLIPBOARD_MODE : this.constructor.static.PARSER_MODE; } mode = mode || this.constructor.static.PARSER_MODE; // Set up the converter state this.documentData = model.getFullData( undefined, 'roundTrip' ); this.store = model.getStore(); this.internalList = model.getInternalList(); // Internal list of the doc this was cloned from, or itself if not cloned this.originalDocInternalList = model.getOriginalDocument() ? model.getOriginalDocument().getInternalList() : this.internalList; this.mode = mode; this.getDomSubtreeFromData( this.documentData, container, model.getInnerWhitespace() ); // Clear the state this.documentData = null; this.store = null; this.internalList = null; this.originalDocInternalList = null; this.mode = 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.<string|undefined>} [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 ) { const whitespaceHtmlChars = ve.visibleWhitespaceCharacters, isForPreview = this.isForPreview(), dataLen = data.length, doc = container.ownerDocument; let text, annotatedDomElements, annotatedDomElementStack, domElement = container; // 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. const openAnnotation = () => { // Add text if needed if ( text.length > 0 ) { annotatedDomElements.push( doc.createTextNode( text ) ); text = ''; } annotatedDomElements = []; annotatedDomElementStack.push( annotatedDomElements ); }; const closeAnnotation = ( annotation ) => { const originalDomElements = annotation.getOriginalDomElements( this.store ), origElementText = originalDomElements[ 0 ] && originalDomElements[ 0 ].textContent || ''; let leading = '', trailing = ''; // Add text if needed if ( text.length > 0 ) { annotatedDomElements.push( doc.createTextNode( text ) ); text = ''; } const 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 if ( annotation.constructor.static.trimWhitespace ) { let matches; let first = annotatedChildDomElements[ 0 ]; while ( first && first.nodeType === Node.TEXT_NODE && ( matches = first.data.match( this.leadingWhitespacesRegex ) ) && !this.leadingWhitespaceRegex.test( origElementText ) ) { 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 ]; } let last = annotatedChildDomElements[ annotatedChildDomElements.length - 1 ]; while ( last && last.nodeType === Node.TEXT_NODE && ( matches = last.data.match( this.trailingWhitespacesRegex ) ) && !this.trailingWhitespaceRegex.test( origElementText ) ) { 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 ]; } } let annotationElement; if ( annotatedChildDomElements.length ) { annotationElement = this.getDomElementsFromDataElement( annotation.getElement(), doc, annotatedChildDomElements )[ 0 ]; } if ( leading ) { annotatedDomElements.push( doc.createTextNode( leading ) ); } let n, len; if ( annotationElement ) { for ( n = 0, len = annotatedChildDomElements.length; n < len; n++ ) { annotationElement.appendChild( annotatedChildDomElements[ n ] ); } annotatedDomElements.push( annotationElement ); } else { for ( n = 0, len = annotatedChildDomElements.length; n < len; n++ ) { annotatedDomElements.push( annotatedChildDomElements[ n ] ); } } if ( trailing ) { annotatedDomElements.push( doc.createTextNode( trailing ) ); } }; const findEndOfNode = ( k ) => { let n, depth; for ( n = k + 1, depth = 1; n < dataLen && depth > 0; n++ ) { if ( data[ n ].type ) { depth += data[ n ].type.charAt( 0 ) === '/' ? -1 : 1; } } if ( depth !== 0 ) { throw new Error( 'Unbalanced data: ' + depth + ' element(s) left open.' ); } return n; }; const getDataElementOrSlice = ( i ) => { let 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; }; const getChar = ( char ) => { if ( isForPreview && !domElement.veHasSignificantWhitespace && Object.prototype.hasOwnProperty.call( whitespaceHtmlChars, char ) ) { char = whitespaceHtmlChars[ char ]; } return char; }; const annotationStack = new ve.dm.AnnotationSet( this.store ); for ( let i = 0; i < dataLen; i++ ) { if ( typeof data[ i ] === 'string' ) { // Text text = ''; let 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 && this.onlyWhitespaceRegex.test( data[ i ] ) && this.isForParser() ) ) { text += getChar( 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.nodeFactory.canNodeSerializeAsContent( 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.nodeFactory.canNodeSerializeAsContent( data[ i ].type ) ) ) ) { const annotations = new ve.dm.AnnotationSet( this.store, data[ i ].annotations || data[ i ][ 1 ] ); this.constructor.static.openAndCloseAnnotations( annotationStack, annotations, openAnnotation, closeAnnotation ); if ( data[ i ].annotations === undefined ) { // Annotated text text += getChar( data[ i ][ 0 ] ); } else { // Annotated node // Add text if needed if ( text.length > 0 ) { annotatedDomElements.push( doc.createTextNode( text ) ); text = ''; } // Insert the elements const dataElementOrSlice = getDataElementOrSlice( i ); const childDomElements = this.getDomElementsFromDataElement( dataElementOrSlice, doc ); for ( let 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 this.constructor.static.openAndCloseAnnotations( annotationStack, new ve.dm.AnnotationSet( this.store ), openAnnotation, closeAnnotation ); // Put the annotated nodes in the DOM for ( let j = 0; j < annotatedDomElements.length; j++ ) { domElement.appendChild( annotatedDomElements[ j ] ); } } else if ( data[ i ].type !== undefined ) { const dataElement = data[ i ]; // Element if ( dataElement.type.charAt( 0 ) === '/' ) { // Close element const parentDomElement = domElement.parentNode; const type = data[ i ].type.slice( 1 ); const isContentNode = this.nodeFactory.isNodeContent( type ); // Process whitespace // whitespace = [ outerPre, innerPre, innerPost, outerPost ] const 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. const 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 const textNode = doc.createTextNode( pre ); textNode.veIsWhitespace = true; domElement.insertBefore( textNode, domElement.firstChild ); } } const lastChild = domElement.veInternal.childDomElements ? domElement.veInternal .childDomElements[ domElement.veInternal.childDomElements.length - 1 ] .lastChild : domElement.lastChild; const ours = domElement.veInternal.whitespace[ 2 ]; let theirs; 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 const 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. let 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) data[ i + 1 ] === undefined || data[ i + 1 ].type.charAt( 0 ) === '/' || // Document ends when we encounter the internal list ( data[ i + 1 ].type && this.nodeFactory.isNodeInternal( data[ i + 1 ].type ) ) ) ) { 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; const previousSiblings = domElement.parentNode.childNodes; // Note: previousSiblings includes the current element // so we only go up to length - 2 for ( let j = previousSiblings.length - 2; j >= 0; j-- ) { const sibling = previousSiblings[ j ]; if ( ve.isBlockElement( sibling ) ) { // Stop searching early when we get to a block element. break; } // If we find content, don't unwrap. if ( // Text node content (non-whitespace) ( sibling.nodeType === Node.TEXT_NODE && !sibling.veIsWhitespace ) || // Inline content tag ( sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName !== 'META' && sibling.tagName !== 'LINK' ) ) { // we've found unwrapped content so don't unwrap doUnwrap = false; 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.nodeFactory.isNodeInternal( data[ i ].type ) ) { // Reached the internal list, finish break; } const isContentNode = this.nodeFactory.isNodeContent( data[ i ].type ); const dataElementOrSlice = getDataElementOrSlice( i ); const 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 ( let j = 0; j < childDomElements.length; j++ ) { domElement.appendChild( childDomElements[ j ] ); } // Descend into the first child node const 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 const ours = domElement.veInternal.whitespace[ 0 ]; let theirs; 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 const 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 container.normalize(); }; /* Initialization */ ve.dm.converter = new ve.dm.Converter( ve.dm.modelRegistry, ve.dm.nodeFactory, ve.dm.annotationFactory );
| ver. 1.1 | |
.
| PHP 8.4.18 | Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ñтраницы: 0 |
proxy
|
phpinfo
|
ÐаÑтройка