5

This is a variant on the common request for an XPath to return all siblings until some condition, answered with characteristic fullness by Dimitre Novatchev at XPath axis, get all following nodes until using this pattern:

$x/following-sibling::p
   [1 = count(preceding-sibling::node()[name() = name($x)][1] | $x)]

But that pattern relies on the symmetry of following-sibling and preceding-sibling, on the ability to look in both directions along an axis.

Is there a comparable pattern when the axis is ancestor-or-self?

For example:

<t>
  <a xml:base="/news/" >
    <b xml:base="reports/">
      <c xml:base="politics/" />
      <c xml:base="sports/" >
        <d xml:base="reports/" />
        <d xml:base="photos/" >
          <img url="A1.jpg" />
          <img url="A2.jpg" />
        </d>
      </c>
      <c xml:base="entertainment" />
    </b>
  </a>
</t>

The straighforward

<xsl:template match="img">

    <xsl:for-each select="ancestor-or-self::*[@xml:base]">
        <xsl:value-of select="@xml:base"/>
    </xsl:for-each>

    <xsl:value-of select="@url"/>

</xsl:template>

would return

 /news/reports/sports/photos/A1.jpg
 /news/reports/sports/photos/A1.jpg

but if

      <c xml:base="sports/" >

were instead

      <c xml:base="/sports/" >

with that leading /, the for-each needs to stop, so as to return

 /sports/photos/A1.jpg
 /sports/photos/A2.jpg

How (in XSLT/XPath 1.0) to make it stop?

Community
  • 1
  • 1
JPM
  • 2,029
  • 2
  • 24
  • 27
  • My idea was to test for count(elements meeting the condition, from the starting element on up) = count(elements meeting the condition, from the candidate on up). But my attempts to code that failed. – JPM Jan 05 '13 at 01:51
  • What exactly nodes do you want to select? It isn't specified in the question. – Dimitre Novatchev Jan 05 '13 at 02:25
  • The ancestor nodes from the matching img element up to, but no further than, one that matches some condition, here an @xml:base that begins with a /. – JPM Jan 05 '13 at 02:52

4 Answers4

3

This XSLT 1.0 transformation:

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

 <xsl:param name="pWanted" select="//img"/>
 <xsl:param name="pWantedAttr" select="'url'"/>

 <xsl:template match="/">
     <xsl:apply-templates select="$pWanted"/>
 </xsl:template>

 <xsl:template match="*[not(starts-with(@xml:base, '/'))]">
  <xsl:apply-templates select="ancestor::*[@xml:base][1]"/>
  <xsl:value-of select="concat(@xml:base,@*[name()=$pWantedAttr])"/>
  <xsl:if test="not(@xml:base)"><xsl:text>&#xA;</xsl:text></xsl:if>
 </xsl:template>

 <xsl:template match="*[starts-with(@xml:base, '/')]">
  <xsl:value-of select="@xml:base"/>
 </xsl:template>
</xsl:stylesheet>

when applied to this XML document:

<t>
  <a xml:base="/news/" >
    <b xml:base="reports/">
      <c xml:base="politics/" />
      <c xml:base="/sports/" >
        <d xml:base="reports/" />
        <d xml:base="photos/" >
          <img url="A1.jpg" />
          <img url="A2.jpg" />
        </d>
      </c>
      <c xml:base="entertainment" />
    </b>
  </a>
</t>

produces the wanted, correct result:

/sports/photos/A1.jpg
/sports/photos/A2.jpg

Update -- A single XPath 2.0 expression solution:

   for $target in //img,
       $top in $target/ancestor::*[starts-with(@xml:base,'/')][1]
    return
      string-join(
         (
             $top/@xml:base
           , $top/descendant::*
                [@xml:base and . intersect $target/ancestor::*]
                   /@xml:base
           , $target/@url,
           '&#xA;'
        ),
        ''
                )

XSLT 2.0 - based verification:

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

 <xsl:template match="/">
     <xsl:sequence select=
      "for $target in //img,
           $top in $target/ancestor::*[starts-with(@xml:base,'/')][1]
        return
          string-join(
             (
                 $top/@xml:base
               , $top/descendant::*
                    [@xml:base and . intersect $target/ancestor::*]
                       /@xml:base
               , $target/@url,
               '&#xA;'
            ),
            ''
                    )
      "/>
 </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the provided XML document:

<t>
  <a xml:base="/news/" >
    <b xml:base="reports/">
      <c xml:base="politics/" />
      <c xml:base="sports/" >
        <d xml:base="reports/" />
        <d xml:base="photos/" >
          <img url="A1.jpg" />
          <img url="A2.jpg" />
        </d>
      </c>
      <c xml:base="entertainment" />
    </b>
  </a>
