1 /** 2 * Copyright (C) 2012 KO GmbH <copyright@kogmbh.com> 3 * 4 * @licstart 5 * This file is part of WebODF. 6 * 7 * WebODF is free software: you can redistribute it and/or modify it 8 * under the terms of the GNU Affero General Public License (GNU AGPL) 9 * as published by the Free Software Foundation, either version 3 of 10 * the License, or (at your option) any later version. 11 * 12 * WebODF is distributed in the hope that it will be useful, but 13 * WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU Affero General Public License for more details. 16 * 17 * You should have received a copy of the GNU Affero General Public License 18 * along with WebODF. If not, see <http://www.gnu.org/licenses/>. 19 * @licend 20 * 21 * @source: http://www.webodf.org/ 22 * @source: https://github.com/kogmbh/WebODF/ 23 */ 24 25 /*global Node, xmldom, XPathResult, runtime*/ 26 /*jslint emptyblock: true*/ 27 28 /** 29 * Iterator over nodes uses in the xpath implementation 30 * @class 31 * @interface 32 */ 33 xmldom.XPathIterator = function XPathIterator() {"use strict"; }; 34 /** 35 * @return {?Node} 36 */ 37 xmldom.XPathIterator.prototype.next = function () {"use strict"; }; 38 /** 39 * @return {undefined} 40 */ 41 xmldom.XPathIterator.prototype.reset = function () {"use strict"; }; 42 /*jslint emptyblock: false*/ 43 44 /** 45 * @typedef{{ 46 steps: !Array.<{ 47 predicates: !Array.<!xmldom.XPathAtom>, 48 location: string 49 }>, 50 value: * 51 }}*/ 52 xmldom.XPathAtom; 53 54 /** 55 * @return {!{getODFElementsWithXPath:function(!Element,!string,!function(string):?string):!Array.<!Element>}} 56 */ 57 function createXPathSingleton() { 58 "use strict"; 59 var /**@type{function(!xmldom.XPathIterator,!xmldom.XPathAtom,!Function):!xmldom.XPathIterator}*/ 60 createXPathPathIterator, 61 /**@type{function(string,number,!Array.<!xmldom.XPathAtom>):number}*/ 62 parsePredicates; 63 /** 64 * @param {!number} a 65 * @param {!number} b 66 * @param {!number} c 67 * @return {!boolean} 68 */ 69 function isSmallestPositive(a, b, c) { 70 return a !== -1 && (a < b || b === -1) && (a < c || c === -1); 71 } 72 /** 73 * Parse a subset of xpaths. 74 * The xpath predicates may contain xpaths. The location may be equated to 75 * a value. If a parsing error occurs, null is returned. 76 * @param {!string} xpath 77 * @param {!number} pos 78 * @param {!number} end 79 * @param {!Array} steps 80 * @return {number} 81 */ 82 function parseXPathStep(xpath, pos, end, steps) { 83 var location = "", 84 predicates = [], 85 /**@type{number}*/ 86 brapos = xpath.indexOf('[', pos), 87 /**@type{number}*/ 88 slapos = xpath.indexOf('/', pos), 89 /**@type{number}*/ 90 eqpos = xpath.indexOf('=', pos); 91 // parse the location 92 if (isSmallestPositive(slapos, brapos, eqpos)) { 93 location = xpath.substring(pos, slapos); 94 pos = slapos + 1; 95 } else if (isSmallestPositive(brapos, slapos, eqpos)) { 96 location = xpath.substring(pos, brapos); 97 pos = parsePredicates(xpath, brapos, predicates); 98 } else if (isSmallestPositive(eqpos, slapos, brapos)) { 99 location = xpath.substring(pos, eqpos); 100 pos = eqpos; 101 } else { 102 location = xpath.substring(pos, end); 103 pos = end; 104 } 105 steps.push({location: location, predicates: predicates}); 106 return pos; 107 } 108 /** 109 * @param {string} xpath 110 * @return {!xmldom.XPathAtom} 111 */ 112 function parseXPath(xpath) { 113 var /**@type{!Array.<{predicates: !Array.<!xmldom.XPathAtom>,location:string}>}*/ 114 steps = [], 115 p = 0, 116 end = xpath.length, 117 value; 118 while (p < end) { 119 p = parseXPathStep(xpath, p, end, steps); 120 if (p < end && xpath[p] === '=') { 121 value = xpath.substring(p + 1, end); 122 if (value.length > 2 && 123 (value[0] === '\'' || value[0] === '"')) { 124 value = value.slice(1, value.length - 1); 125 } else { 126 try { 127 value = parseInt(value, 10); 128 } catch (ignore) { 129 } 130 } 131 p = end; 132 } 133 } 134 return {steps: steps, value: value}; 135 } 136 /** 137 * @param {string} xpath 138 * @param {number} start 139 * @param {!Array.<!xmldom.XPathAtom>} predicates 140 * @return {number} 141 */ 142 parsePredicates = function parsePredicates(xpath, start, predicates) { 143 var pos = start, 144 l = xpath.length, 145 depth = 0; 146 while (pos < l) { 147 if (xpath[pos] === ']') { 148 depth -= 1; 149 if (depth <= 0) { 150 predicates.push(parseXPath(xpath.substring(start, pos))); 151 } 152 } else if (xpath[pos] === '[') { 153 if (depth <= 0) { 154 start = pos + 1; 155 } 156 depth += 1; 157 } 158 pos += 1; 159 } 160 return pos; 161 }; 162 /** 163 * @class 164 * @constructor 165 * @augments xmldom.XPathIterator 166 * @implements {xmldom.XPathIterator} 167 */ 168 function XPathNodeIterator() { 169 var /**@type{?Node}*/ 170 node = null, 171 /**@type{boolean}*/ 172 done = false; 173 /** 174 * @param {?Node} n 175 * @return {undefined} 176 */ 177 this.setNode = function setNode(n) { 178 node = n; 179 }; 180 /** 181 * @return {undefined} 182 */ 183 this.reset = function () { 184 done = false; 185 }; 186 /** 187 * @return {?Node} 188 */ 189 this.next = function next() { 190 var val = done ? null : node; 191 done = true; 192 return val; 193 }; 194 } 195 /** 196 * @class 197 * @constructor 198 * @augments xmldom.XPathIterator 199 * @implements {xmldom.XPathIterator} 200 * @param {xmldom.XPathIterator} it 201 * @param {!string} namespace 202 * @param {!string} localName 203 */ 204 function AttributeIterator(it, namespace, localName) { 205 this.reset = function reset() { 206 it.reset(); 207 }; 208 /** 209 * @return {?Node} 210 */ 211 this.next = function next() { 212 var node = it.next(); 213 while (node) { 214 if (node.nodeType === Node.ELEMENT_NODE) { 215 node = /**@type{!Element}*/(node).getAttributeNodeNS( 216 namespace, 217 localName 218 ); 219 } 220 if (node) { 221 return node; 222 } 223 node = it.next(); 224 } 225 return node; 226 }; 227 } 228 /** 229 * @class 230 * @constructor 231 * @augments xmldom.XPathIterator 232 * @implements {xmldom.XPathIterator} 233 * @param {xmldom.XPathIterator} it 234 * @param {boolean} recurse 235 */ 236 function AllChildElementIterator(it, recurse) { 237 var root = it.next(), 238 node = null; 239 this.reset = function reset() { 240 it.reset(); 241 root = it.next(); 242 node = null; 243 }; 244 this.next = function next() { 245 while (root) { 246 if (node) { 247 if (recurse && node.firstChild) { 248 node = node.firstChild; 249 } else { 250 while (!node.nextSibling && node !== root) { 251 node = node.parentNode; 252 } 253 if (node === root) { 254 root = it.next(); 255 } else { 256 node = node.nextSibling; 257 } 258 } 259 } else { 260 do { 261 // node = (recurse) ?root :root.firstChild; 262 node = root.firstChild; 263 if (!node) { 264 root = it.next(); 265 } 266 } while (root && !node); 267 } 268 if (node && node.nodeType === Node.ELEMENT_NODE) { 269 return node; 270 } 271 } 272 return null; 273 }; 274 } 275 /** 276 * @class 277 * @constructor 278 * @augments xmldom.XPathIterator 279 * @implements {xmldom.XPathIterator} 280 * @param {xmldom.XPathIterator} it 281 * @param {function(Node):boolean} condition 282 */ 283 function ConditionIterator(it, condition) { 284 this.reset = function reset() { 285 it.reset(); 286 }; 287 this.next = function next() { 288 var n = it.next(); 289 while (n && !condition(n)) { 290 n = it.next(); 291 } 292 return n; 293 }; 294 } 295 /** 296 * @param {xmldom.XPathIterator} it 297 * @param {string} name 298 * @param {function(string):?string} namespaceResolver 299 * @return {!ConditionIterator} 300 */ 301 function createNodenameFilter(it, name, namespaceResolver) { 302 var s = name.split(':', 2), 303 namespace = namespaceResolver(s[0]), 304 localName = s[1]; 305 return new ConditionIterator(it, function (node) { 306 return node.localName === localName && 307 node.namespaceURI === namespace; 308 }); 309 } 310 /** 311 * @param {xmldom.XPathIterator} it 312 * @param {!xmldom.XPathAtom} p 313 * @param {function(string):?string} namespaceResolver 314 * @return {!ConditionIterator} 315 */ 316 function createPredicateFilteredIterator(it, p, namespaceResolver) { 317 var nit = new XPathNodeIterator(), 318 pit = createXPathPathIterator(nit, p, namespaceResolver), 319 value = p.value; 320 if (value === undefined) { 321 return new ConditionIterator(it, function (node) { 322 nit.setNode(node); 323 pit.reset(); 324 return pit.next() !== null; 325 }); 326 } 327 return new ConditionIterator(it, function (node) { 328 nit.setNode(node); 329 pit.reset(); 330 var n = pit.next(); 331 // todo: distinuish between number and string 332 return n ? n.nodeValue === value : false; 333 }); 334 } 335 /** 336 * @param {!Array.<!xmldom.XPathAtom>} p 337 * @param {!number} i 338 * @return {!xmldom.XPathAtom} 339 */ 340 function item(p, i) { 341 return p[i]; 342 } 343 /** 344 * @param {!xmldom.XPathIterator} it 345 * @param {!xmldom.XPathAtom} xpath 346 * @param {!function(string):?string} namespaceResolver 347 * @return {!xmldom.XPathIterator} 348 */ 349 createXPathPathIterator = function createXPathPathIterator(it, xpath, 350 namespaceResolver) { 351 var i, j, step, location, s, p, ns; 352 for (i = 0; i < xpath.steps.length; i += 1) { 353 step = xpath.steps[i]; 354 location = step.location; 355 if (location === "") { 356 it = new AllChildElementIterator(it, false); 357 } else if (location[0] === '@') { 358 s = location.substr(1).split(":", 2); 359 ns = namespaceResolver(s[0]); 360 if (!ns) { 361 throw "No namespace associated with the prefix " + s[0]; 362 } 363 it = new AttributeIterator(it, ns, s[1]); 364 } else if (location !== ".") { 365 it = new AllChildElementIterator(it, false); 366 if (location.indexOf(":") !== -1) { 367 it = createNodenameFilter(it, location, namespaceResolver); 368 } 369 } 370 for (j = 0; j < step.predicates.length; j += 1) { 371 p = item(step.predicates, j); 372 it = createPredicateFilteredIterator(it, p, namespaceResolver); 373 } 374 } 375 return it; 376 }; 377 /** 378 * @param {!Element} node 379 * @param {!string} xpath 380 * @param {!function(string):?string} namespaceResolver 381 * @return {!Array.<Element>} 382 */ 383 function fallback(node, xpath, namespaceResolver) { 384 var it = new XPathNodeIterator(), 385 i, 386 nodelist, 387 parsedXPath; 388 it.setNode(node); 389 parsedXPath = parseXPath(xpath); 390 it = createXPathPathIterator(it, parsedXPath, namespaceResolver); 391 nodelist = []; 392 i = it.next(); 393 while (i) { 394 nodelist.push(i); 395 i = it.next(); 396 } 397 return nodelist; 398 } 399 /** 400 * @param {!Element} node 401 * @param {!string} xpath 402 * @param {!function(string):?string} namespaceResolver 403 * @return {!Array.<!Element>} 404 */ 405 function getODFElementsWithXPath(node, xpath, namespaceResolver) { 406 var doc = node.ownerDocument, 407 nodes, 408 elements = [], 409 n = null; 410 if (!doc || typeof doc.evaluate !== 'function') { 411 elements = fallback(node, xpath, namespaceResolver); 412 } else { 413 nodes = doc.evaluate(xpath, node, namespaceResolver, 414 XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null); 415 n = nodes.iterateNext(); 416 while (n !== null) { 417 if (n.nodeType === Node.ELEMENT_NODE) { 418 elements.push(n); 419 } 420 n = nodes.iterateNext(); 421 } 422 } 423 return elements; 424 } 425 return { 426 getODFElementsWithXPath: getODFElementsWithXPath 427 }; 428 } 429 /** 430 * Wrapper for XPath functions 431 * @const 432 * @type {!{getODFElementsWithXPath:function(!Element,!string,!function(string):?string):!Array.<!Element>}} 433 */ 434 xmldom.XPath = createXPathSingleton(); 435