/** * 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 {Integer} [length] Length of content data in document * @param {Object} [attributes] Reference to map of attribute key/value pairs */ ve.dm.Node = function ( type, length, attributes ) { // Inheritance ve.Node.call( this, type ); // Properties this.length = length || 0; this.attributes = attributes || {}; this.fringeWhitespace = {}; this.doc = undefined; }; /* 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 ); }; /** * Gets the inner length. * * @method * @returns {Integer} 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 {Integer} 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 {Integer} 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 {Integer} 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 {Integer} Offset of node */ ve.dm.Node.prototype.getOffset = function () { return this.root === this ? 0 : this.root.getOffsetFromNode( this ); }; /** * Gets an element attribute value. * * @method * @returns {Mixed} Value of attribute, or undefined if no such attribute exists */ ve.dm.Node.prototype.getAttribute = function ( key ) { return this.attributes[key]; }; /** * Gets a reference to this node's attributes object * * @method * @returns {Object} Attributes object (by reference) */ ve.dm.Node.prototype.getAttributes = function () { return this.attributes; }; /** * Get a clone of the linear model element for this node. The attributes object is deep-copied. * * @returns {Object} Element object with 'type' and (optionally) 'attributes' fields */ ve.dm.Node.prototype.getClonedElement = function () { var retval = { 'type': this.type }; if ( !ve.isEmptyObject( this.attributes ) ) { retval.attributes = ve.copyObject( this.attributes ); } if ( !ve.isEmptyObject( this.fringeWhitespace ) ) { retval.fringeWhitespace = ve.copyObject( this.fringeWhitespace ); } return retval; }; /** * 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; }; /** * Store whitespace that was stripped from the fringes of this node, so it can be restored when * converting back to HTML. * * This function can be passed an object with multiple position-value pairs: * setFringeWhitespace( { 'innerPre': ' ', 'outerPost': '\n' } ) is equivalent to: * setFringeWhitespace( 'innerPre', ' ' ); setFringeWhitespace( 'outerPost', '\n' ); * * @param {String} position Position where the whitespace occurred: * 'innerPre', 'innerPost', 'outerPre' or 'outerPost' * @param {String} value The whitespace */ ve.dm.Node.prototype.setFringeWhitespace = function ( position, value ) { var k; if ( typeof position === 'object' ) { for ( k in position ) { this.fringeWhitespace[k] = position[k]; } } else { this.fringeWhitespace[position] = value; } }; /* Inheritance */ ve.extendClass( ve.dm.Node, ve.Node );