/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ var EXPORTED_SYMBOLS = ["ID", "Link", "XPath", "Selector", "Name", "Anon", "AnonXPath", "Lookup", "_byID", "_byName", "_byAttrib", "_byAnonAttrib", ]; var Cc = Components.classes; var Ci = Components.interfaces; var Cu = Components.utils; Cu.import("resource://gre/modules/Services.jsm"); var utils = {}; Cu.import('resource://mozmill/stdlib/utils.js', utils); var strings = {}; Cu.import('resource://mozmill/stdlib/strings.js', strings); var arrays = {}; Cu.import('resource://mozmill/stdlib/arrays.js', arrays); var json2 = {}; Cu.import('resource://mozmill/stdlib/json2.js', json2); var withs = {}; Cu.import('resource://mozmill/stdlib/withs.js', withs); var dom = {}; Cu.import('resource://mozmill/stdlib/dom.js', dom); var objects = {}; Cu.import('resource://mozmill/stdlib/objects.js', objects); var countQuotes = function (str) { var count = 0; var i = 0; while (i < str.length) { i = str.indexOf('"', i); if (i != -1) { count++; i++; } else { break; } } return count; }; /** * smartSplit() * * Takes a lookup string as input and returns * a list of each node in the string */ var smartSplit = function (str) { // Ensure we have an even number of quotes if (countQuotes(str) % 2 != 0) { throw new Error ("Invalid Lookup Expression"); } /** * This regex matches a single "node" in a lookup string. * In otherwords, it matches the part between the two '/'s * * Regex Explanation: * \/ - start matching at the first forward slash * ([^\/"]*"[^"]*")* - match as many pairs of quotes as possible until we hit a slash (ignore slashes inside quotes) * [^\/]* - match the remainder of text outside of last quote but before next slash */ var re = /\/([^\/"]*"[^"]*")*[^\/]*/g var ret = [] var match = re.exec(str); while (match != null) { ret.push(match[0].replace(/^\//, "")); match = re.exec(str); } return ret; }; /** * defaultDocuments() * * Returns a list of default documents in which to search for elements * if no document is provided */ function defaultDocuments() { var win = Services.wm.getMostRecentWindow("navigator:browser"); return [ win.document, utils.getBrowserObject(win).selectedBrowser.contentWindow.document ]; }; /** * nodeSearch() * * Takes an optional document, callback and locator string * Returns a handle to the located element or null */ function nodeSearch(doc, func, string) { if (doc != undefined) { var documents = [doc]; } else { var documents = defaultDocuments(); } var e = null; var element = null; //inline function to recursively find the element in the DOM, cross frame. var search = function (win, func, string) { if (win == null) { return; } //do the lookup in the current window element = func.call(win, string); if (!element || (element.length == 0)) { var frames = win.frames; for (var i = 0; i < frames.length; i++) { search(frames[i], func, string); } } else { e = element; } }; for (var i = 0; i < documents.length; ++i) { var win = documents[i].defaultView; search(win, func, string); if (e) { break; } } return e; }; /** * Selector() * * Finds an element by selector string */ function Selector(_document, selector, index) { if (selector == undefined) { throw new Error('Selector constructor did not recieve enough arguments.'); } this.selector = selector; this.getNodeForDocument = function (s) { return this.document.querySelectorAll(s); }; var nodes = nodeSearch(_document, this.getNodeForDocument, this.selector); return nodes ? nodes[index || 0] : null; }; /** * ID() * * Finds an element by ID */ function ID(_document, nodeID) { if (nodeID == undefined) { throw new Error('ID constructor did not recieve enough arguments.'); } this.getNodeForDocument = function (nodeID) { return this.document.getElementById(nodeID); }; return nodeSearch(_document, this.getNodeForDocument, nodeID); }; /** * Link() * * Finds a link by innerHTML */ function Link(_document, linkName) { if (linkName == undefined) { throw new Error('Link constructor did not recieve enough arguments.'); } this.getNodeForDocument = function (linkName) { var getText = function (el) { var text = ""; if (el.nodeType == 3) { //textNode if (el.data != undefined) { text = el.data; } else { text = el.innerHTML; } text = text.replace(/n|r|t/g, " "); } else if (el.nodeType == 1) { //elementNode for (var i = 0; i < el.childNodes.length; i++) { var child = el.childNodes.item(i); text += getText(child); } if (el.tagName == "P" || el.tagName == "BR" || el.tagName == "HR" || el.tagName == "DIV") { text += "\n"; } } return text; }; //sometimes the windows won't have this function try { var links = this.document.getElementsByTagName('a'); } catch (e) { // ADD LOG LINE mresults.write('Error: '+ e, 'lightred'); } for (var i = 0; i < links.length; i++) { var el = links[i]; //if (getText(el).indexOf(this.linkName) != -1) { if (el.innerHTML.indexOf(linkName) != -1) { return el; } } return null; }; return nodeSearch(_document, this.getNodeForDocument, linkName); }; /** * XPath() * * Finds an element by XPath */ function XPath(_document, expr) { if (expr == undefined) { throw new Error('XPath constructor did not recieve enough arguments.'); } this.getNodeForDocument = function (s) { var aNode = this.document; var aExpr = s; var xpe = null; if (this.document.defaultView == null) { xpe = new getMethodInWindows('XPathEvaluator')(); } else { xpe = new this.document.defaultView.XPathEvaluator(); } var nsResolver = xpe.createNSResolver(aNode.ownerDocument == null ? aNode.documentElement : aNode.ownerDocument.documentElement); var result = xpe.evaluate(aExpr, aNode, nsResolver, 0, null); var found = []; var res; while (res = result.iterateNext()) { found.push(res); } return found[0]; }; return nodeSearch(_document, this.getNodeForDocument, expr); }; /** * Name() * * Finds an element by Name */ function Name(_document, nName) { if (nName == undefined) { throw new Error('Name constructor did not recieve enough arguments.'); } this.getNodeForDocument = function (s) { try{ var els = this.document.getElementsByName(s); if (els.length > 0) { return els[0]; } } catch (e) { } return null; }; return nodeSearch(_document, this.getNodeForDocument, nName); }; var _returnResult = function (results) { if (results.length == 0) { return null } else if (results.length == 1) { return results[0]; } else { return results; } } var _forChildren = function (element, name, value) { var results = []; var nodes = Array.from(element.childNodes).filter(e => e); for (var i in nodes) { var n = nodes[i]; if (n[name] == value) { results.push(n); } } return results; } var _forAnonChildren = function (_document, element, name, value) { var results = []; var nodes = Array.from(_document.getAnoymousNodes(element)).filter(e => e); for (var i in nodes ) { var n = nodes[i]; if (n[name] == value) { results.push(n); } } return results; } var _byID = function (_document, parent, value) { return _returnResult(_forChildren(parent, 'id', value)); } var _byName = function (_document, parent, value) { return _returnResult(_forChildren(parent, 'tagName', value)); } var _byAttrib = function (parent, attributes) { var results = []; var nodes = parent.childNodes; for (var i in nodes) { var n = nodes[i]; requirementPass = 0; requirementLength = 0; for (var a in attributes) { requirementLength++; try { if (n.getAttribute(a) == attributes[a]) { requirementPass++; } } catch (e) { // Workaround any bugs in custom attribute crap in XUL elements } } if (requirementPass == requirementLength) { results.push(n); } } return _returnResult(results) } var _byAnonAttrib = function (_document, parent, attributes) { var results = []; if (objects.getLength(attributes) == 1) { for (var i in attributes) { var k = i; var v = attributes[i]; } var result = _document.getAnonymousElementByAttribute(parent, k, v); if (result) { return result; } } var nodes = Array.from(_document.getAnonymousNodes(parent)).filter(n => n.getAttribute); function resultsForNodes (nodes) { for (var i in nodes) { var n = nodes[i]; requirementPass = 0; requirementLength = 0; for (var a in attributes) { requirementLength++; if (n.getAttribute(a) == attributes[a]) { requirementPass++; } } if (requirementPass == requirementLength) { results.push(n); } } } resultsForNodes(nodes); if (results.length == 0) { resultsForNodes(Array.from(parent.childNodes).filter(n => n != undefined && n.getAttribute)); } return _returnResult(results) } var _byIndex = function (_document, parent, i) { if (parent instanceof Array) { return parent[i]; } return parent.childNodes[i]; } var _anonByName = function (_document, parent, value) { return _returnResult(_forAnonChildren(_document, parent, 'tagName', value)); } var _anonByAttrib = function (_document, parent, value) { return _byAnonAttrib(_document, parent, value); } var _anonByIndex = function (_document, parent, i) { return _document.getAnonymousNodes(parent)[i]; } /** * Lookup() * * Finds an element by Lookup expression */ function Lookup(_document, expression) { if (expression == undefined) { throw new Error('Lookup constructor did not recieve enough arguments.'); } var expSplit = smartSplit(expression).filter(e => e != ''); expSplit.unshift(_document); var nCases = {'id':_byID, 'name':_byName, 'attrib':_byAttrib, 'index':_byIndex}; var aCases = {'name':_anonByName, 'attrib':_anonByAttrib, 'index':_anonByIndex}; /** * Reduces the lookup expression * @param {Object} parentNode * Parent node (previousValue of the formerly executed reduce callback) * @param {String} exp * Lookup expression for the parents child node * * @returns {Object} Node found by the given expression */ var reduceLookup = function (parentNode, exp) { // Abort in case the parent node was not found if (!parentNode) { return false; } // Handle case where only index is provided var cases = nCases; // Handle ending index before any of the expression gets mangled if (withs.endsWith(exp, ']')) { var expIndex = json2.JSON.parse(strings.vslice(exp, '[', ']')); } // Handle anon if (withs.startsWith(exp, 'anon')) { exp = strings.vslice(exp, '(', ')'); cases = aCases; } if (withs.startsWith(exp, '[')) { try { var obj = json2.JSON.parse(strings.vslice(exp, '[', ']')); } catch (e) { throw new SyntaxError(e + '. String to be parsed was || ' + strings.vslice(exp, '[', ']') + ' ||'); } var r = cases['index'](_document, parentNode, obj); if (r == null) { throw new SyntaxError('Expression "' + exp + '" returned null. Anonymous == ' + (cases == aCases)); } return r; } for (var c in cases) { if (withs.startsWith(exp, c)) { try { var obj = json2.JSON.parse(strings.vslice(exp, '(', ')')) } catch (e) { throw new SyntaxError(e + '. String to be parsed was || ' + strings.vslice(exp, '(', ')') + ' ||'); } var result = cases[c](_document, parentNode, obj); } } if (!result) { if (withs.startsWith(exp, '{')) { try { var obj = json2.JSON.parse(exp); } catch (e) { throw new SyntaxError(e + '. String to be parsed was || ' + exp + ' ||'); } if (cases == aCases) { var result = _anonByAttrib(_document, parentNode, obj); } else { var result = _byAttrib(parentNode, obj); } } } // Final return if (expIndex) { // TODO: Check length and raise error return result[expIndex]; } else { // TODO: Check length and raise error return result; } // Maybe we should cause an exception here return false; }; return expSplit.reduce(reduceLookup); };