1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/string"
  5 ],
  6 function(Str) {
  7 
  8 // ********************************************************************************************* //
  9 // Constants
 10 
 11 var Ci = Components.interfaces;
 12 var Cc = Components.classes;
 13 var Cu = Components.utils;
 14 
 15 var Xpath = {};
 16 
 17 // ********************************************************************************************* //
 18 // XPATH
 19 
 20 /**
 21  * Gets an XPath for an element which describes its hierarchical location.
 22  */
 23 Xpath.getElementXPath = function(element)
 24 {
 25     if (element && element.id)
 26         return '//*[@id="' + element.id + '"]';
 27     else
 28         return Xpath.getElementTreeXPath(element);
 29 };
 30 
 31 Xpath.getElementTreeXPath = function(element)
 32 {
 33     var paths = [];
 34 
 35     // Use nodeName (instead of localName) so namespace prefix is included (if any).
 36     for (; element && element.nodeType == Node.ELEMENT_NODE; element = element.parentNode)
 37     {
 38         var index = 0;
 39         for (var sibling = element.previousSibling; sibling; sibling = sibling.previousSibling)
 40         {
 41             // Ignore document type declaration.
 42             if (sibling.nodeType == Node.DOCUMENT_TYPE_NODE)
 43                 continue;
 44 
 45             if (sibling.nodeName == element.nodeName)
 46                 ++index;
 47         }
 48 
 49         var tagName = element.nodeName.toLowerCase();
 50         var pathIndex = (index ? "[" + (index+1) + "]" : "");
 51         paths.splice(0, 0, tagName + pathIndex);
 52     }
 53 
 54     return paths.length ? "/" + paths.join("/") : null;
 55 };
 56 
 57 Xpath.cssToXPath = function(rule)
 58 {
 59     var regElement = /^([#.]?)([a-z0-9\\*_-]*)((\|)([a-z0-9\\*_-]*))?/i;
 60     var regAttr1 = /^\[([^\]]*)\]/i;
 61     var regAttr2 = /^\[\s*([^~=\s]+)\s*(~?=)\s*"([^"]+)"\s*\]/i;
 62     var regPseudo = /^:([a-z_-])+/i;
 63     var regCombinator = /^(\s*[>+\s])?/i;
 64     var regComma = /^\s*,/i;
 65 
 66     var index = 1;
 67     var parts = ["//", "*"];
 68     var lastRule = null;
 69 
 70     while (rule.length && rule != lastRule)
 71     {
 72         lastRule = rule;
 73 
 74         // Trim leading whitespace
 75         rule = Str.trim(rule);
 76         if (!rule.length)
 77             break;
 78 
 79         // Match the element identifier
 80         var m = regElement.exec(rule);
 81         if (m)
 82         {
 83             if (!m[1])
 84             {
 85                 // XXXjoe Namespace ignored for now
 86                 if (m[5])
 87                     parts[index] = m[5];
 88                 else
 89                     parts[index] = m[2];
 90             }
 91             else if (m[1] == '#')
 92                 parts.push("[@id='" + m[2] + "']");
 93             else if (m[1] == '.')
 94                 parts.push("[contains(concat(' ',normalize-space(@class),' '), ' " + m[2] + " ')]");
 95 
 96             rule = rule.substr(m[0].length);
 97         }
 98 
 99         // Match attribute selectors
100         m = regAttr2.exec(rule);
101         if (m)
102         {
103             if (m[2] == "~=")
104                 parts.push("[contains(@" + m[1] + ", '" + m[3] + "')]");
105             else
106                 parts.push("[@" + m[1] + "='" + m[3] + "']");
107 
108             rule = rule.substr(m[0].length);
109         }
110         else
111         {
112             m = regAttr1.exec(rule);
113             if (m)
114             {
115                 parts.push("[@" + m[1] + "]");
116                 rule = rule.substr(m[0].length);
117             }
118         }
119 
120         // Skip over pseudo-classes and pseudo-elements, which are of no use to us
121         m = regPseudo.exec(rule);
122         while (m)
123         {
124             rule = rule.substr(m[0].length);
125             m = regPseudo.exec(rule);
126         }
127 
128         // Match combinators
129         m = regCombinator.exec(rule);
130         if (m && m[0].length)
131         {
132             if (m[0].indexOf(">") != -1)
133                 parts.push("/");
134             else if (m[0].indexOf("+") != -1)
135                 parts.push("/following-sibling::");
136             else
137                 parts.push("//");
138 
139             index = parts.length;
140             parts.push("*");
141             rule = rule.substr(m[0].length);
142         }
143 
144         m = regComma.exec(rule);
145         if (m)
146         {
147             parts.push(" | ", "//", "*");
148             index = parts.length-1;
149             rule = rule.substr(m[0].length);
150         }
151     }
152 
153     var xpath = parts.join("");
154     return xpath;
155 };
156 
157 Xpath.getElementsBySelector = function(doc, css)
158 {
159     var xpath = Xpath.cssToXPath(css);
160     return Xpath.getElementsByXPath(doc, xpath);
161 };
162 
163 Xpath.getElementsByXPath = function(doc, xpath)
164 {
165     var result = Xpath.evaluateXPath(doc, xpath);
166 
167     if (result instanceof Array)
168         return result;
169 
170     return [];
171 };
172 
173 Xpath.evaluateXPath = function(doc, xpath, contextNode, resultType)
174 {
175     if (contextNode === undefined)
176         contextNode = doc;
177 
178     if (resultType === undefined)
179         resultType = XPathResult.ANY_TYPE;
180 
181     try
182     {
183         var result = doc.evaluate(xpath, contextNode, null, resultType, null);
184     }
185     catch (exc)
186     {
187         // If an invalid XPath expression was entered, it should be caught without exception
188         return;
189     }
190 
191     switch (result.resultType)
192     {
193         case XPathResult.NUMBER_TYPE:
194             return result.numberValue;
195 
196         case XPathResult.STRING_TYPE:
197             return result.stringValue;
198             
199         case XPathResult.BOOLEAN_TYPE:
200             return result.booleanValue;
201 
202         case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
203         case XPathResult.ORDERED_NODE_ITERATOR_TYPE:
204             var nodes = [];
205             for (var item = result.iterateNext(); item; item = result.iterateNext())
206                 nodes.push(item);
207             return nodes;
208             
209         case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE:
210         case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE:
211             var nodes = [];
212             for (var i = 0; i < result.snapshotLength; ++i)
213                 nodes.push(result.snapshotItem(i));
214             return nodes;
215             
216         case XPathResult.ANY_UNORDERED_NODE_TYPE:
217         case XPathResult.FIRST_ORDERED_NODE_TYPE:
218             return result.singleNodeValue;
219     }
220 };
221 
222 Xpath.getRuleMatchingElements = function(rule, doc)
223 {
224     var css = rule.selectorText;
225     var xpath = Xpath.cssToXPath(css);
226     return Xpath.getElementsByXPath(doc, xpath);
227 };
228 
229 // ********************************************************************************************* //
230 
231 return Xpath;
232 
233 // ********************************************************************************************* //
234 });
235