mediawiki-extensions-Visual.../modules/ve/ve.BranchNode.js
Catrope 9372ca60ad Kill getOffsetFromNode() with fire
The way it operated was evil. It did a depth-first search from the root,
finding the node using reference equality. For documents with deep
structures, this could take a long time. Inez did some profiling and
found it was called tens of millions of times on a complex document.

Kill getOffsetFromNode() and move its functionality to getOffset().
The logic has been completely rewritten: getOffset() now traverses
up from the node rather than down from the root, and pretty much does
the reverse of what getNodeFromOffset() does. This should be much more
efficient even without offset caching in the node objects (which we may
still implement later).

Change-Id: I125f9fa423c40db6472e2c4a7c94214218ba3bc7
2013-05-09 17:26:22 -07:00

141 lines
3.7 KiB
JavaScript

/*!
* VisualEditor BranchNode class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Branch node mixin.
*
* Extenders are expected to inherit from ve.Node.
*
* Branch nodes are immutable, which is why there are no methods for adding or removing children.
* DataModel classes will add this functionality, and other subclasses will implement behavior that
* mimcs changes made to DataModel nodes.
*
* @class
* @abstract
* @constructor
* @param {ve.Node[]} children Array of children to add
*/
ve.BranchNode = function VeBranchNode( children ) {
this.children = ve.isArray( children ) ? children : [];
};
/* Methods */
/**
* Check if the node has children.
*
* @method
* @returns {boolean} Whether the node has children
*/
ve.BranchNode.prototype.hasChildren = function () {
return true;
};
/**
* Get child nodes.
*
* @method
* @returns {ve.Node[]} List of child nodes
*/
ve.BranchNode.prototype.getChildren = function () {
return this.children;
};
/**
* Get the index of a child node.
*
* @method
* @param {ve.dm.Node} node Child node to find index of
* @returns {number} Index of child node or -1 if node was not found
*/
ve.BranchNode.prototype.indexOf = function ( node ) {
return ve.indexOf( node, this.children );
};
/**
* Set the root node.
*
* @method
* @see ve.Node#setRoot
* @param {ve.Node} root Node to use as root
*/
ve.BranchNode.prototype.setRoot = function ( root ) {
if ( root === this.root ) {
// Nothing to do, don't recurse into all descendants
return;
}
this.root = root;
for ( var i = 0; i < this.children.length; i++ ) {
this.children[i].setRoot( root );
}
};
/**
* Set the document the node is a part of.
*
* @method
* @see ve.Node#setDocument
* @param {ve.Document} root Node to use as root
*/
ve.BranchNode.prototype.setDocument = function ( doc ) {
if ( doc === this.doc ) {
// Nothing to do, don't recurse into all descendants
return;
}
this.doc = doc;
for ( var i = 0; i < this.children.length; i++ ) {
this.children[i].setDocument( doc );
}
};
/**
* Get a node from an offset.
*
* This method is pretty expensive. If you need to get different slices of the same content, get
* the content first, then slice it up locally.
*
* TODO: Rewrite this method to not use recursion, because the function call overhead is expensive
*
* @method
* @param {number} offset Offset get node for
* @param {boolean} [shallow] Do not iterate into child nodes of child nodes
* @returns {ve.Node|null} Node at offset, or null if non was found
*/
ve.BranchNode.prototype.getNodeFromOffset = function ( offset, shallow ) {
if ( offset === 0 ) {
return this;
}
// TODO a lot of logic is duplicated in selectNodes(), abstract that into a traverser or something
if ( this.children.length ) {
var i, length, nodeLength, childNode,
nodeOffset = 0;
for ( i = 0, length = this.children.length; i < length; i++ ) {
childNode = this.children[i];
if ( offset === nodeOffset ) {
// The requested offset is right before childNode,
// so it's not inside any of this's children, but inside this
return this;
}
nodeLength = childNode.getOuterLength();
if ( offset >= nodeOffset && offset < nodeOffset + nodeLength ) {
if ( !shallow && childNode.hasChildren() && childNode.getChildren().length ) {
return this.getNodeFromOffset.call( childNode, offset - nodeOffset - 1 );
} else {
return childNode;
}
}
nodeOffset += nodeLength;
}
if ( offset === nodeOffset ) {
// The requested offset is right before this.children[i],
// so it's not inside any of this's children, but inside this
return this;
}
}
return null;
};