views:

619

answers:

3

I'm working on a Greasemonkey script which needs to operate on each node between two others. Currently, I'm getting the first node using an (increasingly-complicated) XPath expression. I have another expression to get the "between" nodes, but it contains the initial expression twice and is getting rather long. Here's an earlier version which only contained two "clauses":

var xpHeader   = "//h2[a/@name='section-References' or a/@name='References']";
var xpContents = "//h2[a/@name='section-References' or a/@name='References']/following-sibling::*[following-sibling::h2[1] = //h2[a/@name='section-References' or a/@name='References']/following-sibling::h2[1]]"

What I'm looking for is a way to select the "contents" based on a context node rather than reincluding the original expression multiple times — that "header" expression is going to get considerably more complex very quickly. I know this can be done in XSLT using the current() function, but of course that isn't available in vanilla XPath:

<xsl:template match="//h2[a/@name='section-References' or a/@name='References']">
    <xsl:for-each select="following-sibling::*[following-sibling::h2[1] = current()/following-sibling::h2[1]]">
        <!-- do stuff -->
    </xsl:for-each>
</xsl:template>

As I type this, it occurs to me that at this point, it would probably be easier to use the DOM to collect the contents rather than XPath, but I'm still interested to know if this is something that can be done.

The original version of the script is available on UserScripts.org.

+1  A: 

Although you're writing XPath expressions, to Javascript, they're just strings. You can concatenate strings.

var xpContents = xpHeader + "/following-sibling::*["
  + "following-sibling::h2[1] = " + xpHeader + "/following-sibling::h2[1]"
  + "]";

Now your header expression can get as complicated as you want without affecting the complexity of the assignment of content expression. The XPath evaluator will still have to parse the whole string, and if there is no optimization of the query, then it may get evaluated multiple times, but even that might be fast enough that it doesn't matter.

Rob Kennedy
That's actually not a bad thought. It certainly addresses the maintenance issue. :-)
Ben Blank
+1  A: 

You can use jQuery or another JavaScript framework to make working with the DOM easier.

Example:

// ==UserScript==
// @name           MyScript
// @namespace      http://example.com
// @description    Example
// @include        *
//
// @require     http://ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.min.js
// ==/UserScript==

$('h2[some crazy attributes...]').each(function() {
  // Do something
});

Check out the jQuery reference for more info on selecting and traversing DOM elements via XPath attributes.

jQuery Selectors

jQuery Traversal

jQuery next()

jQuery nextAll()

Daniel X Moore
+1  A: 

It sounds like using the DOM will be a bit easier, once you use XPath to collect things at that level. Assuming you use no framework, something along the lines of:

var nodes = document.evaluate("//h2[a/@name='section-References' or a/@name='References']/following-sibling::*", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
var contents=[];
if (nodes.snapshotLength>0) contents.push(new Array());
var currentGroup=0;
for (var i=0;i<nodes.snapshotLength;i++) {
  if (nodes.shapshotItem(i)==<your favorite way to detect the right flavor of h2 element>) {
    currentGroup++;
    contents.push(new Array());
    continue;
  }
  contents[currentGroup].push(nodes.snapshotItem(i));
}

It's a bit verbose, but you'll end up with an array of arrays of items between interesting h2 nodes.

Paul Marshall