1 /**
  2  * @fileoverview A function call expression.
  3  */
  4 
  5 goog.provide('xrx.xpath.FunctionCall');
  6 
  7 goog.require('goog.array');
  8 goog.require('goog.dom');
  9 goog.require('goog.string');
 10 goog.require('xrx.xpath.DataType');
 11 goog.require('xrx.xpath.Expr');
 12 goog.require('xrx.xpath.Function');
 13 goog.require('xrx.node');
 14 goog.require('xrx.xpath.NodeSet');
 15 
 16 
 17 
 18 /**
 19  * A function call expression.
 20  *
 21  * @constructor
 22  * @extends {xrx.xpath.Expr}
 23  * @param {!xrx.xpath.FunctionCall.BuiltInFunc} func Function.
 24  * @param {!Array.<!xrx.xpath.Expr>} args Arguments to the function.
 25  */
 26 xrx.xpath.FunctionCall = function(func, args) {
 27   // Check the provided arguments match the function parameters.
 28   if (args.length < func.minArgs) {
 29     throw new Error('Function ' + func.name + ' expects at least' +
 30         func.minArgs + ' arguments, ' + args.length + ' given');
 31   }
 32   if (!goog.isNull(func.maxArgs) && args.length > func.maxArgs) {
 33     throw new Error('Function ' + func.name + ' expects at most ' +
 34         func.maxArgs + ' arguments, ' + args.length + ' given');
 35   }
 36   if (func.nodesetsRequired_) {
 37     goog.array.forEach(args, function(arg, i) {
 38       if (arg.getDataType() != xrx.xpath.DataType.NODESET) {
 39         throw new Error('Argument ' + i + ' to function ' + func.name +
 40             ' is not of type Nodeset: ' + arg);
 41       }
 42     });
 43   }
 44   xrx.xpath.Expr.call(this, func.returnType);
 45 
 46   /**
 47    * @type {!xrx.xpath.FunctionCall.BuiltInFunc}
 48    * @private
 49    */
 50   this.func_ = func;
 51 
 52   /**
 53    * @type {!Array.<!xrx.xpath.Expr>}
 54    * @private
 55    */
 56   this.args_ = args;
 57 
 58   this.setNeedContextPosition(func.needContextPosition_ ||
 59       goog.array.some(args, function(arg) {
 60         return arg.doesNeedContextPosition();
 61       }));
 62   this.setNeedContextNode(
 63       (func.needContextNodeWithoutArgs_ && !args.length) ||
 64       (func.needContextNodeWithArgs_ && !!args.length) ||
 65       goog.array.some(args, function(arg) {
 66         return arg.doesNeedContextNode();
 67       }));
 68 };
 69 goog.inherits(xrx.xpath.FunctionCall, xrx.xpath.Expr);
 70 
 71 
 72 /**
 73  * @override
 74  */
 75 xrx.xpath.FunctionCall.prototype.evaluate = function(ctx) {
 76   var result = this.func_.evaluate.apply(null,
 77       goog.array.concat(ctx, this.args_));
 78   return /** @type {!(string|boolean|number|xrx.xpath.NodeSet)} */ (result);
 79 };
 80 
 81 
 82 /**
 83  * @override
 84  */
 85 xrx.xpath.FunctionCall.prototype.toString = function() {
 86   var text = 'Function: ' + this.func_;
 87   if (this.args_.length) {
 88     var args = goog.array.reduce(this.args_, function(prev, curr) {
 89       return prev + xrx.xpath.Expr.indent(curr);
 90     }, 'Arguments:');
 91     text += xrx.xpath.Expr.indent(args);
 92   }
 93   return text;
 94 };
 95 
 96 
 97 
 98 /**
 99  * A mapping from function names to Func objects.
100  *
101  * @private
102  * @type {!Object.<string, !xrx.xpath.FunctionCall.BuiltInFunc>}
103  */
104 xrx.xpath.FunctionCall.nameToFuncMap_ = {};
105 
106 
107 /**
108  * Constructs a Func and maps its name to it.
109  *
110  * @param {string} name Name of the function.
111  * @param {xrx.xpath.DataType} returnType Datatype of the function return value.
112  * @param {boolean} needContextPosition Whether the function needs a context
113  *     position.
114  * @param {boolean} needContextNodeWithoutArgs Whether the function needs a
115  *     context node when not given arguments.
116  * @param {boolean} needContextNodeWithArgs Whether the function needs a context
117  *     node when the function is given arguments.
118  * @param {function(!xrx.xpath.Context, ...[!xrx.xpath.Expr]):*} evaluate
119  *     Evaluates the function in a context with any number of expression
120  *     arguments.
121  * @param {number} minArgs Minimum number of arguments accepted by the function.
122  * @param {?number=} opt_maxArgs Maximum number of arguments accepted by the
123  *     function; null means there is no max; defaults to minArgs.
124  * @param {boolean=} opt_nodesetsRequired Whether the args must be nodesets.
125  * @return {!xrx.xpath.FunctionCall.BuiltInFunc} The function created.
126  */
127 xrx.xpath.FunctionCall.createFunc = function(name, returnType,
128     needContextPosition, needContextNodeWithoutArgs, needContextNodeWithArgs,
129     evaluate, minArgs, opt_maxArgs, opt_nodesetsRequired) {
130   if (name in xrx.xpath.FunctionCall.nameToFuncMap_) {
131     throw new Error('Function already created: ' + name + '.');
132   }
133   var func = new xrx.xpath.Function(name, returnType,
134       needContextPosition, needContextNodeWithoutArgs, needContextNodeWithArgs,
135       evaluate, minArgs, opt_maxArgs, opt_nodesetsRequired);
136   func = /** @type {!xrx.xpath.FunctionCall.BuiltInFunc} */ (func);
137   xrx.xpath.FunctionCall.nameToFuncMap_[name] = func;
138   return func;
139 };
140 
141 
142 /**
143  * Returns the function object for this name.
144  *
145  * @param {string} name The function's name.
146  * @return {xrx.xpath.FunctionCall.BuiltInFunc} The function object.
147  */
148 xrx.xpath.FunctionCall.getFunc = function(name) {
149   return xrx.xpath.FunctionCall.nameToFuncMap_[name] || null;
150 };
151 
152 
153 /**
154  * An XPath function enumeration.
155  *
156  * <p>A list of XPath 1.0 functions:
157  * http://www.w3.org/TR/xpath/#corelib
158  *
159  * @enum {!Object}
160  */
161 xrx.xpath.FunctionCall.BuiltInFunc = {
162   BOOLEAN: xrx.xpath.FunctionCall.createFunc('boolean',
163       xrx.xpath.DataType.BOOLEAN, false, false, false,
164       function(ctx, expr) {
165         return expr.asBool(ctx);
166       }, 1),
167   CEILING: xrx.xpath.FunctionCall.createFunc('ceiling',
168       xrx.xpath.DataType.NUMBER, false, false, false,
169       function(ctx, expr) {
170         return Math.ceil(expr.asNumber(ctx));
171       }, 1),
172   CONCAT: xrx.xpath.FunctionCall.createFunc('concat',
173       xrx.xpath.DataType.STRING, false, false, false,
174       function(ctx, var_args) {
175         var exprs = goog.array.slice(arguments, 1);
176         return goog.array.reduce(exprs, function(prev, curr) {
177           return prev + curr.asString(ctx);
178         }, '');
179       }, 2, null),
180   CONTAINS: xrx.xpath.FunctionCall.createFunc('contains',
181       xrx.xpath.DataType.BOOLEAN, false, false, false,
182       function(ctx, expr1, expr2) {
183         return goog.string.contains(expr1.asString(ctx), expr2.asString(ctx));
184       }, 2),
185   COUNT: xrx.xpath.FunctionCall.createFunc('count',
186       xrx.xpath.DataType.NUMBER, false, false, false,
187       function(ctx, expr) {
188         return expr.evaluate(ctx).getLength();
189       }, 1, 1, true),
190   FALSE: xrx.xpath.FunctionCall.createFunc('false',
191       xrx.xpath.DataType.BOOLEAN, false, false, false,
192       function(ctx) {
193         return false;
194       }, 0),
195   FLOOR: xrx.xpath.FunctionCall.createFunc('floor',
196       xrx.xpath.DataType.NUMBER, false, false, false,
197       function(ctx, expr) {
198         return Math.floor(expr.asNumber(ctx));
199       }, 1),
200   ID: xrx.xpath.FunctionCall.createFunc('id',
201       xrx.xpath.DataType.NODESET, false, false, false,
202       function(ctx, expr) {
203         var ctxNode = ctx.getNode();
204         var doc = ctxNode.type() === xrx.node.DOCUMENT ? ctxNode :
205             ctxNode.ownerDocument;
206         var ids = expr.asString(ctx).split(/\s+/);
207         var nsArray = [];
208         goog.array.forEach(ids, function(id) {
209           var elem = idSingle(id);
210           if (elem && !goog.array.contains(nsArray, elem)) {
211             nsArray.push(elem);
212           }
213         });
214         nsArray.sort(goog.dom.compareNodeOrder);
215         var ns = new xrx.xpath.NodeSet();
216         goog.array.forEach(nsArray, function(n) {
217           ns.add(n);
218         });
219         return ns;
220 
221         function idSingle(id) {
222             return doc.getElementById(id);
223         }
224       }, 1),
225   LANG: xrx.xpath.FunctionCall.createFunc('lang',
226       xrx.xpath.DataType.BOOLEAN, false, false, false,
227       function(ctx, expr) {
228         // TODO(user): Fully implement this.
229         return false;
230       }, 1),
231   LAST: xrx.xpath.FunctionCall.createFunc('last',
232       xrx.xpath.DataType.NUMBER, true, false, false,
233       function(ctx) {
234         if (arguments.length != 1) {
235           throw Error('Function last expects ()');
236         }
237         return ctx.getLast();
238       }, 0),
239   LOCAL_NAME: xrx.xpath.FunctionCall.createFunc('local-name',
240       xrx.xpath.DataType.STRING, false, true, false,
241       function(ctx, opt_expr) {
242         var node = opt_expr ? opt_expr.evaluate(ctx).getFirst() : ctx.getNode();
243         return node ? node.nodeName.toLowerCase() : '';
244       }, 0, 1, true),
245   NAME: xrx.xpath.FunctionCall.createFunc('name',
246       xrx.xpath.DataType.STRING, false, true, false,
247       function(ctx, opt_expr) {
248         // TODO(user): Fully implement this.
249         var node = opt_expr ? opt_expr.evaluate(ctx).getFirst() : ctx.getNode();
250         return node ? node.nodeName.toLowerCase() : '';
251       }, 0, 1, true),
252   NAMESPACE_URI: xrx.xpath.FunctionCall.createFunc('namespace-uri',
253       xrx.xpath.DataType.STRING, true, false, false,
254       function(ctx, opt_expr) {
255         // TODO(user): Fully implement this.
256         return '';
257       }, 0, 1, true),
258   NORMALIZE_SPACE: xrx.xpath.FunctionCall.createFunc('normalize-space',
259       xrx.xpath.DataType.STRING, false, true, false,
260       function(ctx, opt_expr) {
261         var str = opt_expr ? opt_expr.asString(ctx) :
262             xrx.node.getValueAsString(ctx.getNode());
263         return goog.string.collapseWhitespace(str);
264       }, 0, 1),
265   NOT: xrx.xpath.FunctionCall.createFunc('not',
266       xrx.xpath.DataType.BOOLEAN, false, false, false,
267       function(ctx, expr) {
268         return !expr.asBool(ctx);
269       }, 1),
270   NUMBER: xrx.xpath.FunctionCall.createFunc('number',
271       xrx.xpath.DataType.NUMBER, false, true, false,
272       function(ctx, opt_expr) {
273         return opt_expr ? opt_expr.asNumber(ctx) :
274             xrx.node.getValueAsNumber(ctx.getNode());
275       }, 0, 1),
276   POSITION: xrx.xpath.FunctionCall.createFunc('position',
277       xrx.xpath.DataType.NUMBER, true, false, false,
278       function(ctx) {
279         return ctx.getPosition();
280       }, 0),
281   ROUND: xrx.xpath.FunctionCall.createFunc('round',
282       xrx.xpath.DataType.NUMBER, false, false, false,
283       function(ctx, expr) {
284         return Math.round(expr.asNumber(ctx));
285       }, 1),
286   STARTS_WITH: xrx.xpath.FunctionCall.createFunc('starts-with',
287       xrx.xpath.DataType.BOOLEAN, false, false, false,
288       function(ctx, expr1, expr2) {
289         return goog.string.startsWith(expr1.asString(ctx), expr2.asString(ctx));
290       }, 2),
291   STRING: xrx.xpath.FunctionCall.createFunc(
292       'string', xrx.xpath.DataType.STRING, false, true, false,
293       function(ctx, opt_expr) {
294         return opt_expr ? opt_expr.asString(ctx) :
295             xrx.node.getValueAsString(ctx.getNode());
296       }, 0, 1),
297   STRING_LENGTH: xrx.xpath.FunctionCall.createFunc('string-length',
298       xrx.xpath.DataType.NUMBER, false, true, false,
299       function(ctx, opt_expr) {
300         var str = opt_expr ? opt_expr.asString(ctx) :
301             xrx.node.getValueAsString(ctx.getNode());
302         return str.length;
303       }, 0, 1),
304   SUBSTRING: xrx.xpath.FunctionCall.createFunc('substring',
305       xrx.xpath.DataType.STRING, false, false, false,
306       function(ctx, expr1, expr2, opt_expr3) {
307         var startRaw = expr2.asNumber(ctx);
308         if (isNaN(startRaw) || startRaw == Infinity || startRaw == -Infinity) {
309           return '';
310         }
311         var lengthRaw = opt_expr3 ? opt_expr3.asNumber(ctx) : Infinity;
312         if (isNaN(lengthRaw) || lengthRaw === -Infinity) {
313           return '';
314         }
315 
316         // XPath indices are 1-based.
317         var startInt = Math.round(startRaw) - 1;
318         var start = Math.max(startInt, 0);
319         var str = expr1.asString(ctx);
320 
321         if (lengthRaw == Infinity) {
322           return str.substring(start);
323         } else {
324           var lengthInt = Math.round(lengthRaw);
325           // Length is from startInt, not start!
326           return str.substring(start, startInt + lengthInt);
327         }
328       }, 2, 3),
329   SUBSTRING_AFTER: xrx.xpath.FunctionCall.createFunc('substring-after',
330       xrx.xpath.DataType.STRING, false, false, false,
331       function(ctx, expr1, expr2) {
332         var str1 = expr1.asString(ctx);
333         var str2 = expr2.asString(ctx);
334         var str2Index = str1.indexOf(str2);
335         return str2Index == -1 ? '' : str1.substring(str2Index + str2.length);
336       }, 2),
337   SUBSTRING_BEFORE: xrx.xpath.FunctionCall.createFunc('substring-before',
338       xrx.xpath.DataType.STRING, false, false, false,
339       function(ctx, expr1, expr2) {
340         var str1 = expr1.asString(ctx);
341         var str2 = expr2.asString(ctx);
342         var str2Index = str1.indexOf(str2);
343         return str2Index == -1 ? '' : str1.substring(0, str2Index);
344       }, 2),
345   SUM: xrx.xpath.FunctionCall.createFunc('sum',
346       xrx.xpath.DataType.NUMBER, false, false, false,
347       function(ctx, expr) {
348         var ns = expr.evaluate(ctx);
349         var iter = ns.iterator();
350         var prev = 0;
351         for (var node = iter.next(); node; node = iter.next()) {
352           prev += xrx.node.getValueAsNumber(node);
353         }
354         return prev;
355       }, 1, 1, true),
356   TRANSLATE: xrx.xpath.FunctionCall.createFunc('translate',
357       xrx.xpath.DataType.STRING, false, false, false,
358       function(ctx, expr1, expr2, expr3) {
359         var str1 = expr1.asString(ctx);
360         var str2 = expr2.asString(ctx);
361         var str3 = expr3.asString(ctx);
362 
363         var map = [];
364         for (var i = 0; i < str2.length; i++) {
365           var ch = str2.charAt(i);
366           if (!(ch in map)) {
367             // If i >= str3.length, charAt will return the empty string.
368             map[ch] = str3.charAt(i);
369           }
370         }
371 
372         var translated = '';
373         for (var i = 0; i < str1.length; i++) {
374           var ch = str1.charAt(i);
375           translated += (ch in map) ? map[ch] : ch;
376         }
377         return translated;
378       }, 3),
379   TRUE: xrx.xpath.FunctionCall.createFunc(
380       'true', xrx.xpath.DataType.BOOLEAN, false, false, false,
381       function(ctx) {
382         return true;
383       }, 0)
384 };
385