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