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?
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>
</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,
'
'
),
''
)
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,
'
'
),
''
)
"/>
</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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With