Thursday, December 31, 2009

XPath Axis Selectors Implemented in JavaScript

Here is a quick implementation of the XPath axis selectors in JavaScript. Each function accepts a context node as a parameter, a filtering function, and a stop function. The two function arguments are optional and may be omitted.

If the filter function is present, it will be executed for each node encountered along the requested axis. It must return true for the node to be included in the output.

If the stop function is present, it will also be executed for each node encountered. When and if it returns true, the search along the axis will stop, and whatever nodes have been accumulated up to (and including) that point will be returned.

Here it is for download. Here is a minified version, only 2k!

This would be used like so:

var node = document.getElementById('myTable'), results;

// Find all descendant nodes
results = AXIS.descendant(node);

// Find all descendant text nodes
results = AXIS.descendant(node, function (n) {
  return (n.nodeType === 3);
});

// Find the closest FORM ancestor:
function isForm(n) {
  return (n.nodeName.toLowerCase() === 'form');
}
results = AXIS.ancestor(node, isForm, isForm)[0];

// Locate all H1 items between this node and the next table:
results = AXIS.following(node, function (n) {
  return (n.nodeName.toLowerCase() === 'h1');
}, function (n) {
  return (n.nodeName.toLowerCase() === 'table');
});

These are specifically written to operate on nodes, not elements. In other words, text nodes will be included as potential return values. This is great for me, as my first use for these is to assist in determining what text the user has selected. If the inclusion of text nodes were not a requirement, then one might consider optimizing the "descendant" axis to use querySelectorAll('*') instead--at least, in modern browsers.

This should work in all browsers. I did run into one snag with IE6. Apparently, if you have a <base> tag in the source, then the resulting tree structure ends up looking something like this:

<html>
    <head>
        <base>
            <body>...</body> <-- Same body element!
        </base>
    </head>
    <body>...</body>         <-- Same body element!
</html>

That <body> tag there is not a duplicate; it's really the same element, just included in two places in the DOM. It is both a descendant and a sibling of the <head> element. This causes an infinite loop when crawling the "following" axis, as the code crawls out of the <body> into the <base>, then into the <head>, then enters the <body> again.

Conditional compilation is used to fix this specifically for IE6, so the performance of other browsers should not be affected.