38

Here is an excerpt of my xml :

<node/>
<node/>
<node id="1">content</node>
<node/>
<node/>
<node/>
<node id="2">content</node>
<node/>
<node/>

I am positioned in the node[@id='1']. I need an Xpath to match all the <node/> elements until the next not empty node (here node[@id='2']).


Edit: the @id attributes are only to explain my problem more clearly, but are not in my original XML. I need a solution which does not use the @id attributes.


I do not want to match the empty siblings after node[@id='2'], so I can't use a naive following-sibling::node[text()=''].

How can I achieve this ?

glmxndr
  • 45,516
  • 29
  • 93
  • 118
  • See also: http://stackoverflow.com/questions/2165566/xslt-select-following-sibling-until-reaching-a-specified-tag – WBT Jul 09 '12 at 21:56
  • This might be of use: [http://stackoverflow.com/questions/2063619/how-to-reformat-xml-with-group-adjacent-xslt](http://stackoverflow.com/questions/2063619/how-to-reformat-xml-with-group-adjacent-xslt) – Igor Nadj Jan 29 '10 at 13:25

3 Answers3

23

You could do it this way:

../node[not(text()) and preceding-sibling::node[@id][1][@id='1']]

where '1' is the id of the current node (generate the expression dynamically).

The expression says:

  • from the current context go to the parent
  • select those child nodes that
  • have no text and
  • from all "preceding sibling nodes that have an id" the first one must have an id of 1

If you are in XSLT you can select from the following-sibling axis because you can use the current() function:

<!-- the for-each is merely to switch the current node -->
<xsl:for-each select="node[@id='1']">
  <xsl:copy-of select="
    following-sibling::node[
      not(text()) and
      generate-id(preceding-sibling::node[@id][1])
      =
      generate-id(current())
    ]
  " />
</xsl:for-each>

or simpler (and more efficient) with a key:

<xsl:key 
  name="kNode" 
  match="node[not(text())]" 
  use="generate-id(preceding-sibling::node[@id][1])"
/>

<xsl:copy-of select="key('kNode', generate-id(node[@id='1']))" />
Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • I finally went another route, because I'm outside of XSLT, so I select all the following-sibling nodes, and iterate on them, and stop my loop when I encounter the next not empty. I accept your answer as the most thorough, because I now believe there is no one-liner in XPath to do what I asked. – glmxndr Feb 03 '10 at 08:09
  • @subtenante: Erm - but there *is* an XPath one-liner that does that right in my answer?! – Tomalak Feb 03 '10 at 08:16
  • yes, you are right, my question in not really clear about the fact that the id attributes I have put were only for show and explaining the problem. I actually have not the id attributes in my XML. – glmxndr Feb 03 '10 at 08:27
  • 11
    @subtenante: That's why you should never make up your code samples when you want a real problem solved. – Tomalak Feb 03 '10 at 09:54
13

Simpler than the accepted answer:

//node[@id='1']/following-sibling::node[following::node[@id='2']]
  • Find a node anywhere whose id is '1'
  • Now find all the following sibling node elements
  • ...but only if those elements also have a node with id="2" somewhere after them.

Shown in action with a more clear test document (and legal id values):

xml = '<root>
<node id="a"/><node id="b"/>
<node id="c">content</node>
<node id="d"/><node id="e"/><node id="f"/>
<node id="g">content</node>
<node id="h"/><node id="i"/>
</root>'

# A Ruby library that uses libxml2; http://nokogiri.org
require 'nokogiri'; doc = Nokogiri::XML(xml)

expression = "//node[@id='c']/following-sibling::node[following::node[@id='g']]"
puts doc.xpath(expression)
#=> <node id="d"/>
#=> <node id="e"/>
#=> <node id="f"/>
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • Why do you chose the `following` axis here? That may give false positives further down the road, if the document is larger (besides from the following-axis being more slower than any other axis). – Abel Oct 17 '15 at 22:53
8

XPath 2.0 has the operators '<<' and '>>' where node1 << node2 is true if node1 precedes node2 in document order. So based on that with XPath 2.0 in an XSLT 2.0 stylesheet where the current node is the node[@id = '1'] you could use

  following-sibling::node[not(text()) and . << current()/following-sibling::node[@od][1]]

That also needs the current() function from XSLT, so that is why I said "with XPath 2.0 in an XSLT 2.0 stylesheet". The syntax above is pure XPath, in an XSLT stylesheet you would need to escape '<<' as '&lt;&lt;'.

Martin Honnen
  • 160,499
  • 6
  • 90
  • 110