I apply templates with variable in select attribute which contains part of a tree. From that I call another apply templates with following-sibling:: construction, but it applies to all tree. For example:
<a>
<b id="1" ol="1" />
<b id="2" ol="0" />
<b id="3" ol="0" />
<b id="4" ol="1" />
<b id="5" ol="0" />
<b id="6" ol="0" />
<b id="7" ol="1" />
<b id="8" ol="0" />
<b id="9" ol="0" />
<b id="10" ol="1" />
<b id="11" ol="0" />
<b id="12" ol="0" />
<b id="13" ol="1" />
<b id="14" ol="0" />
<b id="15" ol="0" />
<b id="16" ol="1" />
</a>
...
<xsl:variable name="part" select="b[#ol = 1] />
<xsl:apply-templates mode="top" select="$part[position() mod 3 = 1]" />
...
<xsl:template mode="top" match="*">
<tr>
<xsl:apply-template mode="inner" select=".|following-sibling::b[not(position() > 2)]" />
</tr>
<xsl:template>
<xsl:template mode="inner" match="*">
<p><xsl:value-of select="#id" /></p>
<xsl:template>
What I expect is
<tr><p>1</p><p>4</p><p>7</p></tr>
<tr><p>10</p><p>13</p><p>16</p></tr>
What I have got
<tr><p>1</p><p>2</p><p>3</p></tr>
<tr><p>10</p><p>11</p><p>12</p></tr>
So why did template "top" change context to complete tree instead of $part while applying following-sibling? And how to get expected variant?
XPath selects nodes in an input tree, it does never change that input tree. So selecting some nodes does not in any way change the structure and relations in the tree, the sibling or children or ancestors remain the same. If you want to manipulate a tree use XSLT or XQuery. As you already use XSLT, with XSLT 1.0 you would need to write templates to create a result tree fragment with the new structure, then after applying an extension function like exsl:node-set you can process the intermediate tree. With XSLT 2.0 you don't need the extension function but you need to construct an intermediate tree.
To achieve the output you want you could use
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output indent="yes" method="html"/>
<xsl:template match="a">
<xsl:variable name="part" select="b[#ol = 1]" />
<xsl:apply-templates mode="top" select="$part[position() mod 3 = 1]" />
</xsl:template>
<xsl:template mode="top" match="*">
<tr>
<xsl:apply-templates mode="inner" select=".|following-sibling::b[#ol = 1][not(position() > 2)]" />
</tr>
</xsl:template>
<xsl:template mode="inner" match="*">
<p><xsl:value-of select="#id" /></p>
</xsl:template>
</xsl:stylesheet>
with that XSLT stylesheet Saxon 6.5.5 transforms
<a>
<b id="1" ol="1" />
<b id="2" ol="0" />
<b id="3" ol="0" />
<b id="4" ol="1" />
<b id="5" ol="0" />
<b id="6" ol="0" />
<b id="7" ol="1" />
<b id="8" ol="0" />
<b id="9" ol="0" />
<b id="10" ol="1" />
<b id="11" ol="0" />
<b id="12" ol="0" />
<b id="13" ol="1" />
<b id="14" ol="0" />
<b id="15" ol="0" />
<b id="16" ol="1" />
</a>
into
<tr>
<p>1</p>
<p>4</p>
<p>7</p>
</tr>
<tr>
<p>10</p>
<p>13</p>
<p>16</p>
</tr>
$part selects elements with #ol=1, namely the elements 1,4,7,10,13,16.
$part[position() mod 3 = 1] selects the items in $part whose position within $part is 1, 4, 7, ... That is, it selects the elements with ids 1 and 10.
You then apply templates to these, to output the groups of three elements starting with these two, which gives you the groups (1,2,3) and (10,11,12).
I think your mistake is probably in imagining that position() returns the position of an element in the tree, rather than the position of the element in the list you are filtering.
Related
Context node:
<a>
<c refid="1" />
<c refid="2" />
<c refid="3" />
<c refid="4" />
<c refid="5" />
</a>
It gets the nodes referred to above, using a proprietary command:
<xsl:for-each select="get-a(#refid)">
<a id="1">
<f att1="C"/>
<f att2="I"/>
</a>
<a id="2">
<f att1="C"/>
<f att2="I"/>
</a>
<a id="3">
<!--doesn't have f att1-->
<f att2="I"/>
</a>
<a id="4">
<f att1="R"/>
<f att2="S"/>
</a>
<a id="5">
<f att1="G"/>
<f att2="I"/>
</a>
At present, I have it call a template within a for-each, but that will only do each node separately, obviously.
But it must process them first based on the att2 value (these are set values, always I or S, so no problem), and then within that, based on att1 value to produce something like below, the first P node being the problem:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0">
<!--xsl:output method="xml" indent="no" encoding="UTF-8"/-->
<!-- This takes the first asset (betting that it's correct...) and uses it to establish the parameters below -->
<xsl:variable name="rootSourceAssetXml">
<xsl:copy-of select="/asset[1]"/>
<!--xsl:copy-of select="./asset"/-->
</xsl:variable>
<xsl:template match="/">
<xsl:variable name="xml">
<O>
<xsl:for-each select="$rootSourceAssetXml/asset/child_asset_rel[#key='user.']">
<xsl:sort select="cs:get-asset(#child_asset)/f/#att2" order="ascending" data-type="text"/>
<xsl:sort select="cs:get-asset(#child_asset)/f/#att1" order="descending" data-type="text"/>
<xsl:variable name="getChapter">
<xsl:variable name="getChapterXML" select="cs:get-asset(#child_asset)"/>
<xsl:for-each select="$getChapterXML">
<xsl:if test="$getChapterXML/f/#att2='ebook'">
<xsl:copy-of select="$getChapterXML"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:call-template name="Group">
<xsl:with-param name="thingGroup" select="$getChapter"/>
<xsl:with-param name="resourceTypeWeb" select="'EBook'"/>
</xsl:call-template>
<xsl:variable name="getChapter">
<xsl:variable name="getChapterXML" select="cs:get-asset(#child_asset)"/>
<xsl:for-each select="$getChapterXML">
<xsl:if test="$getChapterXML/f att2='instructor'">
<xsl:copy-of select="$getChapterXML"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:call-template name="Group">
<xsl:with-param name="thingGroup" select="$getChapter"/>
<xsl:with-param name="resourceTypeWeb" select="'Instructor'"/>
</xsl:call-template>
</xsl:for-each>
</O>
</xsl:variable> <!-- Closes xml variable block -->
</xsl:template>
<xsl:template name="Group">
<xsl:param name="thingGroup"/>
<xsl:param name="resourceTypeWeb"/>
<xsl:variable name="eachAsset">
<xsl:for-each select="$thingGroup">
<xsl:copy-of select="$thingGroup/asset"/>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="productResourceDescriptionGroupWeb" select="$eachAsset/asset/asset_feature[#feature='XXX:product-resource-description-group-web']/#value_string"/>
<xsl:variable name="productResourceDescriptionDetailWeb" select="$eachAsset/asset/asset_feature[#feature='XXX:product-resource-description-detail-web']/#value_string"/>
<xsl:for-each select="$eachAsset/asset">
<P>
<xsl:attribute name="e"><xsl:value-of select="$resourceTypeWeb"/></xsl:attribute>
<xsl:attribute name="d"><xsl:value-of select="$productResourceDescriptionGroupWeb"/></xsl:attribute>
<xsl:call-template name="Detail">
<xsl:with-param name="resourceTypeWeb" select="$resourceTypeWeb"/>
<xsl:with-param name="topAssetTypeName" select="$topAssetTypeName"/>
<xsl:with-param name="thing" select="$eachAsset"/>
<xsl:with-param name="productResourceDescriptionDetailWeb" select="$productResourceDescriptionDetailWeb"/>
</xsl:call-template>
</P>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Should produce this:
<O>
<P d="C" e="I"> <!-- This C is not a set value but a string, and it could be anything. Once the nodes with the same attribute are isolated, it is fine to grab the att value from node in position 1-->
<id1>
<!-- other info via call template; this works -->
</id1>
<id2>
<!-- other info via call template; this works -->
</id2>
</P>
<P d="NULL" e="I">
<id3>
<!-- other info via call template; this works -->
</id3>
</P>
<P d="G" e="I">
<id5>
<!-- other info via call template; this works -->
</id5>
</P>
<P d="R" e="S">
<id4>
<!-- other info via call template; this works -->
</id4>
</P>
</O>
I have tried for-each-group calling a different template, and for-each with a sort for the value of att1, and other methods with no success.
This gives the right order, but I cannot bring nodes with same C value together:
<xsl:sort select="a/f/#att2"/>
<xsl:sort select="a/f/#att1"/>
The logic should be
for each <a> with same att2 value
for each <a> with same att1 value
output a single P with d=att1 value
then process nodes with same att1
I know XSLT can't "loop" the way I'm used to with Perl, but I feel there is some way to do this by grouping or sorting, I just can't find the right combination. I keep getting so close, but then can't complete it.
Many thanks in advance.
Your question is (still) very confusing. Consider the following simplified example:
Input XML
<root>
<a id="1">
<f att1="C"/>
<f att2="I"/>
</a>
<a id="2">
<f att1="C"/>
<f att2="I"/>
</a>
<a id="3">
<!--doesn't have f att1-->
<f att2="I"/>
</a>
<a id="4">
<f att1="R"/>
<f att2="S"/>
</a>
<a id="5">
<f att1="G"/>
<f att2="I"/>
</a>
</root>
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="root">
<root>
<xsl:for-each-group select="a" group-by="f/#att2">
<xsl:for-each-group select="current-group()" group-by="if (f/#att1) then f/#att1 else 'NULL'">
<P d="{current-grouping-key()}" e="{f/#att2}">
<xsl:for-each select="current-group()">
<abc id="{#id}"/>
</xsl:for-each>
</P>
</xsl:for-each-group>
</xsl:for-each-group>
</root>
</xsl:template>
</xsl:stylesheet>
Result
<?xml version="1.0" encoding="UTF-8"?>
<root>
<P d="C" e="I">
<abc id="1"/>
<abc id="2"/>
</P>
<P d="NULL" e="I">
<abc id="3"/>
</P>
<P d="G" e="I">
<abc id="5"/>
</P>
<P d="R" e="S">
<abc id="4"/>
</P>
</root>
This shows how to implement your stated logic:
for each <a> with same att2 value
for each <a> with same att1 value
output a single P with d=att1 value
then process nodes with same att1
and nothing else.
i would like to know how to get sum of nodes only when they satisfy a condition,having the xml:
<segma>
<conservacio>
<cons estat="A">Excel·lent conservació</cons>
<cons estat="B">Bona conservació</cons>
<cons estat="C">Regular conservació</cons>
<cons estat="D">Mala conservació</cons>
</conservacio>
<categories>
<categoria cat="1">Mobiliari</categoria>
<categoria cat="2">Alimentació</categoria>
<categoria cat="3">Roba</categoria>
</categories>
<clients>
<registre dni="111">Marti</registre>
<registre dni="222">Jana</registre>
<registre dni="333">Edu</registre>
<registre dni="444">Santi</registre>
<registre dni="555">Mia</registre>
<registre dni="666">Pau M</registre>
</clients>
<productes>
<prod id="1" cons="A" cat="1">
<nom>Cuna</nom>
<preu_nou>70</preu_nou>
<preu>30</preu>
<ofertes>
<oferta client="111" />
<oferta client="333" />
</ofertes>
<venta client="111" />
</prod>
<prod id="2" cons="B" cat="2">
<nom>Baby cook</nom>
<preu_nou>120</preu_nou>
<preu>60</preu>
<ofertes>
<oferta client="111" />
<oferta client="333" />
<oferta client="444" />
<oferta client="555" />
</ofertes>
<venta client="555" />
</prod>
<prod id="3" cons="A" cat="1">
<nom>Mama pata</nom>
<preu_nou>70</preu_nou>
<preu>5</preu>
<ofertes>
<oferta client="444" />
<oferta client="555" />
</ofertes>
<venta client="444" />
</prod>
<prod id="4" cons="B" cat="3">
<nom>Conjunt xandall</nom>
<preu_nou>40</preu_nou>
<preu>15</preu>
<ofertes>
<oferta client="222" />
<oferta client="555" />
</ofertes>
<venta client="222" />
</prod>
<prod id="5" cons="C" cat="3">
<nom>Pack 3 texans</nom>
<preu_nou>70</preu_nou>
<preu>25</preu>
<ofertes>
<oferta client="333" />
<oferta client="444" />
<oferta client="555" />
</ofertes>
<venta client="333" />
</prod>
<prod id="6" cons="A" cat="2">
<nom>Saca leches</nom>
<preu_nou>130</preu_nou>
<preu>70</preu>
<ofertes>
<oferta client="111" />
<oferta client="222" />
<oferta client="555" />
</ofertes>
<venta client="111" />
</prod>
<prod id="7" cons="C" cat="2">
<nom>Llet continuació</nom>
<preu_nou>11</preu_nou>
<preu>3</preu>
<ofertes>
</ofertes>
</prod>
</productes>
</segma>
I want to make a list of which products has a client bought(prod/venta/#client), and the sum of prices("preu") of the products:
Having an xsl like :
<h2>Ex 4</h2>
<table border="1">
<xsl:for-each select="//registre">
<xsl:variable name="dni" select="#dni"/>
<tr>
<td> <xsl:value-of select="."/></td>
<xsl:for-each select="//prod">
<xsl:if test="venta/#client=$dni">
<td><xsl:value-of select="nom"/></td>
</xsl:if>
</xsl:for-each>
I would like to get an output like :
client1 prod1 prod2 sum_of_price_prod1+prod2
client2 prod4 prod6 prod 7 sum_of_price_of(prod4+prod6+prod7)
Ex: Maria Baby Cook Texans 744.35
Sorry for the bad english.
You can define a variable to hold all the prod elements current...
<xsl:variable name="prod" select="//prod[venta/#client=$dni]" />
Then you can list select the prod elements like so...
<xsl:for-each select="$prod">
And to get the total sum you can do this...
<xsl:value-of select="sum($prod/preu)" />
Try this XSLT
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html" doctype-public="XSLT-compat" omit-xml-declaration="yes" encoding="UTF-8" indent="yes" />
<xsl:template match="/">
<h2>Ex 4</h2>
<table border="1">
<xsl:for-each select="//registre">
<xsl:variable name="dni" select="#dni"/>
<xsl:variable name="prod" select="//prod[venta/#client=$dni]" />
<tr>
<td><xsl:value-of select="."/></td>
<td>
<xsl:for-each select="$prod">
<xsl:value-of select="nom"/><br />
</xsl:for-each>
</td>
<td>
<xsl:value-of select="sum($prod/preu)" />
</td>
</tr>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>
Alternatively, you could use an xsl:key to look up the prod elements
<xsl:key name="prod" match="prod" use="venta/#client" />
Try this XSLT too
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html" doctype-public="XSLT-compat" omit-xml-declaration="yes" encoding="UTF-8" indent="yes" />
<xsl:key name="prod" match="prod" use="venta/#client" />
<xsl:template match="/">
<h2>Ex 4</h2>
<table border="1">
<xsl:for-each select="//registre">
<tr>
<td><xsl:value-of select="."/></td>
<td>
<xsl:for-each select="key('prod', #dni)">
<xsl:value-of select="nom"/><br />
</xsl:for-each>
</td>
<td>
<xsl:value-of select="sum(key('prod', #dni)/preu)" />
</td>
</tr>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>
Given siblings, some of which are <row> elements and some not, like this,
<h />
<row id='v' />
<a />
<b />
<row id='w' />
<d />
<row id='x' />
<row id='y' />
<f />
<r />
<row id='z' />
using xslt 1.0, I need to process them in order but to group the non-row ones together as I go, like this,
<notRow>
<h />
</notRow>
<row id='v' />
<notRow>
<a />
<b />
</notRow>
<row id='w' />
<notRow>
<d />
<row id='x' />
<row id='y' />
<notRow>
<f />
<r />
</notRow>
<row id='z' />
The first and last may or may not be <row> elements.
How?
It can be as short and simple as this (no need of calling templates several times, xsl:for-each, xsl:if). Here is the complete transformation:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kFollowing" match="*/*[not(self::row)]"
use="concat(generate-id(..), '+',
generate-id(preceding-sibling::row[1])
)"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template priority="2" match=
"*/*[not(self::row)
and
(preceding-sibling::*[1][self::row]
or not(preceding-sibling::*)
)]">
<notRow>
<xsl:copy-of select=
"key('kFollowing', concat(generate-id(..), '+',
generate-id(preceding-sibling::row[1])
))"/>
</notRow>
</xsl:template>
<xsl:template match="*/*[not(self::row)]"/>
</xsl:stylesheet>
When this transformation is applied on the provided XML (wrapped into a single top element to make it well-formed):
<t>
<h />
<row id='v' />
<a />
<b />
<row id='w' />
<d />
<row id='x' />
<row id='y' />
<f />
<r />
<row id='z' />
</t>
the wanted, correct result is produced:
<t>
<notRow>
<h/>
</notRow>
<row id="v"/>
<notRow>
<a/>
<b/>
</notRow>
<row id="w"/>
<notRow>
<d/>
</notRow>
<row id="x"/>
<row id="y"/>
<notRow>
<f/>
<r/>
</notRow>
<row id="z"/>
</t>
Update:
The OP has expressed an additional requirement that nodes need be processed by matching templates -- not just copied.
This requires only minimal change:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kFollowing" match="*/*[not(self::row)]"
use="concat(generate-id(..), '+',
generate-id(preceding-sibling::row[1])
)"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template priority="2" match=
"*/*[not(self::row)
and
(preceding-sibling::*[1][self::row]
or not(preceding-sibling::*)
)]">
<notRow>
<xsl:apply-templates mode="group" select=
"key('kFollowing', concat(generate-id(..), '+',
generate-id(preceding-sibling::row[1])
))"/>
</notRow>
</xsl:template>
<!-- This template can be replaced with whatever processing needed -->
<xsl:template match="*" mode="group">
<xsl:copy-of select="."/>
</xsl:template>
<xsl:template match="*/*[not(self::row)]"/>
</xsl:stylesheet>
The template that operates in mode "group" should be substituted with template(s) that implement the exact wanted processing. In this case it copies the matched element -- but in the real application any wanted processing would go here.
You might be able to do a trick with a key to group each non-row element by its preceding row (if there is one), or its parent element if not:
<xsl:key name="elementsFollowingRow"
match="*[not(self::row)]"
use="generate-id( (.. | preceding-sibling::row )[last()])" />
and define a named template to put in a notRow if the current element has any associated elements according to the key
<xsl:template name="addNotRow">
<xsl:if test="key('elementsFollowingRow', generate-id())">
<notRow>
<xsl:copy-of select="key('elementsFollowingRow', generate-id())" />
</notRow>
</xsl:if>
</xsl:template>
Then in the template where you're matching the parent element (the one that contains all these row and non-row elements you can do
<xsl:call-template name="addNotRow" />
<xsl:for-each select="row">
<xsl:copy-of select="." />
<xsl:call-template name="addNotRow" />
</xsl:for-each>
The first call-template outside the for-each will deal with any notRow that is required before the first row, and the call inside the for-each will put in any notRow required after the row in question.
This isn't pretty, but it works.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="t">
<xsl:if test="row[1]/preceding-sibling::*">
<notRow>
<xsl:for-each select="row[1]/preceding-sibling::*" >
<xsl:copy />
</xsl:for-each>
</notRow>
</xsl:if>
<xsl:for-each select="row">
<xsl:copy-of select="."/>
<xsl:if test="following-sibling::row[1]/preceding-sibling::*[generate-id(preceding-sibling::row[1])=generate-id(current())]">
<notRow>
<xsl:for-each select="following-sibling::row[1]/preceding-sibling::*[generate-id(preceding-sibling::row[1])=generate-id(current())]">
<xsl:copy />
</xsl:for-each>
</notRow>
</xsl:if>
</xsl:for-each>
<xsl:if test="row[last()]/following-sibling::*">
<notRow>
<xsl:for-each select="row[last()]/following-sibling::*" >
<xsl:copy />
</xsl:for-each>
</notRow>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
On this XML source
<t>
<h />
<i />
<row id='v' />
<a />
<b />
<row id='w' />
<d />
<row id='x' />
<row id='y' />
<f />
<r />
<row id='z' />
<i />
</t>
it returns the correct result:
<notRow>
<h/>
<i/>
</notRow>
<row id="v"/>
<notRow>
<a/>
<b/>
</notRow>
<row id="w"/>
<notRow>
<d/>
</notRow>
<row id="x"/>
<row id="y"/>
<notRow>
<f/>
<r/>
</notRow>
<row id="z"/>
<notRow>
<i/>
</notRow>
but it does seem there should be something simpler.
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>
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.
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
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>
I'm trying to build a multi-level dropdrown CSS menu for a website I'm doing on the umbraco content management system.
I need to build it to have the following structure:
<ul id="nav">
<li>Page #1</li>
<li>
Page #2
<ul>
<li>Subpage #1</li>
<li>Subpage #2</li>
</ul>
</li>
</ul>
So now I'm trying to figure out how to do the nesting using XSLT. This is what I have so far:
<xsl:output method="xml" omit-xml-declaration="yes"/>
<xsl:param name="currentPage"/>
<!-- update this variable on how deep your menu should be -->
<xsl:variable name="maxLevelForMenu" select="4"/>
<xsl:template match="/">
<ul id="nav">
<xsl:call-template name="drawNodes">
<xsl:with-param
name="parent"
select="$currentPage/ancestor-or-self::node [#level=1]"
/>
</xsl:call-template>
</ul>
</xsl:template>
<xsl:template name="drawNodes">
<xsl:param name="parent"/>
<xsl:if test="umbraco.library:IsProtected($parent/#id, $parent/#path) = 0 or (umbraco.library:IsProtected($parent/#id, $parent/#path) = 1 and umbraco.library:IsLoggedOn() = 1)">
<xsl:for-each select="$parent/node [string(./data [#alias='umbracoNaviHide']) != '1' and #level <= $maxLevelForMenu]">
<li>
<a href="{umbraco.library:NiceUrl(#id)}">
<xsl:value-of select="#nodeName"/>
</a>
<xsl:if test="count(./node [string(./data [#alias='umbracoNaviHide']) != '1' and #level <= $maxLevelForMenu]) > 0">
<xsl:call-template name="drawNodes">
<xsl:with-param name="parent" select="."/>
</xsl:call-template>
</xsl:if>
</li>
</xsl:for-each>
</xsl:if>
</xsl:template>
What I can't seem to figure out is how to check if the first level (here Page #1 and Page #2) has any children, and if they do add the extra <ul> to contain the <li> children.
Anyone out there to point me in the right direction?
First off, no need pass the a parent parameter around. The context will transport this information.
Here is the XSL stylesheet that should solve your problem:
<!-- update this variable on how deep your menu should be -->
<xsl:variable name="maxLevelForMenu" select="4"/>
<!--- match the document root --->
<xsl:template match="/root">
<div id="nav">
<xsl:call-template name="SubTree" />
</div>
</xsl:template>
<!-- this will be called by xsl:apply-templates -->
<xsl:template match="node">
<!-- the node is either protected, or the user is logged on (no need to check for IsProtected twice) -->
<xsl:if test="umbraco.library:IsProtected($parent/#id, $parent/#path) = 0 or umbraco.library:IsLoggedOn() = 1">
<li>
<xsl:value-of select="#nodeName"/>
<xsl:call-template name="SubTree" />
</li>
</xsl:if>
</xsl:template>
<xsl:template name="SubTree">
<!-- render sub-tree only if there are any child nodes --->
<xsl:if test="node">
<ul>
<xsl:apply-templates select="node[data[#alias='umbracoNaviHide'] != '1'][#level <= $maxLevelForMenu]">
<!-- ensure sorted output of the child nodes --->
<xsl:sort select="#sortOrder" data-type="number" />
</xsl:apply-templates>
</ul>
</xsl:if>
</xsl:template>
This is the XML I tested it on (I don't know much about Umbraco, but after looking at some samples I hope I got close to an Umbraco document):
<root id="-1">
<node id="1" level="1" sortOrder="1" nodeName="Page #1">
<data alias="umbracoNaviHide">0</data>
</node>
<node id="2" level="1" sortOrder="2" nodeName="Page #2">
<data alias="umbracoNaviHide">0</data>
<node id="3" level="2" sortOrder="2" nodeName="Subpage #2.2">
<data alias="umbracoNaviHide">0</data>
</node>
<node id="4" level="2" sortOrder="1" nodeName="Subpage #2.1">
<data alias="umbracoNaviHide">0</data>
<node id="5" level="3" sortOrder="3" nodeName="Subpage #2.1.1">
<data alias="umbracoNaviHide">0</data>
</node>
</node>
<node id="6" level="2" sortOrder="3" nodeName="Subpage #2.3">
<data alias="umbracoNaviHide">1</data>
</node>
</node>
<node id="7" level="1" sortOrder="3" nodeName="Page #3">
<data alias="umbracoNaviHide">1</data>
</node>
</root>
This is the output:
<div id="nav">
<ul>
<li>Page #1</li>
<li>Page #2
<ul>
<li>Subpage #2.1
<ul>
<li>Subpage #2.1.1</li>
</ul>
</li>
<li>Subpage #2.2</li>
</ul>
</li>
</ul>
</div>
There is nothing very special about this problem. The following solution tests that the node-list for <xsl:apply-templates/>
is not empty, before applying the templates:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes"/>
<xsl:variable name="vLevel" select="0"/>
<xsl:template match="root">
<xsl:variable name="vnextLevelNodes"
select="node[#level = $vLevel+1]"/>
<xsl:if test="$vnextLevelNodes">
<ul>
<xsl:apply-templates select="$vnextLevelNodes"/>
</ul>
</xsl:if>
</xsl:template>
<xsl:template match="node">
<!-- the node is either protected, or the user is logged on (no need to check for IsProtected twice) -->
<!-- <xsl:if test=
"umbraco.library:IsProtected($parent/#id, $parent/#path) = 0
or
umbraco.library:IsLoggedOn() = 1"> -->
<xsl:if test="1">
<li>
<!-- <a href="{umbraco.library:NiceUrl(#id)}"> -->
<a href="'umbraco.library:NiceUrl(#id)'">
<xsl:value-of select="#nodeName"/>
</a>
<xsl:variable name="vnextLevelNodes"
select="node[#level = current()/#level+1]"/>
<xsl:if test="$vnextLevelNodes">
<ul>
<xsl:apply-templates select="$vnextLevelNodes"/>
</ul>
</xsl:if>
</li>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
I have used the following XML source document:
<root id="-1">
<node id="1" level="1" sortOrder="1" nodeName="Page #1">
<data alias="umbracoNaviHide">0</data>
</node>
<node id="2" level="1" sortOrder="2" nodeName="Page #2">
<data alias="umbracoNaviHide">0</data>
<node id="3" level="2" sortOrder="2" nodeName="Subpage #2.2">
<data alias="umbracoNaviHide">0</data>
</node>
<node id="4" level="2" sortOrder="1" nodeName="Subpage #2.1">
<data alias="umbracoNaviHide">0</data>
<node id="5" level="3" sortOrder="3" nodeName="Subpage #2.1.1">
<data alias="umbracoNaviHide">0</data>
</node>
</node>
<node id="6" level="2" sortOrder="3" nodeName="Subpage #2.3">
<data alias="umbracoNaviHide">1</data>
</node>
</node>
<node id="7" level="1" sortOrder="3" nodeName="Page #3">
<data alias="umbracoNaviHide">1</data>
</node>
</root>
Also, I have commented out any code referencing Umbraco extension functions, as I don't have access to them.
When the above transformation is applied on this source XML document, the correct, wanted result is produced:
<ul>
<li>
Page #1
</li>
<li>
Page #2
<ul>
<li>
Subpage #2.2
</li>
<li>
Subpage #2.1
<ul>
<li>
Subpage #2.1.1
</li>
</ul>
</li>
<li>
Subpage #2.3
</li>
</ul>
</li>
<li>
Page #3
</li>
</ul>
Hope this helped.
Cheers,
Dimitre Novatchev