mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-29 00:30:44 +00:00
63e6702c52
This makes things like == Foo == * Bar render without the leading and trailing spaces, while still round-tripping those spaces. * Added a .fringeWhitespace property to the linear model and ve.dm.Node ** Object containing innerPre, innerPost, outerPre, outerPost ** Only inner* are used right now, outer* are planned for future use ** Like .attributes , it's suppressed if it's an empty object * In getDataFromDom(): ** Store the stripped whitespace in .fringeWhitespace ** Move emptiness check up: empty elements with .fringeWhitespace have to be preserved ** Move paragraph wrapping up: .fringeWhitespace has to be applied to the generated paragraph, not its parent ** Add wrapperElement to keep track of the element .fringeWhitespace has to be added to; this is either dataElement or the generated paragraph or nothing, but we can't modify dataElement because it's used later * In getDomFromData(): ** When processing an opening, store the fringeWhitespace data in the generated DOM node ** When processing a closing, add the stored whitespace back in * In the ve.dm.Document constructor, pass through .fringeWhitespace from the linear model data to the generated nodes Tests: * Change one existing test case to account for this change * Add three new test cases for this behavior * Add normalizedHtml field so I can test behavior with bare content Change-Id: I0411544652dd72b923c831c495d69ee4322a2c14
292 lines
7.2 KiB
JavaScript
292 lines
7.2 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 {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 );
|