</t>

the XPath expression is evaluated and the result from this evaluation is copied to the output:

/news/reports/sports/photos/A1.jpg
 /news/reports/sports/photos/A2.jpg

With the modified document:

<t>
  <a xml:base="/news/" >
    <b xml:base="reports/">
      <c xml:base="politics/" />
      <c xml:base="/sports/" >
        <d xml:base="reports/" />
        <d xml:base="photos/" >
          <img url="A1.jpg" />
          <img url="A2.jpg" />
        </d>
      </c>
      <c xml:base="entertainment" />
    </b>
  </a>
</t>

again the wanted, correct result is produced:

/sports/photos/A1.jpg
 /sports/photos/A2.jpg

Update2:

The OP has suggested this simplification:

Update added by original poster: Once embedded in the full application, where the full url replaced the relative one, Dimitre's approach ended up being this simple

:

<xsl:template match="@url">
    <xsl:attribute name="url">
        <xsl:apply-templates mode="uri" select=".." />
        <xsl:value-of select="."/>
    </xsl:attribute>
</xsl:template>

<xsl:template match="*"  mode="uri">
    <xsl:if test="not(starts-with(@xml:base, '/'))">
        <xsl:apply-templates select="ancestor::*[@xml:base][1]" mode="uri"/>
    </xsl:if>
    <xsl:value-of select="@xml:base"/>
</xsl:template>
Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
  • Every time I think I'm finally grokking XSLT, Dimitre takes me to a whole new level! (I wonder why this kind of recursion works for the ancestor axis, but not with the sibling axis.) – JPM Jan 05 '13 at 03:08
  • @JPM, You are welcome. The reason things are slightly different for siblings is that there is no "preceding-sibling-or-self::" and no "following-sibling-or-self" axis. – Dimitre Novatchev Jan 05 '13 at 03:43
  • As mentioned in the OP, I need 1.0. As you can guess, I need a little routine to resolve relative URIs. So I'm now extending your nice 1.0 approach to handle the rest of the resolution. – JPM Jan 05 '13 at 17:24
  • @JPM, Yes, I know. I made this update to show what is a truly "single XPath expression" that also solves the problem, so that the reader would not be confused by claims that aren't really a "single XPath expression solution". – Dimitre Novatchev Jan 05 '13 at 17:32
  • @Dimitre, I suggested a postscript to your answer. Reject the addition if you want and I'll put it into the OP. – JPM Jan 05 '13 at 18:25
  • @JPM, This is a nice simplification. However, this won't work if not all the ancestors have the attribute `xml:base`. I will add a similar edit, clearly stating the assumption that all ancestors in the chain do have an `xml:base` attribute. – Dimitre Novatchev Jan 05 '13 at 18:53
  • @Dimitre, Why must all have xml:base? It's working fine for me when they don't. And with a couple small additions, I got a complete resolver of relative uri paths in a ten-line template. Shazam! – JPM Jan 05 '13 at 20:13
  • @Dimitre, you removed that comment where you said something nice about my improvement. Darn. That was the closest I figure I'll ever get to answering a SO question on XSLT! :) (Thanks again for your help.) – JPM Jan 06 '13 at 17:08
2

There is a way to select the right nodes in a single for-each select expression:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:template match="img">
    <xsl:for-each select="
      ancestor-or-self::*[
        starts-with(@xml:base, '/')
      ][1]/descendant-or-self::*[
        @xml:base and .//img[generate-id() = generate-id(current())]
      ]">
      <xsl:value-of select="@xml:base"/>
    </xsl:for-each>

    <xsl:value-of select="@url"/>
  </xsl:template>

</xsl:stylesheet>

Given this input XML:

<t>
  <a xml:base="/news/" >
    <b xml:base="reports/">
      <c xml:base="politics/" />
      <c xml:base="/sports/" >
        <d xml:base="reports/" />
        <d xml:base="photos/" >
          <img url="A1.jpg" />
          <img url="A2.jpg" />
        </d>
      </c>
      <c xml:base="entertainment" />
    </b>
  </a>
</t>

The correct result is produced:

/sports/photos/A1.jpg
/sports/photos/A2.jpg

The XPath expression could be read as "Beginning with the closest ancestor whose @xml:base starts with a slash, select that and all of its descendants who have the current <img> as one of their descendants."

