michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["XPathGenerator"]; michael@0: michael@0: this.XPathGenerator = { michael@0: // these two hashes should be kept in sync michael@0: namespaceURIs: { michael@0: "xhtml": "http://www.w3.org/1999/xhtml", michael@0: "xul": "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" michael@0: }, michael@0: namespacePrefixes: { michael@0: "http://www.w3.org/1999/xhtml": "xhtml", michael@0: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul": "xul" michael@0: }, michael@0: michael@0: /** michael@0: * Generates an approximate XPath query to an (X)HTML node michael@0: */ michael@0: generate: function sss_xph_generate(aNode) { michael@0: // have we reached the document node already? michael@0: if (!aNode.parentNode) michael@0: return ""; michael@0: michael@0: // Access localName, namespaceURI just once per node since it's expensive. michael@0: let nNamespaceURI = aNode.namespaceURI; michael@0: let nLocalName = aNode.localName; michael@0: michael@0: let prefix = this.namespacePrefixes[nNamespaceURI] || null; michael@0: let tag = (prefix ? prefix + ":" : "") + this.escapeName(nLocalName); michael@0: michael@0: // stop once we've found a tag with an ID michael@0: if (aNode.id) michael@0: return "//" + tag + "[@id=" + this.quoteArgument(aNode.id) + "]"; michael@0: michael@0: // count the number of previous sibling nodes of the same tag michael@0: // (and possible also the same name) michael@0: let count = 0; michael@0: let nName = aNode.name || null; michael@0: for (let n = aNode; (n = n.previousSibling); ) michael@0: if (n.localName == nLocalName && n.namespaceURI == nNamespaceURI && michael@0: (!nName || n.name == nName)) michael@0: count++; michael@0: michael@0: // recurse until hitting either the document node or an ID'd node michael@0: return this.generate(aNode.parentNode) + "/" + tag + michael@0: (nName ? "[@name=" + this.quoteArgument(nName) + "]" : "") + michael@0: (count ? "[" + (count + 1) + "]" : ""); michael@0: }, michael@0: michael@0: /** michael@0: * Resolves an XPath query generated by XPathGenerator.generate michael@0: */ michael@0: resolve: function sss_xph_resolve(aDocument, aQuery) { michael@0: let xptype = Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE; michael@0: return aDocument.evaluate(aQuery, aDocument, this.resolveNS, xptype, null).singleNodeValue; michael@0: }, michael@0: michael@0: /** michael@0: * Namespace resolver for the above XPath resolver michael@0: */ michael@0: resolveNS: function sss_xph_resolveNS(aPrefix) { michael@0: return XPathGenerator.namespaceURIs[aPrefix] || null; michael@0: }, michael@0: michael@0: /** michael@0: * @returns valid XPath for the given node (usually just the local name itself) michael@0: */ michael@0: escapeName: function sss_xph_escapeName(aName) { michael@0: // we can't just use the node's local name, if it contains michael@0: // special characters (cf. bug 485482) michael@0: return /^\w+$/.test(aName) ? aName : michael@0: "*[local-name()=" + this.quoteArgument(aName) + "]"; michael@0: }, michael@0: michael@0: /** michael@0: * @returns a properly quoted string to insert into an XPath query michael@0: */ michael@0: quoteArgument: function sss_xph_quoteArgument(aArg) { michael@0: return !/'/.test(aArg) ? "'" + aArg + "'" : michael@0: !/"/.test(aArg) ? '"' + aArg + '"' : michael@0: "concat('" + aArg.replace(/'+/g, "',\"$&\",'") + "')"; michael@0: }, michael@0: michael@0: /** michael@0: * @returns an XPath query to all savable form field nodes michael@0: */ michael@0: get restorableFormNodes() { michael@0: // for a comprehensive list of all available types see michael@0: // http://mxr.mozilla.org/mozilla-central/search?string=kInputTypeTable michael@0: let ignoreTypes = ["password", "hidden", "button", "image", "submit", "reset"]; michael@0: // XXXzeniko work-around until lower-case has been implemented (bug 398389) michael@0: let toLowerCase = '"ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"'; michael@0: let ignore = "not(translate(@type, " + toLowerCase + ")='" + michael@0: ignoreTypes.join("' or translate(@type, " + toLowerCase + ")='") + "')"; michael@0: let formNodesXPath = "//textarea|//select|//xhtml:textarea|//xhtml:select|" + michael@0: "//input[" + ignore + "]|//xhtml:input[" + ignore + "]"; michael@0: michael@0: // Special case for about:config's search field. michael@0: formNodesXPath += '|/xul:window[@id="config"]//xul:textbox[@id="textbox"]'; michael@0: michael@0: delete this.restorableFormNodes; michael@0: return (this.restorableFormNodes = formNodesXPath); michael@0: } michael@0: };