Need an xpath that finds all the elements of a particular type before the first occurence of a certain element

Viewed 402

I need an xpath that fetches all the elements of a particular element type, say input, that occurs before the first occurrence of another element. the problem is, there is no proper hierarchy between the targeted elements and the 'another element'. and there can be any number of 'another element' present in the html.

i tried using the 'following' axes and it works if there is only one 'another element'. but if there are many it doesn't work

<a>
    <b>
        <input>zyx</input>
        <div>abc</div>
        <span>def</span>
        <input>ghi</input>
    </b>
    <c>
        <div class="SameAttribute">Test</div>
        <input>jkl</input>
        <div>mno</div>
    </c>
    <d>
        <div class="SameAttribute">Test</div>
        <input>pqr</input>
        <div>stu</div>
    </d>
</a>

as per the html structure above, i want only the input elements that are within the <b> tag. the xpath needs to ignore the input elements that are within <c> and <d> tags Tried this

.//*[self::input][following::div[@class = 'SameAttribute']]

but it picks the elements from both <b> and <c> tags.

When i try this, nothing gets selected

.//*[self::input][following::(div[@class = 'SameAttribute'])[1]]

I cannot write xpaths containing any of the tags <b>, <c>, <d> due to other constraints

5 Answers

i want only the input elements that are within the <b> tag. the xpath needs to ignore the input elements that are within <c> and <d> tags

Use:

//b//input

I need an xpath that fetches all the elements of a particular element type, say input, that occurs before the first occurrence of another element. the problem is, there is no proper hierarchy between the targeted elements and the 'another element'. and there can be any number of 'another element' present in the html.

This is not equivalent to the first requirement quoted above.

You don't specify what is mean't by "another element" but combining the two quoted requirements, and the provided source xml document, one can logically conclude that "another element" here means any following sibling of the element /a/b[1]

These will be selected by:

(//b)[1]//input

or for the provided xml document just:

/a/b[1]//input

If the document had more than one /a/b elements and you wanted to get the input descendants of only these /a/b/ elements that precede any /a/{X} elements, where {X} is a name different from b, use:

/a/b[not(preceding-sibling::*[not(self::b)])]//input

Finally, in the most general case, if you want to select the input descendants of only such b elements that come **before* any other (non-b) element (excluding the top element -- if the top element is a b then any input descendant of the top element satisfies the requirement, here is one XPath expression that selects these:

/*//b[not(ancestor::*[not(self::b) and parent::*]) 
    and not(preceding::*[not(self::b)])]
      //input

Here we use the fact that if an element x is before (in document order) an element y, then x is either an ancestor of y (belongs to itsancestor::* axis) or is a preceding element (belongs to its preceding::* axis)

XSLT-based verification:

This transformation evaluates all 5 XPath expressions and outputs the selected nodes:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

  <xsl:template match="/">
    <xsl:copy-of select="//b//input"/>
    ==================================
    <xsl:copy-of select="(//b)[1]//input"/>
    ==================================
    <xsl:copy-of select="/a/b[1]//input"/>
    ==================================
    <xsl:copy-of select="/a/b[not(preceding-sibling::*[not(self::b)])]//input"/>
    ==================================
    <xsl:copy-of select=
    "/*//b[not(ancestor::*[not(self::b) and parent::*])
        and not(preceding::*[not(self::b)])]
          //input"/>
  </xsl:template>
</xsl:stylesheet>

When applied on the originally-provided XML document:

<a>
    <b>
        <input>zyx</input>
        <div>abc</div>
        <span>def</span>
        <input>ghi</input>
    </b>
    <c>
        <div class="SameAttribute">Test</div>
        <input>jkl</input>
        <div>mno</div>
    </c>
    <d>
        <div class="SameAttribute">Test</div>
        <input>pqr</input>
        <div>stu</div>
    </d>
</a>

the wanted, correct result is selected when evaluating each expression:

<input>zyx</input>
<input>ghi</input>
    ==================================
    <input>zyx</input>
<input>ghi</input>
    ==================================
    <input>zyx</input>
<input>ghi</input>
    ==================================
    <input>zyx</input>
<input>ghi</input>
    ==================================
    <input>zyx</input>
<input>ghi</input>

You can try this xpath

This is for indexing all input (please change count number for other) :

(.//*[self::input][following::div[@class = 'SameAttribute']])[1]

This is for simple way, input between tag <b> :

//b//input

One Xpath that would seem to meet your criteria is:

//input[not(preceding-sibling::*[contains(@class,'SameAttribute')])]

This will find all input elements that do not have a preceding sibling that has a class attribute that contains the class SameAttribute.

The way you've described the problem, the simplest solution is //b/*. Alternatively, if you want all the elements with the same parent as the first input element, you might want (//input)[1]/following-sibling::*.

You certainly don't want the following axis here: read up on the difference between following and following-sibling.

Your expression //*[self::input] is a very convoluted way of saying //input.

I tried using the combination of preceding and ancestor axes to arrive at the solution. following is the xpath that worked for me
(.//div[@class='SameAttribute'])[1]/preceding::*[self::input][ancestor::a]

Related