/*! * VisualEditor DataModel Node class. * * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt * @license The MIT License (MIT); see LICENSE.txt */ /** * Generic DataModel node. * * @abstract * @extends ve.Node * @constructor * @param {number} [length] Length of content data in document * @param {Object} [element] Reference to element in linear model */ ve.dm.Node = function VeDmNode( length, element ) { // Parent constructor ve.Node.call( this ); // Properties this.length = length || 0; this.element = element; this.doc = undefined; }; /** * @event lengthChange * @param diff */ /** * @event update */ /* Inheritance */ ve.inheritClass( ve.dm.Node, ve.Node ); /** * Array of HTML tag names that this node should be a match candidate for. * Empty array means none, null means any. * For more information about element matching, see ve.dm.ModelRegistry. * @static * @property {string[]} static.matchTagNames * @inheritable */ ve.dm.Node.static.matchTagNames = null; /** * Array of RDFa types that this node should be a match candidate for. * Empty array means none, null means any. * For more information about element matching, see ve.dm.ModelRegistry. * @static * @property {Array} static.matchRdfaType Array of strings or regular expressions * @inheritable */ ve.dm.Node.static.matchRdfaTypes = null; /** * Optional function to determine whether this node should match a given element. * Takes an HTMLElement and returns true or false. * This function is only called if this node has a chance of "winning"; see * ve.dm.ModelRegistry for more information about element matching. * If set to null, this property is ignored. Setting this to null is not the same as unconditionally * returning true, because the presence or absence of a matchFunction affects the node's * specificity. * * NOTE: This function is NOT a method, within this function "this" will not refer to an instance * of this class (or to anything reasonable, for that matter). * @static * @property {Function} static.matchFunction * @inheritable */ ve.dm.Node.static.matchFunction = null; /** * Static function to convert a DOM element or set of sibling DOM elements to a linear model data * element for this node type. * * This function is only called if this node "won" the matching for the first DOM element, so * domElements[0] will match this node's matching rule. There is usually only one node in * domElements[]. Multiple nodes will only be passed if this node supports about groups. * If there are multiple nodes, the nodes are all adjacent siblings in the same about group * (i.e. they are grouped together because they have the same value for the about attribute). * * This function is allowed to return a content element when context indicates that a non-content * element is expected or vice versa. If that happens, the converter deals with it in the following way: * * - if a non-content element is expected but a content element is returned: * - open a wrapper paragraph * - put the returned element in the wrapper * - if a content element is expected but a non-content element is returned: * - if we are in a wrapper paragraph: * - if we can close the wrapper: * - close the wrapper * - insert the returned element right after the end of the wrapper * - if we can't close the wrapper: * - alienate the element * - if we aren't in a wrapper paragraph: * - alienate the element * * The returned linear model element must have a type property set to a registered node name * (usually the node's .static.name, but that's not required). It may optionally have an attributes * property set to an object with key-value pairs. Any other properties are not allowed. * * @static * @method * @param {HTMLElement[]} domElements DOM elements to convert. Usually only one element * @param {Object} context Object describing the current state of the converter * @param {boolean} context.expectingContent Whether this function is expected to return a content element * @param {boolean} context.inWrapper Whether this element is in a wrapper paragraph generated by the converter; * can only be true if context.expectingContent is also true * @param {boolean} context.canCloseWrapper Whether the current wrapper paragraph can be closed; * can only be true if context.inWrapper is also true * @returns {Object|null} Linear model element, or null to alienate */ ve.dm.Node.static.toDataElement = function ( /*domElements, context*/ ) { throw new Error( 've.dm.Node subclass must implement toDataElement' ); }; /** * Static function to convert a linear model data element for this node type back to one or more * DOM elements. * * NOTE: If this function returns multiple DOM elements, the DOM elements produced by the children * of this node (if any) will be attached to the first DOM element in the array. * * @static * @method * @param {Object} Linear model element with a type property and optionally an attributes property * @returns {HTMLElement[]} DOM elements */ ve.dm.Node.static.toDomElements = function ( /*dataElement*/ ) { throw new Error( 've.dm.Node subclass must implement toDomElements' ); }; /** * Whether this node type represents metadata. * * Linear model elements with this type will be moved out of the linear model into the metadata. * * @static * @property {boolean} static.isMeta * @inheritable */ ve.dm.Node.static.isMeta = false; /** * Whether this node supports about grouping. When a DOM element matches a node type that has * about grouping enabled, the converter will look for adjacent siblings with the same value for * the about attribute, and ask toDataElement() to produce a single data element for all of those * DOM nodes combined. * * The converter doesn't descend into about groups, i.e. it doesn't convert the children of the * DOM elements that make up the about group. This means the resulting linear model element will * be childless. * * @static * @property {boolean} static.enableAboutGrouping * @inheritable */ ve.dm.Node.static.enableAboutGrouping = false; /** * Whether HTML attributes should be preserved for this node type. If true, the HTML attributes * of the DOM elements will be stored as linear model attributes. The attribute names be * html/i/attrName, where i is the index of the DOM element in the domElements array, and attrName * is the name of the attribute. * * This should generally be enabled, except for node types that store their entire HTML in an * attribute. * * @static * @property {boolean} static.storeHtmlAttributes * @inheritable */ ve.dm.Node.static.storeHtmlAttributes = true; /** * Whether this node type has a wrapping element in the linear model. Most node types are wrapped, * only special node types are not wrapped. * * @static * @property {boolean} static.isWrapped * @inheritable */ ve.dm.Node.static.isWrapped = true; /** * Whether this node type is a content node type. This means the node represents content, cannot * have children, and can only appear as children of a content container node. Content nodes are * also known as inline nodes. * * @static * @property {boolean} static.isContent * @inheritable */ ve.dm.Node.static.isContent = false; /** * Whether this node type can contain content. The children of content container nodes must be * content nodes. * * @static * @property {boolean} static.canContainContent * @inheritable */ ve.dm.Node.static.canContainContent = false; /** * Whether this node type has significant whitespace. Only applies to content container nodes * (i.e. can only be true if canContainContent is also true). * * If a content node has significant whitespace, the text inside it is not subject to whitespace * stripping and preservation. * * @static * @property {boolean} static.hasSignificantWhitespace * @inheritable */ ve.dm.Node.static.hasSignificantWhitespace = false; /** * Array of allowed child node types for this node type. * * An empty array means no children are allowed. null means any node type is allowed as a child. * * @static * @property {string[]|null} static.childNodeTypes * @inheritable */ ve.dm.Node.static.childNodeTypes = null; /** * Array of allowed parent node types for this node type. * * An empty array means this node type cannot be the child of any node. null means this node type * can be the child of any node type. * * @static * @property {string[]|null} static.parentNodeTypes * @inheritable */ ve.dm.Node.static.parentNodeTypes = null; /** * Array of suggested parent node types for this node type. * * These parent node types are allowed but the editor will avoid creating them. * * An empty array means this node type should not be the child of any node. null means this node type * can be the child of any node type. * * @static * @property {string[]|null} static.suggestedParentNodeTypes * @inheritable */ ve.dm.Node.static.suggestedParentNodeTypes = null; /** * Default attributes to set for newly created linear model elements. These defaults will be used * when creating a new element in ve.dm.NodeFactory#getDataElement when there is no DOM node or * existing linear model element to base the attributes on. * * This property is an object with attribute names as keys and attribute values as values. * Attributes may be omitted, in which case they'll simply be undefined. * * @static * @property {Object} static.defaultAttributes * @inheritable */ ve.dm.Node.static.defaultAttributes = {}; /* Methods */ /** * Get allowed child node types. * * @method * @returns {string[]|null} List of node types allowed as children or null if any type is allowed */ ve.dm.Node.prototype.getChildNodeTypes = function () { return this.constructor.static.childNodeTypes; }; /** * Get allowed parent node types. * * @method * @returns {string[]|null} List of node types allowed as parents or null if any type is allowed */ ve.dm.Node.prototype.getParentNodeTypes = function () { return this.constructor.static.parentNodeTypes; }; /** * Get suggested parent node types. * * @method * @returns {string[]|null} List of node types suggested as parents or null if any type is suggested */ ve.dm.Node.prototype.getSuggestedParentNodeTypes = function () { return this.constructor.static.suggestedParentNodeTypes; }; /** * Check if the node can have children. * * @method * @returns {boolean} Node can have children */ ve.dm.Node.prototype.canHaveChildren = function () { return ve.dm.nodeFactory.canNodeHaveChildren( this.type ); }; /** * Check if the node can have grandchildren. * * @method * @returns {boolean} Node can have grandchildren */ ve.dm.Node.prototype.canHaveGrandchildren = function () { return ve.dm.nodeFactory.canNodeHaveGrandchildren( this.type ); }; /** * Check if the node has a wrapped element in the document data. * * @method * @returns {boolean} Node represents a wrapped element */ ve.dm.Node.prototype.isWrapped = function () { return this.constructor.static.isWrapped; }; /** * Check if the node can contain content. * * @method * @returns {boolean} Node can contain content */ ve.dm.Node.prototype.canContainContent = function () { return this.constructor.static.canContainContent; }; /** * Check if the node is content. * * @method * @returns {boolean} Node is content */ ve.dm.Node.prototype.isContent = function () { return this.constructor.static.isContent; }; /** * Check if the node has significant whitespace. * * Can only be true if canContainContent is also true. * * @method * @returns {boolean} Node has significant whitespace */ ve.dm.Node.prototype.hasSignificantWhitespace = function () { return this.constructor.static.hasSignificantWhitespace; }; /** * Check if the node has an ancestor with matching type and attribute values. * * @method * @returns {boolean} Node is content */ ve.dm.Node.prototype.hasMatchingAncestor = function ( type, attributes ) { var key, node = this; // Traverse up to matching node while ( node && node.getType() !== type ) { node = node.getParent(); // Stop at root if ( node === null ) { return false; } } // Check attributes if ( attributes ) { for ( key in attributes ) { if ( node.getAttribute( key ) !== attributes[key] ) { return false; } } } return true; }; /** * Get the length of the node. * * @method * @returns {number} Length of the node's contents */ ve.dm.Node.prototype.getLength = function () { return this.length; }; /** * Get the outer length of the node, which includes wrappers if present. * * @method * @returns {number} Length of the entire node */ ve.dm.Node.prototype.getOuterLength = function () { return this.length + ( this.isWrapped() ? 2 : 0 ); }; /** * Get the range inside the node. * * @method * @returns {ve.Range} Inner node range */ ve.dm.Node.prototype.getRange = function () { var offset = this.getOffset(); if ( this.isWrapped() ) { offset++; } return new ve.Range( offset, offset + this.length ); }; /** * Get the range outside the node. * * @method * @returns {ve.Range} Outer node range */ ve.dm.Node.prototype.getOuterRange = function () { var offset = this.getOffset(); return new ve.Range( offset, offset + this.getOuterLength() ); }; /** * Set the inner length of the node. * * This should only be called after a relevant change to the document data. Calling this method will * not change the document data. * * @method * @param {number} length Length of content * @throws {Error} Invalid content length error if length is less than 0 * @emits lengthChange * @emits update */ ve.dm.Node.prototype.setLength = function ( length ) { if ( length < 0 ) { throw new Error( 'Length cannot be negative' ); } // Compute length adjustment from old length var diff = length - this.length; // Set new length this.length = length; // Adjust the parent's length if ( this.parent ) { this.parent.adjustLength( diff ); } // Emit events this.emit( 'lengthChange', diff ); this.emit( 'update' ); }; /** * Adjust the length. * * This should only be called after a relevant change to the document data. Calling this method will * not change the document data. * * @method * @param {number} adjustment Amount to adjust length by * @throws {Error} Invalid adjustment error if resulting length is less than 0 * @emits lengthChange (diff) * @emits update */ ve.dm.Node.prototype.adjustLength = function ( adjustment ) { this.setLength( this.length + adjustment ); }; /** * Get the offset of the node within the document. * * If the node has no parent than the result will always be 0. * * @method * @returns {number} Offset of node */ ve.dm.Node.prototype.getOffset = function () { return this.root === this ? 0 : this.root.getOffsetFromNode( this ); }; /** * Get the value of an attribute. * * Return value is by reference if array or object. * * @method * @returns {Mixed} Value of attribute, or undefined if no such attribute exists */ ve.dm.Node.prototype.getAttribute = function ( key ) { return this.element && this.element.attributes ? this.element.attributes[key] : undefined; }; /** * Get a copy of all attributes. * * Values are by reference if array or object, similar to using the getAttribute method. * * @method * @param {string} prefix Only return attributes with this prefix, and remove the prefix from them * @returns {Object} Attributes */ ve.dm.Node.prototype.getAttributes = function ( prefix ) { var key, filtered, attributes = this.element && this.element.attributes ? this.element.attributes : {}; if ( prefix ) { filtered = {}; for ( key in attributes ) { if ( key.indexOf( prefix ) === 0 ) { filtered[key.substr( prefix.length )] = attributes[key]; } } return filtered; } return ve.extendObject( {}, attributes ); }; /** * Check if the node has certain attributes. * * If an array of keys is provided only the presence of the attributes will be checked. If an object * with keys and values is provided both the presence of the attributes and their values will be * checked. Comparison of values is done by casting to strings unless the strict argument is used. * * @method * @param {string[]|Object} attributes Array of keys or object of keys and values * @param {boolean} strict Use strict comparison when checking if values match * @returns {boolean} Node has attributes */ ve.dm.Node.prototype.hasAttributes = function ( attributes, strict ) { var key, i, len, ourAttributes = this.getAttributes() || {}; if ( ve.isPlainObject( attributes ) ) { // Node must have all the required attributes for ( key in attributes ) { if ( !( key in ourAttributes ) || ( strict ? attributes[key] !== ourAttributes[key] : String( attributes[key] ) !== String( ourAttributes[key] ) ) ) { return false; } } } else if ( ve.isArray( attributes ) ) { for ( i = 0, len = attributes.length; i < len; i++ ) { if ( !( attributes[i] in ourAttributes ) ) { return false; } } } return true; }; /** * Get a clone of the node's document data element. * * The attributes object will be deep-copied. * * @returns {Object} Cloned element object */ ve.dm.Node.prototype.getClonedElement = function () { var clone = ve.copyObject( this.element ); if ( clone.internal ) { delete clone.internal.generated; } return clone; }; /** * Check if the node can be merged with another. * * For two nodes to be mergeable, the two nodes must either be the same node or: * - Have the same type * - Have the same depth * - Have similar ancestory (each node upstream must have the same type) * * @method * @param {ve.dm.Node} node Node to consider merging with * @returns {boolean} Nodes can be merged */ ve.dm.Node.prototype.canBeMergedWith = function ( node ) { var n1 = this, n2 = node; // Move up from n1 and n2 simultaneously until we find a common ancestor while ( n1 !== n2 ) { if ( // Check if we have reached a root (means there's no common ancestor or unequal depth) ( n1 === null || n2 === null ) || // Ensure that types match n1.getType() !== n2.getType() ) { return false; } // Move up n1 = n1.getParent(); n2 = n2.getParent(); } return true; };