2 // jquery.xmlns.js: xml-namespace selector support for jQuery
4 // This plugin modifies the jQuery tag and attribute selectors to
5 // support optional namespace specifiers as defined in CSS 3:
7 // $("elem") - matches 'elem' nodes in the default namespace
8 // $("|elem") - matches 'elem' nodes that don't have a namespace
9 // $("NS|elem") - matches 'elem' nodes in declared namespace 'NS'
10 // $("*|elem") - matches 'elem' nodes in any namespace
11 // $("NS|*") - matches any nodes in declared namespace 'NS'
13 // A similar synax is also supported for attribute selectors, but note
14 // that the default namespace does *not* apply to attributes - a missing
15 // or empty namespace selector selects only attributes with no namespace.
17 // In a slight break from the W3C standards, and with a nod to ease of
18 // implementation, the empty namespace URI is treated as equivalent to
19 // an unspecified namespace. Plenty of browsers seem to make the same
22 // Namespace declarations live in the $.xmlns object, which is a simple
23 // mapping from namespace ids to namespace URIs. The default namespace
24 // is determined by the value associated with the empty string.
27 // $.xmlns.FOO = "http://www.foo.com/xmlns/foobar"
28 // $.xmlns[""] = "http://www.example.com/new/default/namespace/"
30 // Unfortunately this is a global setting - I can't find a way to do
31 // query-object-specific namespaces since the jQuery selector machinery
32 // is stateless. However, you can use the 'xmlns' function to push and
33 // pop namespace delcarations with ease:
35 // $().xmlns({D:"DAV:"}) // pushes the DAV: namespace
36 // $().xmlns("DAV:") // makes DAV: the default namespace
37 // $().xmlns(false) // pops the namespace we just pushed
38 // $().xmlns(false) // pops again, returning to defaults
40 // To execute this as a kind of "transaction", pass a function as the
41 // second argument. It will be executed in the context of the current
44 // $().xmlns("DAV:",function() {
45 // // The default namespace is DAV: within this function,
46 // // but it is reset to the previous value on exit.
47 // return this.find("response").each(...);
50 // If you pass a string as a function, it will be executed against the
51 // current jQuery object using find(); i.e. the following will find all
52 // "href" elements in the "DAV:" namespace:
54 // $().xmlns("DAV:","href")
57 // And finally, the legal stuff:
59 // Copyright (c) 2009, Ryan Kelly.
60 // TAG and ATTR functions derived from jQuery's selector.js.
61 // Dual licensed under the MIT and GPL licenses.
62 // http://docs.jquery.com/License
67 var jquery_gt_18 = jQuery.fn.jquery >= "1.8";
68 // Some common default namespaces, that are treated specially by browsers.
71 "xml": "http://www.w3.org/XML/1998/namespace",
72 "xmlns": "http://www.w3.org/2000/xmlns/",
73 "html": "http://www.w3.org/1999/xhtml/"
76 rbackslash = /\\(?!\\)/g;
78 // A reverse mapping for common namespace prefixes.
80 var default_xmlns_rev = {}
81 for(var k in default_xmlns) {
82 default_xmlns_rev[default_xmlns[k]] = k;
86 // $.xmlns is a mapping from namespace identifiers to namespace URIs.
87 // The default default-namespace is "*", and we provide some additional
88 // defaults that are specified in the XML Namespaces standard.
90 $.extend({xmlns: $.extend({},default_xmlns,{"":"*"})});
93 // jQuery method to push/pop namespace declarations.
95 // If a single argument is specified:
96 // * if it's a mapping, push those namespaces onto the stack
97 // * if it's a string, push that as the default namespace
98 // * if it evaluates to false, pop the latest namespace
100 // If two arguments are specified, the second is executed "transactionally"
101 // using the namespace declarations found in the first. It can be either a
102 // a selector string (in which case it is passed to this.find()) or a function
103 // (in which case it is called in the context of the current jQuery object).
104 // The given namespace mapping is automatically pushed before executing and
105 // popped afterwards.
107 var xmlns_stack = [];
108 $.fn.extend({xmlns: function(nsmap,func) {
109 if(typeof nsmap == "string") {
113 xmlns_stack.push($.xmlns);
114 $.xmlns = $.extend({},$.xmlns,nsmap);
115 if(func !== undefined) {
116 if(typeof func == "string") {
117 return this.find(func).xmlns(undefined)
121 self = func.call(this);
126 self.xmlns(undefined);
134 $.xmlns = (xmlns_stack ? xmlns_stack.pop() : {});
140 // Convert a namespace prefix into a namespace URI, based
141 // on the delcarations made in $.xmlns.
143 var getNamespaceURI = function(id) {
144 // No namespace id, use the default.
148 // Strip the pipe character from the specifier
149 id = id.substr(0,id.length-1);
150 // Certain special namespaces aren't mapped to a URI
151 if(id == "" || id == "*") {
154 var ns = $.xmlns[id];
155 if(typeof(ns) == "undefined") {
156 throw "Syntax error, undefined namespace prefix '" + id + "'";
162 // Update the regex used by $.expr to parse selector components for a
163 // particular type of selector (e.g. "TAG" or "ATTR").
165 // This logic is taken straight from the jQuery/Sizzle sources.
167 var setExprMatchRegex = (jquery_gt_18) ? function(type,regex) {
168 $.expr.match[type] = regex;
169 } : function(type,regex) {
170 $.expr.match[type] = new RegExp(regex.source + /(?![^\[]*\])(?![^\(]*\))/.source);
171 if($.expr.leftMatch) {
172 $.expr.leftMatch[type] = new RegExp(/(^(?:.|\r|\n)*?)/.source + $.expr.match[type].source.replace(/\\(\d+)/g, function(all, num){
173 return "\\" + (num - 0 + 1);
178 // Modify the TAG match regexp to include optional namespace selector.
179 // This is basically (namespace|)?(tagname).
182 setExprMatchRegex("TAG",/^((?:((?:\\.|[-\w*]|[^\x00-\xa0])+\|)?((?:\\.|[-\w*]|[^\x00-\xa0])+)))/);
184 setExprMatchRegex("TAG",/^((?:((?:[\w\u00c0-\uFFFF\*_-]*\|)?)((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)))/);
187 // Perform some capability-testing.
189 var div = document.createElement("div");
191 // Sometimes getElementsByTagName("*") will return comment nodes,
192 // which we will have to remove from the results.
194 var gebtn_yields_comments = false;
195 div.appendChild(document.createComment(""));
196 if(div.getElementsByTagName("*").length > 0) {
197 gebtn_yields_comments = true;
200 // Some browsers return node.localName in upper case, some in lower case.
202 var localname_is_uppercase = true;
203 if(div.localName && div.localName == "div") {
204 localname_is_uppercase = false;
207 // Allow the testing div to be garbage-collected.
211 function find_tag_17(match, context, isXML) {
212 var ns = getNamespaceURI(match[2]);
214 return find_tag(ns, ln, match, context, isXML);
217 function find_tag_18(match, context, isXML) {
218 var index = match.indexOf("|");
221 if ( typeof context.getElementsByTagName !== "undefined" ) {
222 return context.getElementsByTagName(match);
226 var ns = getNamespaceURI(match.substring(0,index + 1));
227 var ln = match.substring(index + 1);
229 return find_tag(ns, ln, match, context, isXML);
232 function find_tag(ns, ln, match, context, isXML) {
234 if(typeof context.getElementsByTagNameNS != "undefined") {
235 // Easy case - we have getElementsByTagNameNS
236 res = context.getElementsByTagNameNS(ns,ln);
237 } else if(typeof context.selectNodes != "undefined") {
238 // Use xpath if possible (not available on HTML DOM nodes in IE)
239 if(context.ownerDocument) {
240 context.ownerDocument.setProperty("SelectionLanguage","XPath");
242 context.setProperty("SelectionLanguage","XPath");
247 predicate="namespace-uri()='"+ns+"' and local-name()='"+ln+"'";
249 predicate="namespace-uri()='"+ns+"'";
253 predicate="local-name()='"+ln+"'";
257 res = context.selectNodes("descendant-or-self::*["+predicate+"]");
259 res = context.selectNodes("descendant-or-self::*");
262 // Otherwise, we need to simulate using getElementsByTagName
263 res = context.getElementsByTagName(ln);
264 if(gebtn_yields_comments && ln == "*") {
266 for(var i=0; res[i]; i++) {
267 if(res[i].nodeType == 1) {
273 if(res && ns != "*") {
275 for(var i=0; res[i]; i++) {
276 if(res[i].namespaceURI == ns || res[i].tagUrn == ns) {
286 // Modify the TAG find function to account for a namespace selector.
288 $.expr.find.TAG = (jquery_gt_18)? find_tag_18 : find_tag_17;
290 // Check whether a node is part of an XML document.
291 // Copied verbatim from jQuery sources, needed in TAG preFilter below.
293 var isXML = function(elem){
294 return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" ||
295 !!elem.ownerDocument && elem.ownerDocument.documentElement.nodeName !== "HTML";
298 // Modify the TAG preFilter function to work with modified match regexp.
299 // This normalises case of the tag name if we're in a HTML document.
301 $.expr.preFilter.TAG = (jquery_gt_18)? function(match, curLoop, inplace, result, not, isXML) {
304 if(localname_is_uppercase) {
305 ln = ln.toUpperCase();
307 ln = ln.toLowerCase();
310 return [match[0], match[0],getNamespaceURI(match[2]),ln];
311 } : function(match, curLoop, inplace, result, not, isXML) {
314 if(localname_is_uppercase) {
315 ln = ln.toUpperCase();
317 ln = ln.toLowerCase();
320 return [match[0],getNamespaceURI(match[2]),ln];
323 function filter_tag(ln, ns) {
324 var index = ln.indexOf("|");
326 ln = ln.substring(index+1);
327 return function( elem ) {
328 var e_ns = elem.namespaceURI ? elem.namespaceURI : elem.tagUrn;
329 var e_ln = elem.localName ? elem.localName : elem.tagName;
330 if(ns == "*" || e_ns == ns || (ns == "" && !e_ns)) {
331 return ((ln == "*" && elem.nodeType == 1) || e_ln == ln);
338 //Modify the TAG filter function to account for a namespace selector.
340 $.expr.filter.TAG = (jquery_gt_18)? filter_tag : function(elem, match) {
343 return filter_tag(ln, ns).call(this, elem, null);
346 // Modify the ATTR match regexp to extract a namespace selector.
347 // This is basically ([namespace|])(attrname)(op)(quote)(pattern)(quote)
350 setExprMatchRegex("ATTR",/^\[[\x20\t\r\n\f]*(((?:\\.|[-\w]|[^\x00-\xa0])+\|)?((?:\\.|[-\w]|[^\x00-\xa0])+))[\x20\t\r\n\f]*(?:([*^$|!~]?=)[\x20\t\r\n\f]*(?:(['"])((?:\\.|[^\\])*?)\5|((?:\\.|[-\w]|[^\x00-\xa0])+\|)?((?:\\.|[-\w#]|[^\x00-\xa0])+)|)|)[\x20\t\r\n\f]*\]/);
352 setExprMatchRegex("ATTR",/\[\s*((?:((?:[\w\u00c0-\uFFFF\*_-]*\|)?)((?:[\w\u00c0-\uFFFF_-]|\\.)+)))\s*(?:(\S?=)\s*(['"]*)(.*?)\5|)\s*\]/);
355 // Modify the ATTR preFilter function to account for new regexp match groups,
356 // and normalise the namespace URI.
358 $.expr.preFilter.ATTR = (jquery_gt_18)? function( match, context, isXml ) {
359 var name = match[1].replace(rbackslash, "");
361 if( match[4] == "~=" ) {
362 match[6] = " " + match[6] + " ";
365 // Move the given value to match[5] whether quoted or unquoted
366 match[5] = ( match[6] || match[7] || "" ).replace( rbackslash, "" );
368 if(!match[2] || match[2] == "|") {
371 match[2] = getNamespaceURI(match[2]);
374 return match.slice( 0, 6 );
375 } : function(match, curLoop, inplace, result, not, isXML) {
376 var name = match[3].replace(/\\/g, "");
377 if(!isXML && $.expr.attrMap[name]) {
378 match[3] = $.expr.attrMap[name];
380 if( match[4] == "~=" ) {
381 match[6] = " " + match[6] + " ";
383 if(!match[2] || match[2] == "|") {
386 match[2] = getNamespaceURI(match[2]);
393 var attr_op_eval = function(result, operator, check) {
394 if ( result == null ) {
395 return operator === "!=";
404 return operator === "=" ? result === check :
405 operator === "!=" ? result !== check :
406 operator === "^=" ? check && result.indexOf( check ) === 0 :
407 operator === "*=" ? check && result.indexOf( check ) > -1 :
408 operator === "$=" ? check && result.substr( result.length - check.length ) === check :
409 operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 :
410 operator === "|=" ? result === check || result.substr( 0, check.length + 1 ) === check + "-" :
414 //Modify the ATTR filter function to account for namespace selector.
415 //Unfortunately this means factoring out the attribute-checking code
416 //into a separate function, since it might be called multiple times.
418 function filter_attr( prefixedName, ns, name, op, check ) {
419 return function( elem, context ) {
421 var result = $(elem).attr(name);
422 return attr_op_eval(result, op, check);
424 if(ns != "*" && typeof elem.getAttributeNS != "undefined") {
425 return attr_op_eval(elem.getAttributeNS(ns,name), op, check);
428 // Need to iterate over all attributes, either because we couldn't
429 // look it up or because we need to match all namespaces.
430 var attrs = elem.attributes;
431 for (var i=0; attrs[i]; i++) {
432 var ln = attrs[i].localName;
434 ln = attrs[i].nodeName;
435 var idx = ln.indexOf(":");
437 ln = ln.substr(idx+1);
441 result = attrs[i].nodeValue;
442 if(ns == "*" || attrs[i].namespaceURI == ns) {
443 if(attr_op_eval(result, op, check)) {
447 if(attrs[i].namespaceURI === "" && attrs[i].prefix) {
448 if(attrs[i].prefix == default_xmlns_rev[ns]) {
449 if(attr_op_eval(result, op, check)) {
461 $.expr.filter.ATTR = (jquery_gt_18)? filter_attr : function(elem, match) {
465 var check = match[6];
466 return filter_attr(null, ns, name, type, check).call(this, elem, null);