mediawiki-extensions-Visual.../modules/ve/dm/ve.dm.Node.js

603 lines
18 KiB
JavaScript

/*!
* 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 {string} type Symbolic name of node type
* @param {number} [length] Length of content data in document
* @param {Object} [element] Reference to element in linear model
*/
ve.dm.Node = function VeDmNode( type, length, element ) {
// Parent constructor
ve.Node.call( this, type );
// 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 );
/* Static members */
/**
* Symbolic name for the node class. Must be set to a unique string by every subclass. Must not
* conflict with other node names or other annotation names.
* @static
* @property {string} [static.name=null]
* @inheritable
*/
ve.dm.Node.static.name = null;
/**
* 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;
/**
* 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;
};
/**
* 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 () {
return ve.copyObject( this.element );
};
/**
* 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;
};