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>
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 have XML that contains sibling nodes that have identical attribute values, but have different contents. This occurs at both the parent and the child level, as follows:
<myxml>
<a myattr="valuetop1">
<b myattr="valuechild1">
<c>Stuff1</c>
<c>Stuff2</c>
</b>
</a>
<a myattr="valuetop1">
<b myattr="valuechild2">
<c>Stuff3</c>
</b>
</a>
<a myattr="valuetop1">
<b myattr="valuechild2">
<c>Stuff4</c>
</b>
</a>
<a myattr="valuetop1">
<b myattr="valuechild2">
<c>Stuff5</c>
<c>Stuff6</c>
</b>
</a>
<a myattr="valuetop2">
<b myattr="valuechild1">
<c>Stuff1</c>
</b>
</a>
<a myattr="valuetop2">
<b myattr="valuechild3">
<c>Stuff2</c>
</b>
</a>
<a myattr="valuetop2">
<b myattr="valuechild2">
<c>Stuff3</c>
<c>Stuff2</c>
</b>
</a>
<a myattr="valuetop2">
<b myattr="valuechild2">
<c>Stuff4</c>
</b>
</a>
</myxml>
If there are nodes with identical attribute values that exist at the same level, I want to combine their contents under a single instance of that node. In other words, I'm looking for a neat hierarchy like this:
<myxml>
<a myattr="valuetop1">
<b myattr="valuechild1">
<c>Stuff1</c>
<c>Stuff2</c>
</b>
<b myattr="valuechild2">
<c>Stuff3</c>
<c>Stuff4</c>
<c>Stuff5</c>
<c>Stuff6</c>
</b>
</a>
<a myattr="valuetop2">
<b myattr="valuechild1">
<c>Stuff1</c>
</b>
<b myattr="valuechild3">
<c>Stuff2</c>
</b>
<b myattr="valuechild2">
<c>Stuff3</c>
<c>Stuff2</c>
<c>Stuff4</c>
</b>
</a>
</myxml>
The catch is that I don't know what the values of valuetopx or valuechildx will be. I've been banging my head over this one for a couple of days, but can't get my brain around it.
As mentioned in comments, you can use a technique called Muenchian Grouping in XSLT 1.0, but in your case you are doing it on two-levels.
First for the parent, you define the key like so
<xsl:key name="parent" match="a" use="#myattr" />
Then, for the child, you need to take into account both the parent ID and child ID (in the case where a child id may have different parent ids, and so would be a different group)
<xsl:key name="child" match="b" use="concat(../#myattr, '|', #myattr)" />
Then, to get the distinct parent ids, you do this....
<xsl:apply-templates select="a[generate-id() = generate-id(key('parent', #myattr)[1])]" />
And within a distinct parent, to get the distinct child elements, do this...
<xsl:apply-templates select="key('parent', #myattr)/b
[generate-id() = generate-id(key('child', concat(../#myattr, '|', #myattr))[1])]" />
Try this XSLT
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes" />
<xsl:key name="parent" match="a" use="#myattr" />
<xsl:key name="child" match="b" use="concat(../#myattr, '|', #myattr)" />
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*" />
</xsl:copy>
</xsl:template>
<xsl:template match="myxml">
<xsl:copy>
<xsl:apply-templates select="a[generate-id() = generate-id(key('parent', #myattr)[1])]" />
</xsl:copy>
</xsl:template>
<xsl:template match="a">
<xsl:copy>
<xsl:apply-templates select="#*" />
<xsl:apply-templates select="key('parent', #myattr)/b[generate-id() = generate-id(key('child', concat(../#myattr, '|', #myattr))[1])]" />
</xsl:copy>
</xsl:template>
<xsl:template match="b">
<xsl:copy>
<xsl:apply-templates select="#*" />
<xsl:apply-templates select="key('child', concat(../#myattr, '|', #myattr))/c" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
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.
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.
I have the following input XML file:
<root>
<a>
<b>1</b>
</a>
<c>
<d>
<e>2</e>
<f>3</f> or <e>3</e>
</d>
<g h="4"/>
<i>
<j>
<k>
<l m="5" n="6" o="7" />
<l m="8" n="9" o="0" />
</k>
</j>
</i>
</c>
</root>
I would like to use XSLT to transform it into the follow outputs:
OUTPUT 1
<root>
<row b="1" e="2" f="3" h="4" m="5" n="6" o="7" />
<row b="1" e="2" f="3" h="4" m="8" n="9" o="0" />
<root>
OUTPUT 2
<root>
<row b="1" e="2" h="4" m="5" n="6" o="7" />
<row b="1" e="2" h="4" m="8" n="9" o="0" />
<row b="1" e="3" h="4" m="5" n="6" o="7" />
<row b="1" e="3" h="4" m="8" n="9" o="0" />
<root>
Can anyone help my XSLT isn't very strong. Thanks.
It will be easier if you let the occurrence of <e> determine the outer loop constructing your <row>s and have a inner loop iterating over all <l>s.
Try something like this:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:apply-templates />
</xsl:template>
<xsl:template match="root">
<root>
<xsl:apply-templates />
</root>
</xsl:template>
<xsl:template match="e">
<!-- store values of 'e' and 'f' in params -->
<xsl:param name="value_of_e" select="." />
<xsl:param name="value_of_f" select="ancestor::d[1]/f" />
<!-- iterate over all 'l's -->
<xsl:for-each select="//l">
<xsl:element name="row">
<xsl:attribute name="b">
<xsl:value-of select="//b" />
</xsl:attribute>
<xsl:attribute name="e">
<xsl:value-of select="$value_of_e" />
</xsl:attribute>
<!-- only include 'f' if it has a value -->
<xsl:if test="$value_of_f != ''">
<xsl:attribute name="f">
<xsl:value-of select="$value_of_f" />
</xsl:attribute>
</xsl:if>
<xsl:attribute name="h">
<xsl:value-of select="ancestor::c[1]/g/#h" />
</xsl:attribute>
<xsl:attribute name="m">
<xsl:value-of select="./#m" />
</xsl:attribute>
<xsl:attribute name="n">
<xsl:value-of select="./#n" />
</xsl:attribute>
<xsl:attribute name="o">
<xsl:value-of select="./#o" />
</xsl:attribute>
</xsl:element>
</xsl:for-each>
</xsl:template>
<xsl:template match="b" />
<xsl:template match="f" />
<xsl:template match="g" />
</xsl:stylesheet>