blob: 98a56135c4764ded3e4f11b8b3edd3c7047203b6 [file] [log] [blame]
// Copyright 2009 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Class representing a same-directionality sequence of text in
* the same block-level element of the DOM.
*/
goog.provide('bidichecker.DirChunk');
goog.provide('bidichecker.NullDirChunk');
goog.require('bidichecker.HighlightableArea');
goog.require('bidichecker.HighlightableText');
goog.require('bidichecker.utils');
goog.require('goog.array');
/**
* Class associating a character position within a string with a DOM node.
* This is used to keep track of where in the DOM a substring is. Note that the
* actual string and its contents are not accessible from here.
* @param {number} charPos A character position within the string.
* @param {Node} node The lowest-level DOM node whose text contents
* begin or resume at that position.
* @constructor
* @private
*/
bidichecker.CharPositionNode_ = function(charPos, node) {
/** @type {number} */
this.charPos = charPos;
/** @type {Node} */
this.node = node;
};
/**
* Class mapping positions in a string to the DOM nodes which contain them.
* Used to tell where in the DOM a bit of the string is. Note that the actual
* string and its contents are not accessible from here.
* @param {Node} firstNode The lowest-level DOM node whose text
* contents begin at the start of the string (character position 0).
* @constructor
* @private
*/
bidichecker.CharPositionNodeFinder_ = function(firstNode) {
/**
* A list of CharPositionNode's, in the order they appear in the string.
* @type {Array.<bidichecker.CharPositionNode_>}
* @private
*/
this.nodePositions_ = [new bidichecker.CharPositionNode_(0, firstNode)];
/**
* The node most recently added to the list of nodes. Used to avoid
* redundant entries.
* @type {Node}
* @private
*/
this.lastNode_ = firstNode;
};
/**
* Adds the next DOM node, mapped to its position in the string.
* Must have a greater charPos than the previous node added to this object.
* @param {number} charPos The character position within the string.
* @param {Node} node The text DOM node whose contents begin at that position.
*/
bidichecker.CharPositionNodeFinder_.prototype.append = function(charPos, node) {
if (this.lastNode_ != node) {
this.nodePositions_.push(new bidichecker.CharPositionNode_(charPos, node));
this.lastNode_ = node;
}
};
/**
* Given a character position, returns the index of the node containing the
* character at that position.
* @param {number} charPos The character position.
* @return {number} The arary index of the node containing the character.
* @private
*/
bidichecker.CharPositionNodeFinder_.prototype.findNodeIndexAtPosition_ =
function(charPos) {
var index = goog.array.binarySearch(this.nodePositions_, charPos,
function(targetPos, entry) {
return targetPos - entry.charPos;
});
// If the exact character position appears in nodePositions_, binarySearch()
// returns its index in the array. Otherwise, it returns (-i - 1), where i is
// the index at which it would be inserted. In that case, the array index
// we're looking for is the one preceding the insertion point.
return index >= 0 ? index : -index - 2;
};
/**
* Given a character position, returns the node immediately containing the
* character at that position.
* @param {number} charPos The character position of the substring start.
* @return {Node} The text node containing the character at charPos.
*/
bidichecker.CharPositionNodeFinder_.prototype.findNodeAtPosition = function(
charPos) {
var index = this.findNodeIndexAtPosition_(charPos);
return this.nodePositions_[index].node;
};
/**
* Given the character location of a substring of this chunk, returns the
* highlightable area of the DOM containing its text.
* @param {number} startPos The character index of the substring start.
* @param {number} length The substring length in characters.
* @return {!bidichecker.HighlightableArea} The error location.
*/
bidichecker.CharPositionNodeFinder_.prototype.getHighlightableArea = function(
startPos, length) {
var startIndex = this.findNodeIndexAtPosition_(startPos);
var endIndex = this.findNodeIndexAtPosition_(startPos + length - 1);
var nodes = [];
for (var i = startIndex; i <= endIndex; ++i) {
nodes.push(this.nodePositions_[i].node);
}
return new bidichecker.HighlightableText(nodes,
startPos - this.nodePositions_[startIndex].charPos,
startPos + length - this.nodePositions_[endIndex].charPos);
};
/**
* Class representing a chunk of text with the same declared directionality
* within the same block-level element. Chunks can span multiple inline elements
* within a block, so long as the declared directionality is unchanged.
* Successive inline pieces of the chunk are added using the append() method.
* @param {string} text The text contents of the first piece of the chunk.
* @param {boolean} isRtl Whether the declared directionality is right-to-left.
* @param {Node} node The text node containing the first piece of the chunk.
* @param {Element} block The lowest block-level ancestor of {@code node}.
* @param {boolean} isDeclared Whether the chunk is in a declared-directionality
* context.
* @constructor
*/
bidichecker.DirChunk = function(text, isRtl, node, block, isDeclared) {
/**
* Is the declared directionality right-to-left?
* @type {boolean}
* @private
*/
this.isRtl_ = isRtl;
/**
* Is this chunk contained somewhere within an element with a declared
* directionality? (That is, any element below the root element which has a
* "dir" attribute, unless the element also contains a block-level element.)
* @type {boolean}
* @private
*/
this.isDeclared_ = isDeclared;
/**
* The block immediately containing this chunk.
* @type {Element}
* @private
*/
this.block_ = block;
/**
* The pieces of text comprising the contents of the chunk.
* @type {Array.<string>}
* @private
*/
this.textPieces_ = [text];
/**
* The cumulative string length of the text of the chunk so far.
* @type {number}
* @private
*/
this.textLength_ = text.length;
/**
* The map from character positions to nodes containing them.
* @type {bidichecker.CharPositionNodeFinder_}
* @private
*/
this.nodeFinder_ = new bidichecker.CharPositionNodeFinder_(node);
};
/**
* The concatenated textual contents of the chunk, computed lazily on demand.
* @type {?string}
* @private
*/
bidichecker.DirChunk.prototype.text_ = null;
/**
* @return {boolean} Is the declared directionality right-to-left?
*/
bidichecker.DirChunk.prototype.isRtl = function() {
return this.isRtl_;
};
/**
* @return {boolean} Is the chunk in a declared-directionality context?
*/
bidichecker.DirChunk.prototype.isDeclared = function() {
return this.isDeclared_;
};
/**
* @return {Element} The block immediately containing this chunk.
*/
bidichecker.DirChunk.prototype.getBlock = function() {
return this.block_;
};
/**
* Does the chunk contain any characters?
* @return {boolean} Are all the pieces empty?
*/
bidichecker.DirChunk.prototype.isEmpty = function() {
return goog.array.every(this.textPieces_,
function(s) { return s.length == 0; }
);
};
/**
* Appends another segment of text to this chunk.
* @param {string} text The text to append.
* @param {Node} node The text node containing it.
*/
bidichecker.DirChunk.prototype.append = function(text, node) {
this.nodeFinder_.append(this.textLength_, node);
this.textLength_ += text.length;
this.textPieces_.push(text);
this.text_ = null; // Invalidate precomputed value, if any.
};
/**
* Is this chunk in the given context (declared directionality and enclosing
* block)?
* @param {boolean} isRtl Whether the declared directionality is right-to-left.
* @param {Element} block The containing block element.
* @param {boolean} isDeclared Whether the chunk is in a declared-directionality
* context.
* @return {boolean} Is it in the same context?
*/
bidichecker.DirChunk.prototype.hasSameContext = function(isRtl, block,
isDeclared) {
return isRtl == this.isRtl_ && block == this.block_ &&
isDeclared == this.isDeclared_;
};
/**
* Assembles and returns the complete text contents of this chunk.
* @return {string} The text contents of this chunk.
*/
bidichecker.DirChunk.prototype.getText = function() {
// TODO(user): Translate <BR> to a newline, or some other visible text.
if (this.text_ === null) {
this.text_ = this.textPieces_.join('');
}
return /** @type {string} */ (this.text_);
};
/**
* Given the string character index of a substring in this chunk, returns the
* node immediately containing the character at that position.
* @param {number} charPos The character index of the substring start.
* @return {Node} The text node containing the character at charPos.
*/
bidichecker.DirChunk.prototype.findNodeAtPosition = function(charPos) {
return this.nodeFinder_.findNodeAtPosition(charPos);
};
/**
* Given the character location of a substring of this chunk, returns the DOM
* location containing its text.
* @param {number} startPos The character index of the substring start.
* @param {number} length The substring length in characters.
* @return {!bidichecker.HighlightableArea} The error location.
*/
bidichecker.DirChunk.prototype.getHighlightableArea = function(startPos,
length) {
return this.nodeFinder_.getHighlightableArea(startPos, length);
};
/**
* A canonical empty instance of {@code bidichecker.DirChunk} (the Null Object
* pattern).
* @type {bidichecker.DirChunk}
*/
bidichecker.NullDirChunk =
new bidichecker.DirChunk('', false, null, null, false);