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