|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 var EXPORTED_SYMBOLS = ["ID", "Link", "XPath", "Selector", "Name", "Anon", "AnonXPath", |
|
6 "Lookup", "_byID", "_byName", "_byAttrib", "_byAnonAttrib", |
|
7 ]; |
|
8 |
|
9 const Cc = Components.classes; |
|
10 const Ci = Components.interfaces; |
|
11 const Cu = Components.utils; |
|
12 |
|
13 Cu.import("resource://gre/modules/Services.jsm"); |
|
14 |
|
15 var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils); |
|
16 var strings = {}; Cu.import('resource://mozmill/stdlib/strings.js', strings); |
|
17 var arrays = {}; Cu.import('resource://mozmill/stdlib/arrays.js', arrays); |
|
18 var json2 = {}; Cu.import('resource://mozmill/stdlib/json2.js', json2); |
|
19 var withs = {}; Cu.import('resource://mozmill/stdlib/withs.js', withs); |
|
20 var dom = {}; Cu.import('resource://mozmill/stdlib/dom.js', dom); |
|
21 var objects = {}; Cu.import('resource://mozmill/stdlib/objects.js', objects); |
|
22 |
|
23 var countQuotes = function (str) { |
|
24 var count = 0; |
|
25 var i = 0; |
|
26 |
|
27 while (i < str.length) { |
|
28 i = str.indexOf('"', i); |
|
29 if (i != -1) { |
|
30 count++; |
|
31 i++; |
|
32 } else { |
|
33 break; |
|
34 } |
|
35 } |
|
36 |
|
37 return count; |
|
38 }; |
|
39 |
|
40 /** |
|
41 * smartSplit() |
|
42 * |
|
43 * Takes a lookup string as input and returns |
|
44 * a list of each node in the string |
|
45 */ |
|
46 var smartSplit = function (str) { |
|
47 // Ensure we have an even number of quotes |
|
48 if (countQuotes(str) % 2 != 0) { |
|
49 throw new Error ("Invalid Lookup Expression"); |
|
50 } |
|
51 |
|
52 /** |
|
53 * This regex matches a single "node" in a lookup string. |
|
54 * In otherwords, it matches the part between the two '/'s |
|
55 * |
|
56 * Regex Explanation: |
|
57 * \/ - start matching at the first forward slash |
|
58 * ([^\/"]*"[^"]*")* - match as many pairs of quotes as possible until we hit a slash (ignore slashes inside quotes) |
|
59 * [^\/]* - match the remainder of text outside of last quote but before next slash |
|
60 */ |
|
61 var re = /\/([^\/"]*"[^"]*")*[^\/]*/g |
|
62 var ret = [] |
|
63 var match = re.exec(str); |
|
64 |
|
65 while (match != null) { |
|
66 ret.push(match[0].replace(/^\//, "")); |
|
67 match = re.exec(str); |
|
68 } |
|
69 |
|
70 return ret; |
|
71 }; |
|
72 |
|
73 /** |
|
74 * defaultDocuments() |
|
75 * |
|
76 * Returns a list of default documents in which to search for elements |
|
77 * if no document is provided |
|
78 */ |
|
79 function defaultDocuments() { |
|
80 var win = Services.wm.getMostRecentWindow("navigator:browser"); |
|
81 |
|
82 return [ |
|
83 win.document, |
|
84 utils.getBrowserObject(win).selectedBrowser.contentWindow.document |
|
85 ]; |
|
86 }; |
|
87 |
|
88 /** |
|
89 * nodeSearch() |
|
90 * |
|
91 * Takes an optional document, callback and locator string |
|
92 * Returns a handle to the located element or null |
|
93 */ |
|
94 function nodeSearch(doc, func, string) { |
|
95 if (doc != undefined) { |
|
96 var documents = [doc]; |
|
97 } else { |
|
98 var documents = defaultDocuments(); |
|
99 } |
|
100 |
|
101 var e = null; |
|
102 var element = null; |
|
103 |
|
104 //inline function to recursively find the element in the DOM, cross frame. |
|
105 var search = function (win, func, string) { |
|
106 if (win == null) { |
|
107 return; |
|
108 } |
|
109 |
|
110 //do the lookup in the current window |
|
111 element = func.call(win, string); |
|
112 |
|
113 if (!element || (element.length == 0)) { |
|
114 var frames = win.frames; |
|
115 for (var i = 0; i < frames.length; i++) { |
|
116 search(frames[i], func, string); |
|
117 } |
|
118 } else { |
|
119 e = element; |
|
120 } |
|
121 }; |
|
122 |
|
123 for (var i = 0; i < documents.length; ++i) { |
|
124 var win = documents[i].defaultView; |
|
125 search(win, func, string); |
|
126 if (e) { |
|
127 break; |
|
128 } |
|
129 } |
|
130 |
|
131 return e; |
|
132 }; |
|
133 |
|
134 /** |
|
135 * Selector() |
|
136 * |
|
137 * Finds an element by selector string |
|
138 */ |
|
139 function Selector(_document, selector, index) { |
|
140 if (selector == undefined) { |
|
141 throw new Error('Selector constructor did not recieve enough arguments.'); |
|
142 } |
|
143 |
|
144 this.selector = selector; |
|
145 |
|
146 this.getNodeForDocument = function (s) { |
|
147 return this.document.querySelectorAll(s); |
|
148 }; |
|
149 |
|
150 var nodes = nodeSearch(_document, this.getNodeForDocument, this.selector); |
|
151 |
|
152 return nodes ? nodes[index || 0] : null; |
|
153 }; |
|
154 |
|
155 /** |
|
156 * ID() |
|
157 * |
|
158 * Finds an element by ID |
|
159 */ |
|
160 function ID(_document, nodeID) { |
|
161 if (nodeID == undefined) { |
|
162 throw new Error('ID constructor did not recieve enough arguments.'); |
|
163 } |
|
164 |
|
165 this.getNodeForDocument = function (nodeID) { |
|
166 return this.document.getElementById(nodeID); |
|
167 }; |
|
168 |
|
169 return nodeSearch(_document, this.getNodeForDocument, nodeID); |
|
170 }; |
|
171 |
|
172 /** |
|
173 * Link() |
|
174 * |
|
175 * Finds a link by innerHTML |
|
176 */ |
|
177 function Link(_document, linkName) { |
|
178 if (linkName == undefined) { |
|
179 throw new Error('Link constructor did not recieve enough arguments.'); |
|
180 } |
|
181 |
|
182 this.getNodeForDocument = function (linkName) { |
|
183 var getText = function (el) { |
|
184 var text = ""; |
|
185 |
|
186 if (el.nodeType == 3) { //textNode |
|
187 if (el.data != undefined) { |
|
188 text = el.data; |
|
189 } else { |
|
190 text = el.innerHTML; |
|
191 } |
|
192 |
|
193 text = text.replace(/n|r|t/g, " "); |
|
194 } |
|
195 else if (el.nodeType == 1) { //elementNode |
|
196 for (var i = 0; i < el.childNodes.length; i++) { |
|
197 var child = el.childNodes.item(i); |
|
198 text += getText(child); |
|
199 } |
|
200 |
|
201 if (el.tagName == "P" || el.tagName == "BR" || |
|
202 el.tagName == "HR" || el.tagName == "DIV") { |
|
203 text += "\n"; |
|
204 } |
|
205 } |
|
206 |
|
207 return text; |
|
208 }; |
|
209 |
|
210 //sometimes the windows won't have this function |
|
211 try { |
|
212 var links = this.document.getElementsByTagName('a'); |
|
213 } catch (e) { |
|
214 // ADD LOG LINE mresults.write('Error: '+ e, 'lightred'); |
|
215 } |
|
216 |
|
217 for (var i = 0; i < links.length; i++) { |
|
218 var el = links[i]; |
|
219 //if (getText(el).indexOf(this.linkName) != -1) { |
|
220 if (el.innerHTML.indexOf(linkName) != -1) { |
|
221 return el; |
|
222 } |
|
223 } |
|
224 |
|
225 return null; |
|
226 }; |
|
227 |
|
228 return nodeSearch(_document, this.getNodeForDocument, linkName); |
|
229 }; |
|
230 |
|
231 /** |
|
232 * XPath() |
|
233 * |
|
234 * Finds an element by XPath |
|
235 */ |
|
236 function XPath(_document, expr) { |
|
237 if (expr == undefined) { |
|
238 throw new Error('XPath constructor did not recieve enough arguments.'); |
|
239 } |
|
240 |
|
241 this.getNodeForDocument = function (s) { |
|
242 var aNode = this.document; |
|
243 var aExpr = s; |
|
244 var xpe = null; |
|
245 |
|
246 if (this.document.defaultView == null) { |
|
247 xpe = new getMethodInWindows('XPathEvaluator')(); |
|
248 } else { |
|
249 xpe = new this.document.defaultView.XPathEvaluator(); |
|
250 } |
|
251 |
|
252 var nsResolver = xpe.createNSResolver(aNode.ownerDocument == null ? aNode.documentElement |
|
253 : aNode.ownerDocument.documentElement); |
|
254 var result = xpe.evaluate(aExpr, aNode, nsResolver, 0, null); |
|
255 var found = []; |
|
256 var res; |
|
257 |
|
258 while (res = result.iterateNext()) { |
|
259 found.push(res); |
|
260 } |
|
261 |
|
262 return found[0]; |
|
263 }; |
|
264 |
|
265 return nodeSearch(_document, this.getNodeForDocument, expr); |
|
266 }; |
|
267 |
|
268 /** |
|
269 * Name() |
|
270 * |
|
271 * Finds an element by Name |
|
272 */ |
|
273 function Name(_document, nName) { |
|
274 if (nName == undefined) { |
|
275 throw new Error('Name constructor did not recieve enough arguments.'); |
|
276 } |
|
277 |
|
278 this.getNodeForDocument = function (s) { |
|
279 try{ |
|
280 var els = this.document.getElementsByName(s); |
|
281 if (els.length > 0) { |
|
282 return els[0]; |
|
283 } |
|
284 } catch (e) { |
|
285 } |
|
286 |
|
287 return null; |
|
288 }; |
|
289 |
|
290 return nodeSearch(_document, this.getNodeForDocument, nName); |
|
291 }; |
|
292 |
|
293 |
|
294 var _returnResult = function (results) { |
|
295 if (results.length == 0) { |
|
296 return null |
|
297 } |
|
298 else if (results.length == 1) { |
|
299 return results[0]; |
|
300 } else { |
|
301 return results; |
|
302 } |
|
303 } |
|
304 |
|
305 var _forChildren = function (element, name, value) { |
|
306 var results = []; |
|
307 var nodes = [e for each (e in element.childNodes) if (e)] |
|
308 |
|
309 for (var i in nodes) { |
|
310 var n = nodes[i]; |
|
311 if (n[name] == value) { |
|
312 results.push(n); |
|
313 } |
|
314 } |
|
315 |
|
316 return results; |
|
317 } |
|
318 |
|
319 var _forAnonChildren = function (_document, element, name, value) { |
|
320 var results = []; |
|
321 var nodes = [e for each (e in _document.getAnoymousNodes(element)) if (e)]; |
|
322 |
|
323 for (var i in nodes ) { |
|
324 var n = nodes[i]; |
|
325 if (n[name] == value) { |
|
326 results.push(n); |
|
327 } |
|
328 } |
|
329 |
|
330 return results; |
|
331 } |
|
332 |
|
333 var _byID = function (_document, parent, value) { |
|
334 return _returnResult(_forChildren(parent, 'id', value)); |
|
335 } |
|
336 |
|
337 var _byName = function (_document, parent, value) { |
|
338 return _returnResult(_forChildren(parent, 'tagName', value)); |
|
339 } |
|
340 |
|
341 var _byAttrib = function (parent, attributes) { |
|
342 var results = []; |
|
343 var nodes = parent.childNodes; |
|
344 |
|
345 for (var i in nodes) { |
|
346 var n = nodes[i]; |
|
347 requirementPass = 0; |
|
348 requirementLength = 0; |
|
349 |
|
350 for (var a in attributes) { |
|
351 requirementLength++; |
|
352 try { |
|
353 if (n.getAttribute(a) == attributes[a]) { |
|
354 requirementPass++; |
|
355 } |
|
356 } catch (e) { |
|
357 // Workaround any bugs in custom attribute crap in XUL elements |
|
358 } |
|
359 } |
|
360 |
|
361 if (requirementPass == requirementLength) { |
|
362 results.push(n); |
|
363 } |
|
364 } |
|
365 |
|
366 return _returnResult(results) |
|
367 } |
|
368 |
|
369 var _byAnonAttrib = function (_document, parent, attributes) { |
|
370 var results = []; |
|
371 |
|
372 if (objects.getLength(attributes) == 1) { |
|
373 for (var i in attributes) { |
|
374 var k = i; |
|
375 var v = attributes[i]; |
|
376 } |
|
377 |
|
378 var result = _document.getAnonymousElementByAttribute(parent, k, v); |
|
379 if (result) { |
|
380 return result; |
|
381 } |
|
382 } |
|
383 |
|
384 var nodes = [n for each (n in _document.getAnonymousNodes(parent)) if (n.getAttribute)]; |
|
385 |
|
386 function resultsForNodes (nodes) { |
|
387 for (var i in nodes) { |
|
388 var n = nodes[i]; |
|
389 requirementPass = 0; |
|
390 requirementLength = 0; |
|
391 |
|
392 for (var a in attributes) { |
|
393 requirementLength++; |
|
394 if (n.getAttribute(a) == attributes[a]) { |
|
395 requirementPass++; |
|
396 } |
|
397 } |
|
398 |
|
399 if (requirementPass == requirementLength) { |
|
400 results.push(n); |
|
401 } |
|
402 } |
|
403 } |
|
404 |
|
405 resultsForNodes(nodes); |
|
406 if (results.length == 0) { |
|
407 resultsForNodes([n for each (n in parent.childNodes) if (n != undefined && n.getAttribute)]) |
|
408 } |
|
409 |
|
410 return _returnResult(results) |
|
411 } |
|
412 |
|
413 var _byIndex = function (_document, parent, i) { |
|
414 if (parent instanceof Array) { |
|
415 return parent[i]; |
|
416 } |
|
417 |
|
418 return parent.childNodes[i]; |
|
419 } |
|
420 |
|
421 var _anonByName = function (_document, parent, value) { |
|
422 return _returnResult(_forAnonChildren(_document, parent, 'tagName', value)); |
|
423 } |
|
424 |
|
425 var _anonByAttrib = function (_document, parent, value) { |
|
426 return _byAnonAttrib(_document, parent, value); |
|
427 } |
|
428 |
|
429 var _anonByIndex = function (_document, parent, i) { |
|
430 return _document.getAnonymousNodes(parent)[i]; |
|
431 } |
|
432 |
|
433 /** |
|
434 * Lookup() |
|
435 * |
|
436 * Finds an element by Lookup expression |
|
437 */ |
|
438 function Lookup(_document, expression) { |
|
439 if (expression == undefined) { |
|
440 throw new Error('Lookup constructor did not recieve enough arguments.'); |
|
441 } |
|
442 |
|
443 var expSplit = [e for each (e in smartSplit(expression) ) if (e != '')]; |
|
444 expSplit.unshift(_document); |
|
445 |
|
446 var nCases = {'id':_byID, 'name':_byName, 'attrib':_byAttrib, 'index':_byIndex}; |
|
447 var aCases = {'name':_anonByName, 'attrib':_anonByAttrib, 'index':_anonByIndex}; |
|
448 |
|
449 /** |
|
450 * Reduces the lookup expression |
|
451 * @param {Object} parentNode |
|
452 * Parent node (previousValue of the formerly executed reduce callback) |
|
453 * @param {String} exp |
|
454 * Lookup expression for the parents child node |
|
455 * |
|
456 * @returns {Object} Node found by the given expression |
|
457 */ |
|
458 var reduceLookup = function (parentNode, exp) { |
|
459 // Abort in case the parent node was not found |
|
460 if (!parentNode) { |
|
461 return false; |
|
462 } |
|
463 |
|
464 // Handle case where only index is provided |
|
465 var cases = nCases; |
|
466 |
|
467 // Handle ending index before any of the expression gets mangled |
|
468 if (withs.endsWith(exp, ']')) { |
|
469 var expIndex = json2.JSON.parse(strings.vslice(exp, '[', ']')); |
|
470 } |
|
471 |
|
472 // Handle anon |
|
473 if (withs.startsWith(exp, 'anon')) { |
|
474 exp = strings.vslice(exp, '(', ')'); |
|
475 cases = aCases; |
|
476 } |
|
477 |
|
478 if (withs.startsWith(exp, '[')) { |
|
479 try { |
|
480 var obj = json2.JSON.parse(strings.vslice(exp, '[', ']')); |
|
481 } catch (e) { |
|
482 throw new SyntaxError(e + '. String to be parsed was || ' + |
|
483 strings.vslice(exp, '[', ']') + ' ||'); |
|
484 } |
|
485 |
|
486 var r = cases['index'](_document, parentNode, obj); |
|
487 if (r == null) { |
|
488 throw new SyntaxError('Expression "' + exp + |
|
489 '" returned null. Anonymous == ' + (cases == aCases)); |
|
490 } |
|
491 |
|
492 return r; |
|
493 } |
|
494 |
|
495 for (var c in cases) { |
|
496 if (withs.startsWith(exp, c)) { |
|
497 try { |
|
498 var obj = json2.JSON.parse(strings.vslice(exp, '(', ')')) |
|
499 } catch (e) { |
|
500 throw new SyntaxError(e + '. String to be parsed was || ' + |
|
501 strings.vslice(exp, '(', ')') + ' ||'); |
|
502 } |
|
503 var result = cases[c](_document, parentNode, obj); |
|
504 } |
|
505 } |
|
506 |
|
507 if (!result) { |
|
508 if (withs.startsWith(exp, '{')) { |
|
509 try { |
|
510 var obj = json2.JSON.parse(exp); |
|
511 } catch (e) { |
|
512 throw new SyntaxError(e + '. String to be parsed was || ' + exp + ' ||'); |
|
513 } |
|
514 |
|
515 if (cases == aCases) { |
|
516 var result = _anonByAttrib(_document, parentNode, obj); |
|
517 } else { |
|
518 var result = _byAttrib(parentNode, obj); |
|
519 } |
|
520 } |
|
521 } |
|
522 |
|
523 // Final return |
|
524 if (expIndex) { |
|
525 // TODO: Check length and raise error |
|
526 return result[expIndex]; |
|
527 } else { |
|
528 // TODO: Check length and raise error |
|
529 return result; |
|
530 } |
|
531 |
|
532 // Maybe we should cause an exception here |
|
533 return false; |
|
534 }; |
|
535 |
|
536 return expSplit.reduce(reduceLookup); |
|
537 }; |