This effectively selects exactly the one correct path down into the XML tree.

Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • Tomalak, "One single XPath expression" ??? I see *four* XPath expressions here, not to mention that they are part of complex XSLT processing. Please, correct your statement. It should be possible to produce the wanted results with a truly single XPath 2.0 expression. – Dimitre Novatchev Jan 05 '13 at 16:35
  • 1
    @Dimitre An XPath expression can contain more sub-expressions. It still is a single expression. I don't see anything to correct. As an aside, it is entirely unnecessary to post the same comment twice on this website. – Tomalak Jan 05 '13 at 16:45
  • I understood what was meant with "one", and I can see this being a fine approach in the right situation. So a +1 (though no checkmark) from me. I'd be concerned that this would not scale nicely to large trees, with that .//img in there. Or maybe a key on the id would alleviate that. – JPM Jan 05 '13 at 17:10
  • Tomalak, If this is "a single XPath expression", then please, post just a single XPath expression -- not a transformation with a template, `xsl:for-each` and `xsl:value-of`. For example, see the expression I am going to add to my answer in the next few minutes. – Dimitre Novatchev Jan 05 '13 at 17:12
  • @JPM I'm not sure a key would be possible. There is no value that you could use to identify the right descendant `` when your context is at one of its ancestors. You are right about your performance considerations, this will not scale nicely to very deep trees. I merely posted the solution as one that works in the spirit of the `following-sibling`/`preceding-sibling` expression in your question. – Tomalak Jan 05 '13 at 17:28
  • Tomalak, One has a "single XPath expression solution" when one can execute it with a DOM Evaluate() method, and/or with XQuery. Please, show us such a single XPath expression solution -- the code in your answer currently fails this test. – Dimitre Novatchev Jan 05 '13 at 17:36
  • @Dimitre Can you explain what, exactly, is the legal contents of the `` `select` attribute? Maybe I'm missing something here. – Tomalak Jan 05 '13 at 17:36
  • Tomalak, see my previous comment. To answer your question, the XPath expression specified in the `select` attribute of the `xsl:for-each` instruction in your code, *selects nodes* -- it doesn't produce the wanted (string) result. – Dimitre Novatchev Jan 05 '13 at 17:40
  • @Dimitre But you agree that it is one single XPath expression in the select attribute? Good. I'm not sure what we are arguing about, then. I did not imply that XPath was all you need, that's your interpretation of my words. But I've changed my wording. EOD. – Tomalak Jan 05 '13 at 17:49
  • Tomalak, To understand what "single XPath expression solution" means, see the update to my answer. This is an Xpath expression, which can be executed with a standalone Evaluate() (or similarly named) method from any PL. And/or it can be executed by XQuery. Most importantly, the result of the evaluation must be the exactly wanted result. So, no further processing of the result of the evaluation is required. The now modified text in your answer seems OK. – Dimitre Novatchev Jan 05 '13 at 18:05
0

I found it: Count the ancestors that meet the condition before going into the for-each loop. Test each candidate to see if the count of its condition-matching ancestors is the same.

This stylesheet

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="1.0">

    <xsl:template match="img">

        <xsl:variable name="distance" select="count(ancestor-or-self::*[@xml:base][substring(@xml:base,1,1)='/'])" />

        <xsl:for-each select="ancestor-or-self::*[@xml:base][
            count(ancestor-or-self::*[@xml:base][substring(@xml:base,1,1)='/'])=$distance
            ]">
                <xsl:value-of select="@xml:base"/>
        </xsl:for-each>

        <xsl:value-of select="@url"/>

    </xsl:template>

</xsl:stylesheet>

applied to the given XML

<t>
    <a xml:base="/news/" >
        <b xml:base="reports/">
            <c xml:base="politics/" />
            <c xml:base="/sports/" >
                <d xml:base="reports/" />
                <d xml:base="photos/" >
                    <img url="A1.jpg" />
                    <img url="A2.jpg" />
                </d>
            </c>
            <c xml:base="entertainment" />
        </b>
    </a>
</t>

returns the desired result:

                /sports/photos/A1.jpg
                /sports/photos/A2.jpg

Without the / before sports, the for-each matches ancestors all the way to /news:

                /news/reports/sports/photos/A1.jpg
                /news/reports/sports/photos/A2.jpg
JPM
  • 2,029
  • 2
  • 24
  • 27
0

With a minor change to your original template:

<xsl:template match="img">
    <xsl:for-each select="ancestor-or-self::*[@xml:base][not(.//*[starts-with(@xml:base, '/')])]">
         <xsl:value-of select="@xml:base"/>
    </xsl:for-each>
    <xsl:value-of select="@url"/>
</xsl:template>
Sergiu Dumitriu
  • 11,455
  • 3
  • 39
  • 62