// Copyright 2006 The Closure Library Authors. 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 * Expression evaluation utilities. Expression format is very similar to XPath. * * Expression details: * - Of format A/B/C, which will evaluate getChildNode('A').getChildNode('B'). * getChildNodes('C')|getChildNodeValue('C')|getChildNode('C') depending on * call * - If expression ends with '/name()', will get the name() of the node * referenced by the preceding path. * - If expression ends with '/count()', will get the count() of the nodes that * match the expression referenced by the preceding path. * - If expression ends with '?', the value is OK to evaluate to null. This is * not enforced by the expression evaluation functions, instead it is * provided as a flag for client code which may ignore depending on usage * - If expression has [INDEX], will use getChildNodes().getByIndex(INDEX) * */ goog.provide('goog.ds.Expr'); goog.require('goog.ds.BasicNodeList'); goog.require('goog.ds.EmptyNodeList'); goog.require('goog.string'); /** * Create a new expression. An expression uses a string expression language, and * from this string and a passed in DataNode can evaluate to a value, DataNode, * or a DataNodeList. * * @param {string=} opt_expr The string expression. * @constructor * @final */ goog.ds.Expr = function(opt_expr) { if (opt_expr) { this.setSource_(opt_expr); } }; /** * Set the source expression text & parse * * @param {string} expr The string expression source. * @param {Array=} opt_parts Array of the parts of an expression. * @param {goog.ds.Expr=} opt_childExpr Optional child of this expression, * passed in as a hint for processing. * @param {goog.ds.Expr=} opt_prevExpr Optional preceding expression * (i.e. $A/B/C is previous expression to B/C) passed in as a hint for * processing. * @private */ goog.ds.Expr.prototype.setSource_ = function( expr, opt_parts, opt_childExpr, opt_prevExpr) { this.src_ = expr; if (!opt_childExpr && !opt_prevExpr) { // Check whether it can be empty if (goog.string.endsWith(expr, goog.ds.Expr.String_.CAN_BE_EMPTY)) { this.canBeEmpty_ = true; expr = expr.substring(0, expr.length - 1); } // Check whether this is an node function if (goog.string.endsWith(expr, '()')) { if (goog.string.endsWith(expr, goog.ds.Expr.String_.NAME_EXPR) || goog.string.endsWith(expr, goog.ds.Expr.String_.COUNT_EXPR) || goog.string.endsWith(expr, goog.ds.Expr.String_.POSITION_EXPR)) { var lastPos = expr.lastIndexOf(goog.ds.Expr.String_.SEPARATOR); if (lastPos != -1) { this.exprFn_ = expr.substring(lastPos + 1); expr = expr.substring(0, lastPos); } else { this.exprFn_ = expr; expr = goog.ds.Expr.String_.CURRENT_NODE_EXPR; } if (this.exprFn_ == goog.ds.Expr.String_.COUNT_EXPR) { this.isCount_ = true; } } } } // Split into component parts this.parts_ = opt_parts || expr.split('/'); this.size_ = this.parts_.length; this.last_ = this.parts_[this.size_ - 1]; this.root_ = this.parts_[0]; if (this.size_ == 1) { this.rootExpr_ = this; this.isAbsolute_ = goog.string.startsWith(expr, '$'); } else { this.rootExpr_ = goog.ds.Expr.createInternal_(this.root_, null, this, null); this.isAbsolute_ = this.rootExpr_.isAbsolute_; this.root_ = this.rootExpr_.root_; } if (this.size_ == 1 && !this.isAbsolute_) { // Check whether expression maps to current node, for convenience this.isCurrent_ = (expr == goog.ds.Expr.String_.CURRENT_NODE_EXPR || expr == goog.ds.Expr.String_.EMPTY_EXPR); // Whether this expression is just an attribute (i.e. '@foo') this.isJustAttribute_ = goog.string.startsWith(expr, goog.ds.Expr.String_.ATTRIBUTE_START); // Check whether this is a common node expression this.isAllChildNodes_ = expr == goog.ds.Expr.String_.ALL_CHILD_NODES_EXPR; this.isAllAttributes_ = expr == goog.ds.Expr.String_.ALL_ATTRIBUTES_EXPR; this.isAllElements_ = expr == goog.ds.Expr.String_.ALL_ELEMENTS_EXPR; } }; /** * Get the source data path for the expression * @return {string} The path. */ goog.ds.Expr.prototype.getSource = function() { return this.src_; }; /** * Gets the last part of the expression. * @return {?string} Last part of the expression. */ goog.ds.Expr.prototype.getLast = function() { return this.last_; }; /** * Gets the parent expression of this expression, or null if this is top level * @return {goog.ds.Expr} The parent. */ goog.ds.Expr.prototype.getParent = function() { if (!this.parentExprSet_) { if (this.size_ > 1) { this.parentExpr_ = goog.ds.Expr.createInternal_( null, this.parts_.slice(0, this.parts_.length - 1), this, null); } this.parentExprSet_ = true; } return this.parentExpr_; }; /** * Gets the parent expression of this expression, or null if this is top level * @return {goog.ds.Expr} The parent. */ goog.ds.Expr.prototype.getNext = function() { if (!this.nextExprSet_) { if (this.size_ > 1) { this.nextExpr_ = goog.ds.Expr.createInternal_(null, this.parts_.slice(1), null, this); } this.nextExprSet_ = true; } return this.nextExpr_; }; /** * Evaluate an expression on a data node, and return a value * Recursively walks through child nodes to evaluate * TODO(user) Support other expression functions * * @param {goog.ds.DataNode=} opt_ds Optional datasource to evaluate against. * If not provided, evaluates against DataManager global root. * @return {*} Value of the node, or null if doesn't exist. * @suppress {missingRequire} Cannot depend on goog.ds.DataManager because * it creates a circular dependency. */ goog.ds.Expr.prototype.getValue = function(opt_ds) { if (opt_ds == null) { opt_ds = goog.ds.DataManager.getInstance(); } else if (this.isAbsolute_) { opt_ds = opt_ds.getDataRoot ? opt_ds.getDataRoot() : goog.ds.DataManager.getInstance(); } if (this.isCount_) { var nodes = this.getNodes(opt_ds); return nodes.getCount(); } if (this.size_ == 1) { return opt_ds.getChildNodeValue(this.root_); } else if (this.size_ == 0) { return opt_ds.get(); } var nextDs = opt_ds.getChildNode(this.root_); if (nextDs == null) { return null; } else { return this.getNext().getValue(nextDs); } }; /** * Evaluate an expression on a data node, and return matching nodes * Recursively walks through child nodes to evaluate * * @param {goog.ds.DataNode=} opt_ds Optional datasource to evaluate against. * If not provided, evaluates against data root. * @param {boolean=} opt_canCreate If true, will try to create new nodes. * @return {goog.ds.DataNodeList} Matching nodes. */ goog.ds.Expr.prototype.getNodes = function(opt_ds, opt_canCreate) { return /** @type {goog.ds.DataNodeList} */ ( this.getNodes_(opt_ds, false, opt_canCreate)); }; /** * Evaluate an expression on a data node, and return the first matching node * Recursively walks through child nodes to evaluate * * @param {goog.ds.DataNode=} opt_ds Optional datasource to evaluate against. * If not provided, evaluates against DataManager global root. * @param {boolean=} opt_canCreate If true, will try to create new nodes. * @return {goog.ds.DataNode} Matching nodes, or null if doesn't exist. */ goog.ds.Expr.prototype.getNode = function(opt_ds, opt_canCreate) { return /** @type {goog.ds.DataNode} */ ( this.getNodes_(opt_ds, true, opt_canCreate)); }; /** * Evaluate an expression on a data node, and return the first matching node * Recursively walks through child nodes to evaluate * * @param {goog.ds.DataNode=} opt_ds Optional datasource to evaluate against. * If not provided, evaluates against DataManager global root. * @param {boolean=} opt_selectOne Whether to return single matching DataNode * or matching nodes in DataNodeList. * @param {boolean=} opt_canCreate If true, will try to create new nodes. * @return {goog.ds.DataNode|goog.ds.DataNodeList} Matching node or nodes, * depending on value of opt_selectOne. * @private * @suppress {missingRequire} Cannot depend on goog.ds.DataManager because * it creates a circular dependency. */ goog.ds.Expr.prototype.getNodes_ = function( opt_ds, opt_selectOne, opt_canCreate) { if (opt_ds == null) { opt_ds = goog.ds.DataManager.getInstance(); } else if (this.isAbsolute_) { opt_ds = opt_ds.getDataRoot ? opt_ds.getDataRoot() : goog.ds.DataManager.getInstance(); } if (this.size_ == 0 && opt_selectOne) { return opt_ds; } else if (this.size_ == 0 && !opt_selectOne) { return new goog.ds.BasicNodeList([opt_ds]); } else if (this.size_ == 1) { if (opt_selectOne) { return opt_ds.getChildNode(this.root_, opt_canCreate); } else { var possibleListChild = opt_ds.getChildNode(this.root_); if (possibleListChild && possibleListChild.isList()) { return possibleListChild.getChildNodes(); } else { return opt_ds.getChildNodes(this.root_); } } } else { var nextDs = opt_ds.getChildNode(this.root_, opt_canCreate); if (nextDs == null && opt_selectOne) { return null; } else if (nextDs == null && !opt_selectOne) { return new goog.ds.EmptyNodeList(); } return this.getNext().getNodes_(nextDs, opt_selectOne, opt_canCreate); } }; /** * Whether the expression can be null. * * @type {boolean} * @private */ goog.ds.Expr.prototype.canBeEmpty_ = false; /** * The parsed paths in the expression * * @type {Array} * @private */ goog.ds.Expr.prototype.parts_ = []; /** * Number of paths in the expression * * @type {?number} * @private */ goog.ds.Expr.prototype.size_ = null; /** * The root node path in the expression * * @type {string} * @private */ goog.ds.Expr.prototype.root_; /** * The last path in the expression * * @type {?string} * @private */ goog.ds.Expr.prototype.last_ = null; /** * Whether the expression evaluates to current node * * @type {boolean} * @private */ goog.ds.Expr.prototype.isCurrent_ = false; /** * Whether the expression is just an attribute * * @type {boolean} * @private */ goog.ds.Expr.prototype.isJustAttribute_ = false; /** * Does this expression select all DOM-style child nodes (element and text) * * @type {boolean} * @private */ goog.ds.Expr.prototype.isAllChildNodes_ = false; /** * Does this expression select all DOM-style attribute nodes (starts with '@') * * @type {boolean} * @private */ goog.ds.Expr.prototype.isAllAttributes_ = false; /** * Does this expression select all DOM-style element child nodes * * @type {boolean} * @private */ goog.ds.Expr.prototype.isAllElements_ = false; /** * The function used by this expression * * @type {?string} * @private */ goog.ds.Expr.prototype.exprFn_ = null; /** * Cached value for the parent expression. * @type {goog.ds.Expr?} * @private */ goog.ds.Expr.prototype.parentExpr_ = null; /** * Cached value for the next expression. * @type {goog.ds.Expr?} * @private */ goog.ds.Expr.prototype.nextExpr_ = null; /** * Create an expression from a string, can use cached values * * @param {string} expr The expression string. * @return {goog.ds.Expr} The expression object. */ goog.ds.Expr.create = function(expr) { var result = goog.ds.Expr.cache_[expr]; if (result == null) { result = new goog.ds.Expr(expr); goog.ds.Expr.cache_[expr] = result; } return result; }; /** * Create an expression from a string, can use cached values * Uses hints from related expressions to help in creation * * @param {?string=} opt_expr The string expression source. * @param {Array=} opt_parts Array of the parts of an expression. * @param {goog.ds.Expr=} opt_childExpr Optional child of this expression, * passed in as a hint for processing. * @param {goog.ds.Expr=} opt_prevExpr Optional preceding expression * (i.e. $A/B/C is previous expression to B/C) passed in as a hint for * processing. * @return {goog.ds.Expr} The expression object. * @private */ goog.ds.Expr.createInternal_ = function( opt_expr, opt_parts, opt_childExpr, opt_prevExpr) { var expr = opt_expr || opt_parts.join('/'); var result = goog.ds.Expr.cache_[expr]; if (result == null) { result = new goog.ds.Expr(); result.setSource_(expr, opt_parts, opt_childExpr, opt_prevExpr); goog.ds.Expr.cache_[expr] = result; } return result; }; /** * Cache of pre-parsed expressions * @private */ goog.ds.Expr.cache_ = {}; /** * Commonly used strings in expressions. * @enum {string} * @private */ goog.ds.Expr.String_ = { SEPARATOR: '/', CURRENT_NODE_EXPR: '.', EMPTY_EXPR: '', ATTRIBUTE_START: '@', ALL_CHILD_NODES_EXPR: '*|text()', ALL_ATTRIBUTES_EXPR: '@*', ALL_ELEMENTS_EXPR: '*', NAME_EXPR: 'name()', COUNT_EXPR: 'count()', POSITION_EXPR: 'position()', INDEX_START: '[', INDEX_END: ']', CAN_BE_EMPTY: '?' }; /** * Standard expressions */ /** * The current node */ goog.ds.Expr.CURRENT = goog.ds.Expr.create(goog.ds.Expr.String_.CURRENT_NODE_EXPR); /** * For DOM interop - all DOM child nodes (text + element). * Text nodes have dataName #text */ goog.ds.Expr.ALL_CHILD_NODES = goog.ds.Expr.create(goog.ds.Expr.String_.ALL_CHILD_NODES_EXPR); /** * For DOM interop - all DOM element child nodes */ goog.ds.Expr.ALL_ELEMENTS = goog.ds.Expr.create(goog.ds.Expr.String_.ALL_ELEMENTS_EXPR); /** * For DOM interop - all DOM attribute nodes * Attribute nodes have dataName starting with "@" */ goog.ds.Expr.ALL_ATTRIBUTES = goog.ds.Expr.create(goog.ds.Expr.String_.ALL_ATTRIBUTES_EXPR); /** * Get the dataName of a node */ goog.ds.Expr.NAME = goog.ds.Expr.create(goog.ds.Expr.String_.NAME_EXPR); /** * Get the count of nodes matching an expression */ goog.ds.Expr.COUNT = goog.ds.Expr.create(goog.ds.Expr.String_.COUNT_EXPR); /** * Get the position of the "current" node in the current node list * This will only apply for datasources that support the concept of a current * node (none exist yet). This is similar to XPath position() and concept of * current node */ goog.ds.Expr.POSITION = goog.ds.Expr.create(goog.ds.Expr.String_.POSITION_EXPR);