/testing/selenium-core/xpath/util.js
JavaScript | 549 lines | 384 code | 69 blank | 96 comment | 118 complexity | d41835e4919b4163b6e0644850319e3a MD5 | raw file
1// Copyright 2005 Google 2// 3// Author: Steffen Meschkat <mesch@google.com> 4// 5// Miscellaneous utility and placeholder functions. 6 7// Dummy implmentation for the logging functions. Replace by something 8// useful when you want to debug. 9function xpathLog(msg) {}; 10function xsltLog(msg) {}; 11function xsltLogXml(msg) {}; 12 13var ajaxsltIsIE6 = navigator.appVersion.match(/MSIE 6.0/); 14 15// Throws an exception if false. 16function assert(b) { 17 if (!b) { 18 throw "Assertion failed"; 19 } 20} 21 22// Splits a string s at all occurrences of character c. This is like 23// the split() method of the string object, but IE omits empty 24// strings, which violates the invariant (s.split(x).join(x) == s). 25function stringSplit(s, c) { 26 var a = s.indexOf(c); 27 if (a == -1) { 28 return [ s ]; 29 } 30 var parts = []; 31 parts.push(s.substr(0,a)); 32 while (a != -1) { 33 var a1 = s.indexOf(c, a + 1); 34 if (a1 != -1) { 35 parts.push(s.substr(a + 1, a1 - a - 1)); 36 } else { 37 parts.push(s.substr(a + 1)); 38 } 39 a = a1; 40 } 41 return parts; 42} 43 44// The following function does what document.importNode(node, true) 45// would do for us here; however that method is broken in Safari/1.3, 46// so we have to emulate it. 47function xmlImportNode(doc, node) { 48 if (node.nodeType == DOM_TEXT_NODE) { 49 return domCreateTextNode(doc, node.nodeValue); 50 51 } else if (node.nodeType == DOM_CDATA_SECTION_NODE) { 52 return domCreateCDATASection(doc, node.nodeValue); 53 54 } else if (node.nodeType == DOM_ELEMENT_NODE) { 55 var newNode = domCreateElement(doc, node.nodeName); 56 for (var i = 0; i < node.attributes.length; ++i) { 57 var an = node.attributes[i]; 58 var name = an.nodeName; 59 var value = an.nodeValue; 60 domSetAttribute(newNode, name, value); 61 } 62 63 for (var c = node.firstChild; c; c = c.nextSibling) { 64 var cn = arguments.callee(doc, c); 65 domAppendChild(newNode, cn); 66 } 67 68 return newNode; 69 70 } else { 71 return domCreateComment(doc, node.nodeName); 72 } 73} 74 75// A set data structure. It can also be used as a map (i.e. the keys 76// can have values other than 1), but we don't call it map because it 77// would be ambiguous in this context. Also, the map is iterable, so 78// we can use it to replace for-in loops over core javascript Objects. 79// For-in iteration breaks when Object.prototype is modified, which 80// some clients of the maps API do. 81// 82// NOTE(mesch): The set keys by the string value of its element, NOT 83// by the typed value. In particular, objects can't be used as keys. 84// 85// @constructor 86function Set() { 87 this.keys = []; 88} 89 90Set.prototype.size = function() { 91 return this.keys.length; 92} 93 94// Adds the entry to the set, ignoring if it is present. 95Set.prototype.add = function(key, opt_value) { 96 var value = opt_value || 1; 97 if (!this.contains(key)) { 98 this[':' + key] = value; 99 this.keys.push(key); 100 } 101} 102 103// Sets the entry in the set, adding if it is not yet present. 104Set.prototype.set = function(key, opt_value) { 105 var value = opt_value || 1; 106 if (!this.contains(key)) { 107 this[':' + key] = value; 108 this.keys.push(key); 109 } else { 110 this[':' + key] = value; 111 } 112} 113 114// Increments the key's value by 1. This works around the fact that 115// numbers are always passed by value, never by reference, so that we 116// can't increment the value returned by get(), or the iterator 117// argument. Sets the key's value to 1 if it doesn't exist yet. 118Set.prototype.inc = function(key) { 119 if (!this.contains(key)) { 120 this[':' + key] = 1; 121 this.keys.push(key); 122 } else { 123 this[':' + key]++; 124 } 125} 126 127Set.prototype.get = function(key) { 128 if (this.contains(key)) { 129 return this[':' + key]; 130 } else { 131 var undefined; 132 return undefined; 133 } 134} 135 136// Removes the entry from the set. 137Set.prototype.remove = function(key) { 138 if (this.contains(key)) { 139 delete this[':' + key]; 140 removeFromArray(this.keys, key, true); 141 } 142} 143 144// Tests if an entry is in the set. 145Set.prototype.contains = function(entry) { 146 return typeof this[':' + entry] != 'undefined'; 147} 148 149// Gets a list of values in the set. 150Set.prototype.items = function() { 151 var list = []; 152 for (var i = 0; i < this.keys.length; ++i) { 153 var k = this.keys[i]; 154 var v = this[':' + k]; 155 list.push(v); 156 } 157 return list; 158} 159 160 161// Invokes function f for every key value pair in the set as a method 162// of the set. 163Set.prototype.map = function(f) { 164 for (var i = 0; i < this.keys.length; ++i) { 165 var k = this.keys[i]; 166 f.call(this, k, this[':' + k]); 167 } 168} 169 170Set.prototype.clear = function() { 171 for (var i = 0; i < this.keys.length; ++i) { 172 delete this[':' + this.keys[i]]; 173 } 174 this.keys.length = 0; 175} 176 177 178// Applies the given function to each element of the array, preserving 179// this, and passing the index. 180function mapExec(array, func) { 181 for (var i = 0; i < array.length; ++i) { 182 func.call(this, array[i], i); 183 } 184} 185 186// Returns an array that contains the return value of the given 187// function applied to every element of the input array. 188function mapExpr(array, func) { 189 var ret = []; 190 for (var i = 0; i < array.length; ++i) { 191 ret.push(func(array[i])); 192 } 193 return ret; 194}; 195 196// Reverses the given array in place. 197function reverseInplace(array) { 198 for (var i = 0; i < array.length / 2; ++i) { 199 var h = array[i]; 200 var ii = array.length - i - 1; 201 array[i] = array[ii]; 202 array[ii] = h; 203 } 204} 205 206// Removes value from array. Returns the number of instances of value 207// that were removed from array. 208function removeFromArray(array, value, opt_notype) { 209 var shift = 0; 210 for (var i = 0; i < array.length; ++i) { 211 if (array[i] === value || (opt_notype && array[i] == value)) { 212 array.splice(i--, 1); 213 shift++; 214 } 215 } 216 return shift; 217} 218 219// Shallow-copies an array to the end of another array 220// Basically Array.concat, but works with other non-array collections 221function copyArray(dst, src) { 222 if (!src) return; 223 var dstLength = dst.length; 224 for (var i = src.length - 1; i >= 0; --i) { 225 dst[i+dstLength] = src[i]; 226 } 227} 228 229/** 230 * This is an optimization for copying attribute lists in IE. IE includes many 231 * extraneous properties in its DOM attribute lists, which take require 232 * significant extra processing when evaluating attribute steps. With this 233 * function, we ignore any such attributes that has an empty string value. 234 */ 235function copyArrayIgnoringAttributesWithoutValue(dst, src) 236{ 237 if (!src) return; 238 for (var i = src.length - 1; i >= 0; --i) { 239 // this test will pass so long as the attribute has a non-empty string 240 // value, even if that value is "false", "0", "undefined", etc. 241 if (src[i].nodeValue) { 242 dst.push(src[i]); 243 } 244 } 245} 246 247// Returns the text value of a node; for nodes without children this 248// is the nodeValue, for nodes with children this is the concatenation 249// of the value of all children. Browser-specific optimizations are used by 250// default; they can be disabled by passing "true" in as the second parameter. 251function xmlValue(node, disallowBrowserSpecificOptimization) { 252 if (!node) { 253 return ''; 254 } 255 256 var ret = ''; 257 if (node.nodeType == DOM_TEXT_NODE || 258 node.nodeType == DOM_CDATA_SECTION_NODE) { 259 ret += node.nodeValue; 260 261 } else if (node.nodeType == DOM_ATTRIBUTE_NODE) { 262 if (ajaxsltIsIE6) { 263 ret += xmlValueIE6Hack(node); 264 } else { 265 ret += node.nodeValue; 266 } 267 } else if (node.nodeType == DOM_ELEMENT_NODE || 268 node.nodeType == DOM_DOCUMENT_NODE || 269 node.nodeType == DOM_DOCUMENT_FRAGMENT_NODE) { 270 if (!disallowBrowserSpecificOptimization) { 271 // IE, Safari, Opera, and friends 272 var innerText = node.innerText; 273 if (innerText != undefined) { 274 return innerText; 275 } 276 // Firefox 277 var textContent = node.textContent; 278 if (textContent != undefined) { 279 return textContent; 280 } 281 } 282 // pobrecito! 283 var len = node.childNodes.length; 284 for (var i = 0; i < len; ++i) { 285 ret += arguments.callee(node.childNodes[i]); 286 } 287 } 288 return ret; 289} 290 291function xmlValueIE6Hack(node) { 292 // Issue 19, IE6 mangles href attribute when it's a javascript: url 293 var nodeName = node.nodeName; 294 var nodeValue = node.nodeValue; 295 if (nodeName.length != 4) return nodeValue; 296 if (!/^href$/i.test(nodeName)) return nodeValue; 297 if (!/^javascript:/.test(nodeValue)) return nodeValue; 298 return unescape(nodeValue); 299} 300 301// Returns the representation of a node as XML text. 302function xmlText(node, opt_cdata) { 303 var buf = []; 304 xmlTextR(node, buf, opt_cdata); 305 return buf.join(''); 306} 307 308function xmlTextR(node, buf, cdata) { 309 if (node.nodeType == DOM_TEXT_NODE) { 310 buf.push(xmlEscapeText(node.nodeValue)); 311 312 } else if (node.nodeType == DOM_CDATA_SECTION_NODE) { 313 if (cdata) { 314 buf.push(node.nodeValue); 315 } else { 316 buf.push('<![CDATA[' + node.nodeValue + ']]>'); 317 } 318 319 } else if (node.nodeType == DOM_COMMENT_NODE) { 320 buf.push('<!--' + node.nodeValue + '-->'); 321 322 } else if (node.nodeType == DOM_ELEMENT_NODE) { 323 buf.push('<' + xmlFullNodeName(node)); 324 for (var i = 0; i < node.attributes.length; ++i) { 325 var a = node.attributes[i]; 326 if (a && a.nodeName && a.nodeValue) { 327 buf.push(' ' + xmlFullNodeName(a) + '="' + 328 xmlEscapeAttr(a.nodeValue) + '"'); 329 } 330 } 331 332 if (node.childNodes.length == 0) { 333 buf.push('/>'); 334 } else { 335 buf.push('>'); 336 for (var i = 0; i < node.childNodes.length; ++i) { 337 arguments.callee(node.childNodes[i], buf, cdata); 338 } 339 buf.push('</' + xmlFullNodeName(node) + '>'); 340 } 341 342 } else if (node.nodeType == DOM_DOCUMENT_NODE || 343 node.nodeType == DOM_DOCUMENT_FRAGMENT_NODE) { 344 for (var i = 0; i < node.childNodes.length; ++i) { 345 arguments.callee(node.childNodes[i], buf, cdata); 346 } 347 } 348} 349 350function xmlFullNodeName(n) { 351 if (n.prefix && n.nodeName.indexOf(n.prefix + ':') != 0) { 352 return n.prefix + ':' + n.nodeName; 353 } else { 354 return n.nodeName; 355 } 356} 357 358// Escape XML special markup chracters: tag delimiter < > and entity 359// reference start delimiter &. The escaped string can be used in XML 360// text portions (i.e. between tags). 361function xmlEscapeText(s) { 362 return ('' + s).replace(/&/g, '&').replace(/</g, '<'). 363 replace(/>/g, '>'); 364} 365 366// Escape XML special markup characters: tag delimiter < > entity 367// reference start delimiter & and quotes ". The escaped string can be 368// used in double quoted XML attribute value portions (i.e. in 369// attributes within start tags). 370function xmlEscapeAttr(s) { 371 return xmlEscapeText(s).replace(/\"/g, '"'); 372} 373 374// Escape markup in XML text, but don't touch entity references. The 375// escaped string can be used as XML text (i.e. between tags). 376function xmlEscapeTags(s) { 377 return s.replace(/</g, '<').replace(/>/g, '>'); 378} 379 380/** 381 * Wrapper function to access the owner document uniformly for document 382 * and other nodes: for the document node, the owner document is the 383 * node itself, for all others it's the ownerDocument property. 384 * 385 * @param {Node} node 386 * @return {Document} 387 */ 388function xmlOwnerDocument(node) { 389 if (node.nodeType == DOM_DOCUMENT_NODE) { 390 return node; 391 } else { 392 return node.ownerDocument; 393 } 394} 395 396// Wrapper around DOM methods so we can condense their invocations. 397function domGetAttribute(node, name) { 398 return node.getAttribute(name); 399} 400 401function domSetAttribute(node, name, value) { 402 return node.setAttribute(name, value); 403} 404 405function domRemoveAttribute(node, name) { 406 return node.removeAttribute(name); 407} 408 409function domAppendChild(node, child) { 410 return node.appendChild(child); 411} 412 413function domRemoveChild(node, child) { 414 return node.removeChild(child); 415} 416 417function domReplaceChild(node, newChild, oldChild) { 418 return node.replaceChild(newChild, oldChild); 419} 420 421function domInsertBefore(node, newChild, oldChild) { 422 return node.insertBefore(newChild, oldChild); 423} 424 425function domRemoveNode(node) { 426 return domRemoveChild(node.parentNode, node); 427} 428 429function domCreateTextNode(doc, text) { 430 return doc.createTextNode(text); 431} 432 433function domCreateElement(doc, name) { 434 return doc.createElement(name); 435} 436 437function domCreateAttribute(doc, name) { 438 return doc.createAttribute(name); 439} 440 441function domCreateCDATASection(doc, data) { 442 return doc.createCDATASection(data); 443} 444 445function domCreateComment(doc, text) { 446 return doc.createComment(text); 447} 448 449function domCreateDocumentFragment(doc) { 450 return doc.createDocumentFragment(); 451} 452 453function domGetElementById(doc, id) { 454 return doc.getElementById(id); 455} 456 457// Same for window methods. 458function windowSetInterval(win, fun, time) { 459 return win.setInterval(fun, time); 460} 461 462function windowClearInterval(win, id) { 463 return win.clearInterval(id); 464} 465 466/** 467 * Escape the special regular expression characters when the regular expression 468 * is specified as a string. 469 * 470 * Based on: http://simonwillison.net/2006/Jan/20/escape/ 471 */ 472RegExp.escape = (function() { 473 var specials = [ 474 '/', '.', '*', '+', '?', '|', '^', '$', 475 '(', ')', '[', ']', '{', '}', '\\' 476 ]; 477 478 var sRE = new RegExp( 479 '(\\' + specials.join('|\\') + ')', 'g' 480 ); 481 482 return function(text) { 483 return text.replace(sRE, '\\$1'); 484 } 485})(); 486 487/** 488 * Determines whether a predicate expression contains a "positional selector". 489 * A positional selector filters nodes from the nodelist input based on their 490 * position within that list. When such selectors are encountered, the 491 * evaluation of the predicate cannot be depth-first, because the positional 492 * selector may be based on the result of evaluating predicates that precede 493 * it. 494 */ 495function predicateExprHasPositionalSelector(expr, isRecursiveCall) { 496 if (!expr) { 497 return false; 498 } 499 if (!isRecursiveCall && exprReturnsNumberValue(expr)) { 500 // this is a "proximity position"-based predicate 501 return true; 502 } 503 if (expr instanceof FunctionCallExpr) { 504 var value = expr.name.value; 505 return (value == 'last' || value == 'position'); 506 } 507 if (expr instanceof BinaryExpr) { 508 return ( 509 predicateExprHasPositionalSelector(expr.expr1, true) || 510 predicateExprHasPositionalSelector(expr.expr2, true)); 511 } 512 return false; 513} 514 515function exprReturnsNumberValue(expr) { 516 if (expr instanceof FunctionCallExpr) { 517 var isMember = { 518 last: true 519 , position: true 520 , count: true 521 , 'string-length': true 522 , number: true 523 , sum: true 524 , floor: true 525 , ceiling: true 526 , round: true 527 }; 528 return isMember[expr.name.value]; 529 } 530 else if (expr instanceof UnaryMinusExpr) { 531 return true; 532 } 533 else if (expr instanceof BinaryExpr) { 534 var isMember = { 535 '+': true 536 , '-': true 537 , '*': true 538 , mod: true 539 , div: true 540 }; 541 return isMember[expr.op.value]; 542 } 543 else if (expr instanceof NumberExpr) { 544 return true; 545 } 546 return false; 547} 548 549