mediawiki-extensions-Visual.../modules/ve/dm/ve.dm.Node.js
Catrope ce16b275c9 Fix rebase artefact in ve.dm.Node.getAttributes()
This caused it to always return {} when asked for all attributes with a
given prefix

Change-Id: Id3cc053e1911aebb2b7e60e3dd2f325ec7772a6c
2012-11-28 11:20:54 -08:00

354 lines
8.8 KiB
JavaScript

/**
* VisualEditor data model Node class.
*
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Generic DataModel node.
*
* @class
* @abstract
* @constructor
* @extends {ve.Node}
* @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;
};
/* Inheritance */
ve.inheritClass( ve.dm.Node, ve.Node );
/* Methods */
/**
* Gets a list of 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 ve.dm.nodeFactory.getChildNodeTypes( this.type );
};
/**
* Gets a list of 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 ve.dm.nodeFactory.getParentNodeTypes( this.type );
};
/**
* Checks if this node can have child nodes.
*
* @method
* @returns {Boolean} Node can have children
*/
ve.dm.Node.prototype.canHaveChildren = function () {
return ve.dm.nodeFactory.canNodeHaveChildren( this.type );
};
/**
* Checks if this node can have child nodes which can also have child nodes.
*
* @method
* @returns {Boolean} Node can have grandchildren
*/
ve.dm.Node.prototype.canHaveGrandchildren = function () {
return ve.dm.nodeFactory.canNodeHaveGrandchildren( this.type );
};
/**
* Checks if this node represents a wrapped element in the linear model.
*
* @method
* @returns {Boolean} Node represents a wrapped element
*/
ve.dm.Node.prototype.isWrapped = function () {
return ve.dm.nodeFactory.isNodeWrapped( this.type );
};
/**
* Checks if this node can contain content.
*
* @method
* @returns {Boolean} Node can contain content
*/
ve.dm.Node.prototype.canContainContent = function () {
return ve.dm.nodeFactory.canNodeContainContent( this.type );
};
/**
* Checks if this node is content.
*
* @method
* @returns {Boolean} Node is content
*/
ve.dm.Node.prototype.isContent = function () {
return ve.dm.nodeFactory.isNodeContent( this.type );
};
/**
* Checks if this 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 ve.dm.nodeFactory.doesNodeHaveSignificantWhitespace( this.type );
};
/**
* Checks if this node has an ancestor with given type and attributes.
*
* @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;
};
/**
* Gets the inner length.
*
* @method
* @returns {Number} Length of the node's contents
*/
ve.dm.Node.prototype.getLength = function () {
return this.length;
};
/**
* Gets the outer length, including any opening/closing elements.
*
* @method
* @returns {Number} Length of the entire node
*/
ve.dm.Node.prototype.getOuterLength = function () {
return this.length + ( this.isWrapped() ? 2 : 0 );
};
/**
* Gets 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 );
};
/**
* Gets 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() );
};
/**
* Sets the inner length.
*
* @method
* @param {Number} length Length of content
* @throws Invalid content length error if length is less than 0
* @emits lengthChange (diff)
* @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.
*
* @method
* @param {Number} adjustment Amount to adjust length by
* @throws 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 );
};
/**
* Gets the offset of this node within the document.
*
* If this 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 );
};
/**
* Gets an element attribute value.
*
* 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;
};
/**
* Gets a copy of this node's 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 );
};
/**
* Checks if this 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 linear model element for this node. The attributes object is deep-copied.
*
* @returns {Object} Cloned element object
*/
ve.dm.Node.prototype.getClonedElement = function () {
return ve.copyObject( this.element );
};
/**
* Checks if this node can be merged with another.
*
* For two nodes to be mergeable, this node and the given node 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;
};