XPath helpers $x and $X

in
Subscribe to XPath helpers $x and $X 5 posts, 4 voices



Johan Sundström Scriptwright

I'd like to push a little for my XPath micro-library http://ecmanaut.googlecode.com/svn/trunk/lib/gm/$x$X.js -- which offers two primitive functions:

$x(xpath, contextNode), which you might be familiar with from the Firebug console (I think the optional contextNode parameter isn't presently handled in Firebug -- though it allows you to pass a reference point from which the expression should be evaluated -- but more about that further below). This resolves an XPath expression, returning, not a NodeSet, but a proper javascript native Array of DOM nodes (typically), that has all the .forEach(), .map(), .filter(), and so on, methods that normal arrays have, and:

$X(xpath, contextNode), which returns either a single item (the first), if one or more were found, or null.

That is, as long as your XPath query returns DOM nodes (as most do). If, however, it returns a string ($X('string(//title)')), either method will get you a native javascript string, if it returns a number ($X('count(//div)')), you get a number, and if it returns a boolean ($X('count(//title) = 1')), you get true or false, without mucking about with the hideous document.evaluate DOM API.

Which, in real code, means you can do XPath without cluttering up your code, as easy and readable as, like in my Image title captions script, for instance:


// ==UserScript==
// @name           Image title captions
// @...
// @require        http://ecmanaut.googlecode.com/svn/trunk/lib/gm/$x$X.js
// ==/UserScript==

var imgs = $x('//img[@title] | //a[@title]/img'); // all the images we're interested in

or, in cases like del.icio.us mp3, where we just want to test for the existence of a node (if no script tag that loads playtagger.js already exists, run the rest of the script):


  if (!$X('//script[contains(@src,"/playtagger.js")]')) // avoid running twice

The contextNode parameter lets you pass a DOM node of your own, from which to resolve the XPath expression, in a relative fashion. Be sure to not start the expression with '//' (which turns it into an absolute reference from the start of the document anyway), but using relative addressing like './/a', which will grab all links below the given node.

That's all. This is the full length of the library (I told you it was small -- though the code readability benefit is huge):


function $x( xpath, root ) {
  var doc = root ? root.evaluate ? root : root.ownerDocument : document, next;
  var got = doc.evaluate( xpath, root||doc, null, 0, null ), result = [];
  switch (got.resultType) {
    case got.STRING_TYPE:
      return got.stringValue;
    case got.NUMBER_TYPE:
      return got.numberValue;
    case got.BOOLEAN_TYPE:
      return got.booleanValue;
    default:
      while ((next = got.iterateNext()))
	result.push( next );
      return result;
  }
}

function $X( xpath, root ) {
  var got = $x( xpath, root );
  return got instanceof Array ? got[0] : got;
}

All public domain; use, share and spread the word, as much as you like!

 
sizzlemctwizzle Scriptwright

I use this function:

function $x(x, t, r) {
    if (t && t.nodeType) 
        var h = r, r = t, t = h;    
    var d = r ? r.ownerDocument || r : r = document, p;
    switch (t) {
    case 1:
        p = 'numberValue';
        break;
    case 2:
        p = 'stringValue';
        break;
    case 3:
        p = 'booleanValue';
        break;
    case 8: case 9:
        p = 'singleNodeValue';
        break;
    default:
        return d.evaluate(x, r, null, t || 6, null);
    }
    return d.evaluate(x, r, null, t, null)[p];
}

// Optional shortcut functions I like
function $x1(x, r) { return $x(x, 9, r) } 
function $xb(x, r) { return $x(x, 3, r) }

$x is a very smart function because of it's relative element capabilities. You can pass my function a relative reference to an element that is in another document (iframe or result of DOMParser) and it will do all the work for you.

I also wrote a forEach function that works with both xpath (iterator or snapshot) results and arrays. The function is much more robust than the native forEach.

function forEach(lst, cb) {
    if (lst.snapshotItem)
        for (var i = 0, len = lst.snapshotLength; i < len; ++i)
            cb(lst.snapshotItem(i), i, lst);
    else if (lst.iterateNext) {
        var item;
        while (item = lst.iterateNext()) 
            cb(item, lst);
    } else if (typeof lst.length != 'undefined') 
        for (var i = 0, len = lst.length; i < len; ++i)
            cb(lst[i], i, lst);
    else 
        for (var i in lst) 
            cb(lst[i], i, lst);
}
Last Updated 09/19/09

 
JoeSimmons Scriptwright

This is a short one I use. I like to keep it all in one function for all types of results.
You can specify a certain expression, type, and node without having to append numberValue, stringValue, booleanValue, or singleNodeValue to the end of the xpath.

function xp(exp, t, n) {
var x = document.evaluate(exp||"//body",n||document,null,t||6,null);
if(t && t>-1 && t<10) switch(t) {
case 1: x=x.numberValue; break;
case 2: x=x.stringValue; break;
case 3: x=x.booleanValue; break;
case 8: case 9: x=x.singleNodeValue; break;
default: break;
} return x;
}

 
Johan Sundström Scriptwright

The most important thing to fix is to bring down code verbosity so it actually gets at-a-glance readable. All of the above examples constitute improvement towards that end.

 
Photodeus Scriptwright

Only issue I had was with the $x and $X naming.. Makes it really for me to read code.
Personally I'd prefer a simple $x for single node results and $x2 or $xx for multiple node results :)
Making helper functions too short is not a good solution either :)

Cross
Presentational HTML allowed.
Use <code> for inline code and <pre> for code blocks. Use &lt; and &gt; for literal < and >.
We help break paragraphs and link your links.
or cancel