1
votes

With XPath (.NET), I'm trying to select all nodes that don't contain any text node.

Given this document:

<root>
  <node1>
    <node1a>Node 1A</node1a>
  </node1>
  <node2>Node 2</node2>
  <node3>
    <node3a>Node 3A</node3a>
    <node3b></node3b>
  </node3>
  <node4></node4>
  <node5>
    <node5A></node5A>
  </node5>
</root>

I'm tyring to get the nodes:

<node3b></node3b>

<node4></node4>

<node5>
  <node5A></node5A>
</node5>

Note that overlapping subtrees are merged, so node5A should not be returned separately.

I would expect this to pull the trick, but for some reason (which is probably obvious when someone points it out) it doesn't:

//*[count(//text()) = 0]

Note: I'm using XPath tester to try things out.

4

4 Answers

2
votes

Arg... and just when posting, the solution crops up:

//*[count(.//text()) = 0]

Explanation: the condition count(//text()) = 0 counts all text nodes from the root, which is always greater than zero. To count from the current node, I needed to prefix the dot: count(.//text()) = 0

Note that @jvverde correctly remarks that nodes can occur multiple times in the result set. So this expression is not an exact match for the conditions I mention, as node5A is in there twice:

<node3b></node3b>

<node4></node4>

<node5>
  <node5A></node5A>
</node5>

<node5A></node5A>
1
votes

You could also use //*[.=''] as far as empty element should have empty string value.

1
votes

Assuming your result example is really what you want (which is not totally in accordance with statement in the title) the suggestions above

//*[count(.//text()) = 0]

or the preferred way

//*[not(.//text())]

Doesn't work as the result is not what you expected

<node3b />
<node4 />
<node5>
  <node5A />
</node5>
<node5A /> <!-- this node is not present in your example -->

If what you want is all subtrees without any text node not included in other resulting subtrees the solution is this one

//*[not(.//text())][not(ancestor::*[not(.//text())])]

The second predicate remove from the result all the nodes which has at least one ancestor already included in the result

0
votes

You can also use the more simple and readable

//*[not(.//text())]

or replace not(...) by empty(...) if you prefer.

Both are already optimized, so even simple XPath implementations should be able to implement them in a "fail-fast" manner (found one text node, evaluate predicate to false).