blob: e1b0688e93b2c51d3c14cbec61bdc3762ac44645 [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 Walker to traverse the DOM and dispatch events indicating its
* contents, while tracking the directionality.
*/
goog.provide('bidichecker.DomWalker');
goog.provide('bidichecker.DomWalker.EventTypes');
goog.require('bidichecker.utils');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.TagIterator');
goog.require('goog.events.EventTarget');
goog.require('goog.style');
/**
* A DOM tree traverser with bidi-related services which generates events for
* each type of node it encounters. Based on {@code goog.dom.TagIterator}, but
* skips non-displayable elements.
* @param {Element} block A block-level element in the DOM within which to
* traverse.
* @constructor
* @extends {goog.events.EventTarget}
*/
bidichecker.DomWalker = function(block) {
goog.events.EventTarget.call(this);
/**
* Iterator over the tags in the DOM in the top-level block we're working on.
* @type {goog.dom.TagIterator}
* @private
*/
this.nodeIter_ = new goog.dom.TagIterator(block);
/**
* The root block within which we are traversing. Will not be modified over
* the lifetime of this object.
* @type {Element}
* @private
*/
this.rootBlock_ = block;
/**
* Stack of isRtl values of elements from the current location up to the root
* block.
* <p>The stack is initialized with the directionality of the root node. This
* value will be added (again) to the stack when the root node is entered
* during traversal. In principle, the initial value entered here should
* represent the parent of the root node, but we don't actually use that
* because by definition the root node is the highest node we're interested
* in; data about its parent is never used.
* <p>For simplicity of implementation of {@code isDeclared} (see below in
* {@code next_()}), we duplicate the root node's directionality at the start
* of the stack to ensure that the root node is never attributed {@code
* isDeclared} status; otherwise, any arbitrary value could be used here.
* @type {Array.<boolean>}
* @private
*/
this.isRtlStack_ = [goog.style.isRightToLeft(block)];
/**
* Stack of isDeclared values of elements from the current location up to the
* root block.
* @type {Array.<boolean>}
* @private
*/
this.isDeclaredStack_ = [];
/**
* Stack of block elements from the current location up to the root block.
* @type {Array.<Element>}
* @private
*/
this.blockStack_ = [block];
/**
* Frames found so far during traversal.
* @type {Array.<Element>}
* @private
*/
this.frames_ = [];
};
goog.inherits(bidichecker.DomWalker, goog.events.EventTarget);
/**
* The node currently being traversed.
* @type {Node}
* @private
*/
bidichecker.DomWalker.prototype.node_ = null;
/**
* Walks the entire DOM, dispatching events as it goes. Dispatches four types of
* events: {@code StartTag}, {@code EndTag}, {@code TextNode}, and {@code
* EndOfDom}.
*/
bidichecker.DomWalker.prototype.go = function() {
try {
for (;; this.next_()) {
// Empty body; iterator events are handled by the event listeners.
}
} catch (e) {
// Ignore StopIteration; it's just the end of the DOM. Otherwise, rethrow.
if (e !== goog.iter.StopIteration) {
throw e;
} else {
this.dispatchEvent(bidichecker.DomWalker.EventTypes.END_OF_DOM);
}
}
};
/**
* Event types dispatched by {@code bidichecker.DomWalker}.
* @enum {string}
*/
bidichecker.DomWalker.EventTypes = {
START_TAG: 'StartTag',
END_TAG: 'EndTag',
TEXT_NODE: 'TextNode',
END_OF_DOM: 'EndOfDom'
};
/**
* Advances to the next displayable node position in the DOM, dispatching an
* appropriate event if relevant: {@code StartTag}, {@code EndTag}, {@code
* TextNode}.
* @throws {goog.iter.StopIteration} when iteration is done.
* @private
*/
bidichecker.DomWalker.prototype.next_ = function() {
this.node_ = this.nextDisplayableNode_();
if (this.nodeIter_.isStartTag()) {
var element = /** @type {Element} */ (this.node_);
var isRtl = goog.style.isRightToLeft(element);
// If this element has "declared directionality" (see below), we've
// entered a "declared" subtree of the DOM. From that point, we stay
// declared all the way down.
var isDeclared = goog.array.peek(this.isDeclaredStack_) ||
isRtl != goog.array.peek(this.isRtlStack_) ||
this.declaresDir_(element);
this.isDeclaredStack_.push(isDeclared);
this.isRtlStack_.push(isRtl);
// Have to check for a frame before a block, since on some browsers frames
// are also identified as blocks.
if (element.nodeName == 'IFRAME' || element.nodeName == 'FRAME') {
this.frames_.push(element);
} else if (bidichecker.utils.isBlockElement(element)) {
// Entering a new block.
this.blockStack_.push(element);
}
this.dispatchEvent(bidichecker.DomWalker.EventTypes.START_TAG);
} else if (this.nodeIter_.isEndTag()) {
this.dispatchEvent(bidichecker.DomWalker.EventTypes.END_TAG);
// Now pop all the stacks; the state of the stacks when an END_TAG event is
// dispatched should be identical with the state after the corresponding
// START_TAG event.
this.isRtlStack_.pop();
this.isDeclaredStack_.pop();
if (this.node_ == goog.array.peek(this.blockStack_)) {
// Exiting a block.
this.blockStack_.pop();
}
} else if (this.node_.nodeType == goog.dom.NodeType.TEXT) {
this.dispatchEvent(bidichecker.DomWalker.EventTypes.TEXT_NODE);
}
};
/**
* @return {Node} The node at the current traversal position.
*/
bidichecker.DomWalker.prototype.getNode = function() {
return this.node_;
};
/**
* @return {boolean} Whether the current position has declared directionality
* of right-to-left.
*/
bidichecker.DomWalker.prototype.inRtl = function() {
return /** @type {boolean} */ (goog.array.peek(this.isRtlStack_));
};
/**
* @return {boolean} Whether the element containing the current position has
* declared directionality of right-to-left. Never reaches above the root
* node; calling this with the traversal at the root node returns its own
* directionality, not its parent's.
*/
bidichecker.DomWalker.prototype.parentInRtl = function() {
// Once an event has been dispatched, isRtlStack_ always contains at least two
// elements, since it's initialized with an extra copy of the root block's
// directionality.
return /** @type {boolean} */ (this.isRtlStack_[this.isRtlStack_.length - 2]);
};
/**
* @return {boolean} Whether the current position has "declared directionality"
* status.
*/
bidichecker.DomWalker.prototype.inDeclaredDir = function() {
return /** @type {boolean} */ (goog.array.peek(this.isDeclaredStack_));
};
/**
* @return {Element} The block-level element directly containing the current
* position.
*/
bidichecker.DomWalker.prototype.getCurrentBlock = function() {
return (/** @type {Element} */ goog.array.peek(this.blockStack_));
};
/**
* @return {Element} The root block-level element whose contents we are
* traversing.
*/
bidichecker.DomWalker.prototype.getRootBlock = function() {
return this.rootBlock_;
};
/**
* @return {Array.<Element>} List of frames found so far during traversal.
* Only contains top-level frames, not any nested within them.
*/
bidichecker.DomWalker.prototype.getFrames = function() {
return this.frames_;
};
/**
* Does this element impart "declared directionality" status to the text it
* contains? This is true for an element below the root element which has a
* "dir" attribute, unless the element also contains a block-level element.
* @param {Element} element The element to test.
* @return {boolean} Does the element have "isDeclared" status?
* @private
*/
bidichecker.DomWalker.prototype.declaresDir_ = function(element) {
if (!element.dir || element == this.rootBlock_) {
return false;
}
for (var child = element.firstChild; child; child = child.nextSibling) {
if (child.nodeType == goog.dom.NodeType.ELEMENT &&
bidichecker.utils.isBlockElement(/** @type {Element} */ (child))) {
return false;
}
}
return true;
};
/**
* Advances the node iterator while skipping non-displayable elements.
* @return {Node} The next node in a displayable element.
* @throws goog.iter.StopIteration when the whole DOM has been read.
* @private
*/
bidichecker.DomWalker.prototype.nextDisplayableNode_ = function() {
var node = this.nodeIter_.next();
while (this.nodeIter_.isStartTag() &&
!bidichecker.utils.isDisplayable(node)) {
this.nodeIter_.skipTag();
node = this.nodeIter_.next();
}
return node;
};