XSLT Grouping/merging nodes without changing the order - xslt

I found a lot of grouping questions and they almost got me the wanted result. However, with these it was usually the case that the nodes to be grouped were already listed under a separate group node, so that the apply-templates could target exactly this parent node.
I have the following Input XML:
<ROOT>
<ARTICLE>
<IMAGE name="demo.jpg" />
<TABLE>
<TABLETHEAD>
Name1
</TABLETHEAD>
<TABLEROW>
<TABLETH>
Some Text
</TABLETH>
<TABLETD>
Some more Text
</TABLETD>
</TABLEROW>
</TABLE>
<TABLE>
<TABLETHEAD>
Name1
</TABLETHEAD>
<TABLEROW>
<TABLETH>
Demo Text
</TABLETH>
<TABLETD>
More Demo Text
</TABLETD>
</TABLEROW>
<TABLEROW>
<TABLETH>
Even more Text
</TABLETH>
<TABLETD>
Text
</TABLETD>
</TABLEROW>
</TABLE>
<GRAPHIC name="demo-graphic.eps" />
</ARTICLE>
</ROOT>
And this is the desired output:
<ROOT>
<ARTICLE>
<IMAGE name="demo.jpg" />
<TABLE>
<TABLETHEAD>
Name1
</TABLETHEAD>
<TABLEROW>
<TABLETH>
Some Text
</TABLETH>
<TABLETD>
Some more Text
</TABLETD>
</TABLEROW>
<TABLEROW>
<TABLETH>
Demo Text
</TABLETH>
<TABLETD>
More Demo Text
</TABLETD>
</TABLEROW>
<TABLEROW>
<TABLETH>
Even more Text
</TABLETH>
<TABLETD>
Text
</TABLETD>
</TABLEROW>
</TABLE>
<GRAPHIC name="demo-graphic.eps" />
</ARTICLE>
</ROOT>
The tables should be grouped by the same text in TABLETHEAD and the TABLEROWs should be added in each case, so that for each different TABLETHEAD text there is a single table with the TABLEROWs of all the tables belonging to the group.
The important thing is that the sibling nodes (e.g. IMG, GRAPHIC, and many more) remain and the whole order does not change. This is exactly my problem:
If my TABLEs were listed under a common parent node e.g. "TABLES", I could get on with a simple grouping similar to Applying Muenchian grouping for a simple XML with XSLT. So I either lose the sibling nodes or change the order.
I am using xslt 2.0.

With the help of Martin's comment (thanks a lot!) I came up with combining group-by and group-adjacent:
<xsl:template match="ARTICLE">
<xsl:copy>
<xsl:apply-templates select="#*" />
<xsl:for-each-group select="node()" group-adjacent="string(self::TABLE/TABLETHEAD)">
<xsl:for-each-group select="current-group()" group-by="string(self::TABLE/TABLETHEAD)">
<xsl:choose>
<xsl:when test="self::TABLE">
<TABLE>
<xsl:apply-templates select="current-group()[1]/#* | current-group()[1]/TABLETHEAD[1] | current-group()/TABLEROW" />
</TABLE>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()" />
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
This worked for me.

Related

Display a value from a preceding-sibling of a following sibling/child node?

Based on this XML I am trying to display a table with a row for each DOCREF that also shows the STEP2/TITLE where the REFERENCE equals the ID. I can make this work where the DOCREF/#REFERENCE = STEP2/#ID but where I am running into troubles is that it has been requested that the STEP2/TITLE also show for each STEP3 element until there is a new STEP2 element, and then it would show for all the STEP3's until it changes again and so on.
<WORKCARD>
<STSBODY>
<DOCREFS>
<DOCREF REFERENCE="123" VALUE="Ref1"/>
<DOCREF REFERENCE="456" VALUE="Ref2"/>
<DOCREF REFERENCE="789" VALUE="Ref3"/>
</DOCREFS>
</STSBODY>
<BODY>
<ITEMS>
<ITEM>
<XML>
<STEP2 ID="123">
<TITLE>Test1</TITLE>
</STEP2>
</XML>
</ITEM>
<ITEM>
<ITEMXML>
<XML>
<STEP3 ID=456>Step info goes here</STEP3>
</XML>
</ITEMXML>
</ITEM>
<ITEM>
<ITEMXML>
<XML>
<STEP2 ID=789>Test2</STEP3>
</XML>
</ITEMXML>
</ITEM>
</ITEMS>
</BODY>
</WORKCARD>
I am modifying an existing XSLT. Here is the section I am working with and you can see my various attempts that don't get me what I need.
<xsl:key name="step2Ref" match="STEP2" use="#ID" />
<xsl:when test="DOCREF[#TASK_CARD_ITEM > $item]">
<xsl:for-each select="DOCREF[#TASK_CARD_ITEM > $item and not($doctype = 'NDT' and key('refItem', #TASK_CARD_ITEM)/descendant::L1ITEM[#ID] and not(key('refItem',#TASK_CARD_ITEM)/descendant::L2ITEM))]">
<xsl:sort select="#TASK_CARD_ITEM" data-type="number" />
<!--<xsl:call-template name="subtaskitemrow"/>-->
<fo:table-row>
<fo:table-cell number-columns-spanned="6" border="solid 1pt red">
<fo:block>
<!-- this displays the correct info when there is a matching STEP2/#ID-->
<xsl:value-of select="key('step2Ref', #REFERENCE)/TITLE" />***
<!--This gets the STEP2/TITLE where STEP2/#ID = #REFERENCE-->
<xsl:value-of select="//STEP2/#ID" />+++
<xsl:value-of select="//ITEM/#TASK_CARD_ITEM" />***
<xsl:value-of select="#TASK_CARD_ITEM"/>+++
<xsl:variable name="tcitem"><xsl:value-of select="#TASK_CARD_ITEM" /></xsl:variable>
<xsl:value-of select="//ITEM[$tcitem]/#TASK_CARD_ITEM" />***
<!--preceding-sibling STEP2/#ID-->
</fo:block>
</fo:table-cell>
</fo:table-row>
</xsl:for-each>
</xsl:when>
Some notes I've written to myself are:
When looping through the DOCREFS I need to display the STEP2/TITLE for each one until the STEP2/TITLE changes and then display the new one.
I need to match STEP3's preceding-sibling STEP2's #ID
Please help?
ETA:
Desired output for this sample (changed text in xml slightly from original post) would be something like this:
Test1
Test1
Test2
So for the first and second DOCREFs it would show the title from the first STEP2 and the third DOCREF would show the title from the second STEP2.
Your XML document is not well-formed, and - more importantly - the second <STEP2> has no <TITLE>. Assuming it can be corrected to:
<WORKCARD>
<STSBODY>
<DOCREFS>
<DOCREF REFERENCE="123" VALUE="Ref1"/>
<DOCREF REFERENCE="456" VALUE="Ref2"/>
<DOCREF REFERENCE="789" VALUE="Ref3"/>
</DOCREFS>
</STSBODY>
<BODY>
<ITEMS>
<ITEM>
<XML>
<STEP2 ID="123">
<TITLE>Test1</TITLE>
</STEP2>
</XML>
</ITEM>
<ITEM>
<ITEMXML>
<XML>
<STEP3 ID="456">Step info goes here</STEP3>
</XML>
</ITEMXML>
</ITEM>
<ITEM>
<ITEMXML>
<XML>
<STEP2 ID="789">
<TITLE>Test2</TITLE>
</STEP2>
</XML>
</ITEMXML>
</ITEM>
</ITEMS>
</BODY>
</WORKCARD>
you can use the following logic to retrieve the data you need (for testing purposes, more is shown than required in your question):
<xsl:key name="step2Ref" match="STEP2" use="#ID" />
...
<xsl:template match="DOCREF">
<tr>
<!-- DOCREF VALUE-->
<td><xsl:value-of select="#VALUE" /></td>
<!-- DOCREF REFERENCE -->
<td><xsl:value-of select="#REFERENCE" /></td>
<xsl:variable name="ref">
<xsl:choose>
<xsl:when test="key('step2Ref', #REFERENCE)">
<xsl:value-of select="#REFERENCE" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="preceding-sibling::DOCREF[key('step2Ref', #REFERENCE)][1]/#REFERENCE" />
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<!-- REFERENCE USED FOR KEY -->
<td><xsl:value-of select="$ref" /></td>
<!-- RETRIEVED TITLE -->
<td><xsl:value-of select="key('step2Ref', $ref)/TITLE" /></td>
</tr>
</xsl:template>
In the above example, the following result is returned:
Ref1 123 123 Test1
Ref2 456 123 Test1
Ref3 789 789 Test2
Note that it is assumed that the first <DOCREF> does have a related <STEP2>.

<xsl:number> reset numbering

I have an XML file with a list of items with two different qualities and I need to create an HTML output that list the items in the two categories with a numbering sequence that start with a on both. I cannot find a solution. Here are the files I created so far:
XML
<?xml version="1.0" encoding="UTF-8"?>
<refrigerator>
<item>
<quality>Good</quality>
<item_name>eggs</item_name>
</item>
<item>
<quality>Good</quality>
<item_name>chess</item_name>
</item>
<item>
<quality>Good</quality>
<item_name>soda</item_name>
</item>
<item>
<quality>Bad</quality>
<item_name>chicken meat</item_name>
</item>
<item>
<quality>Bad</quality>
<item_name>spinach</item_name>
</item>
<item>
<quality>Bad</quality>
<item_name>potatoes</item_name>
</item>
</refrigerator>
XSL
<table width="100%" border="1">
<tr>
<td>
<strong>These are the good items in the refrigerator/strong>
<xsl:for-each select="refrigerator/item">
<xsl:if test="quality = 'Good'">
<strong><xsl:number format="a) " value="position()"/></strong>
<xsl:value-of select="item_name"/>
</xsl:if>
</xsl:for-each>
, <strong>and these are the bad ones/strong>
<xsl:for-each select="refrigerator/item">
<xsl:if test="quality = 'Bad'">
<strong><xsl:number format="a) " value="position()"/></strong>
<xsl:value-of select="item_name"/>
</xsl:if>
</xsl:for-each>
. Some more text over here.</td>
</tr>
</table>
HTML
These are the good items in the refrigerator:a) eggs b) chess c) soda , and these are the bad ones:d) chicken meat e) spinach f) potatoes . Some more text over here.
OUTPUT needed
These are the good items in the refrigerator:a) eggs b) chess c) soda , and these are the bad ones:a) chicken meat b) spinach c) potatoes . Some more text over here.
Any help is greatly appreciate it.
Regards.
A.
Your problem is that position() is sensitive to exactly what list of nodes you're currently for-eaching over. Instead of
<xsl:for-each select="refrigerator/item">
<xsl:if test="quality = 'Good'">
put the test in the for-each select expression
<xsl:for-each select="refrigerator/item[quality = 'Good']">
and similarly for the "Bad" case.
As Tomalak suggests you can save repeating the same code in the two cases by moving it to a separate template and using apply-templates instead of for-each.
Either: Use <xsl:for-each> correctly.
<xsl:template match="refrigerator">
<table width="100%" border="1">
<tr>
<td>
<strong>These are the good items in the refrigerator</strong>
<xsl:for-each select="item[quality = 'Good']">
<strong><xsl:number format="a) " value="position()"/></strong>
<xsl:value-of select="item_name" />
</xsl:for-each>
<xsl:text>, <xsl:text>
<strong>and these are the bad ones</strong>
<xsl:for-each select="item[quality = 'Bad']">
<strong><xsl:number format="a) " value="position()"/></strong>
<xsl:value-of select="item_name" />
</xsl:for-each>
<xsl:text>. Some more text over here.</xsl:text>
</td>
</tr>
</table>
</xsl:template>
Or, don't repeat yourself and don't use <xsl:for-each> at all.
<xsl:template match="refrigerator">
<table width="100%" border="1">
<tr>
<td>
<strong>These are the good items in the refrigerator</strong>
<xsl:apply-templates select="item[quality = 'Good']" mode="numbered" />
<xsl:text>, <xsl:text>
<strong>and these are the bad ones</strong>
<xsl:apply-templates select="item[quality = 'Bad']" mode="numbered" />
<xsl:text>. Some more text over here.</xsl:text>
</td>
</tr>
</table>
</xsl:template>
<xsl:template match="item" mode="numbered">
<div>
<strong><xsl:number format="a) " value="position()"/></strong>
<xsl:value-of select="item_name" />
</div>
</xsl:template>
Or, and this is even more preferred, use HTML numbered lists. Output <ol> and <li> and style them via CSS, instead of hard-coding list numbers in your output.

Trying to obtain first item after XSLT sort in Sitecore

I have XML that looks like this
<xml>
<news>
<newsitem>
<publishdate>2011-10-11</publishdate>
<title>Article 1</title>
<breakingnewsflag>false</breakingnewsflag>
</newsitem>
<newsitem>
<publishdate>2009-10-14</publishdate>
<title>Article 2</title>
<breakingnewsflag>true</breakingnewsflag>
</newsitem>
<newsitem>
<publishdate>2009-10-12</publishdate>
<title>Article 3</title>
<breakingnewsflag>true</breakingnewsflag>
</newsitem>
</news>
</xml>
Now what I want to do is obtain the most recent item by date, that has the breakingnewsflag set to true.
It seems I can filter on the flag when doing a for-each
<xsl:for-each select="sc:item('/sitecore/content/Home',.)/item[sc:fld('BreakingNewsStory',.)]">
and I can sort inside this collection
<xsl:sort select="sc:fld('PublishDate',.)" order="ascending" />
which leaves me with XSLT that looks like this...
<xsl:for-each select="sc:item('/sitecore/content/Home',.)/item[sc:fld('BreakingNewsStory',.)]">
<xsl:sort select="sc:fld('PublishDate',.)" order="ascending" />
Article Name: <xsl:value-of select="#name" /><br />
Title: <sc:text field="title"/> <br />
</xsl:for-each>
...but I cannot then obtain the first item.
I can obtain the first item after checking the flag by using this syntax
<xsl:for-each select="sc:item('/sitecore/content/Home',.)/item[sc:fld('BreakingNewsStory',.)][1]">
...but that will then grab it before the ordering.
I was about to try putting another foreach after the sort clause, to then obtain the first item, but I figured that was going to be needlessly complicated, and there had to be an easier way to do what I'm trying to do.
If I'm understanding this correctly, you could try using position() to get the first item :
<xsl:for-each select="sc:item('/sitecore/content/Home',.)/item[sc:fld('BreakingNewsStory',.)]">
<xsl:sort select="sc:fld('PublishDate',.)" order="ascending" />
<xsl:if test="position() = 1">
Article Name: <xsl:value-of select="#name" /><br />
Title: <sc:text field="title"/> <br />
</xsl:if>
</xsl:for-each>

XSLT 1.0 Grouping on multiple values on multiple levels

edited in response to comments *
Hello,
I am an XSLT noob and need some help. I am trying to do an filter/group combination with XSLT 1.0 (can't use XSLT 2.0 for this application).
Here is an example of the xml
<entry>
<item>
<name>Widget 2</name>
<rank>2</rank>
<types>
<type>Wood</type>
<type>Fixed</type>
<type>Old</type>
</types>
</item>
<item>
<name>Widget 1</name>
<rank>2</rank>
<types>
<type>Metal</type>
<type>Broken</type>
<type>Old</type>
</types>
</item>
<item>
<name>Widget 3</name>
<rank>1</rank>
<types>
<type>Metal</type>
<type>New</type>
</types>
</item>
</entry>
Now what I want to do is output html where I get a subset of the XML based on <type> and then group on rank. For example, if the user selects all items with the type Metal, the output should be:
<p class="nospace"><font color="#800000">
<b>Rank 1</b></font></p>
<li id="mylist"><b>Widget 3</b></li>
<br\>
<p class="nospace"><font color="#800000">
<b>Rank 2</b></font></p>
<li id="mylist"><b>Widget 1</b></li>
<br\>
of if the user user chooses the type Old the output would be
<p class="nospace"><font color="#800000">
<b>Rank 2</b></font></p>
<li id="mylist"><b>Widget 1</b></li>
<li id="mylist"><b>Widget 2</b></li>
<br\>
I can group using keys on rank along easily enough, but trying to do both is not working. Here is a sample of the xslt I have tried:
<xsl:param name="typeParam"/>
<xsl:key name="byRank" use="rank" match="item"/>
<xsl:for-each select="item[count(.|key('byRank',rank)[1])=1]">
<xsl:sort data-type="number" select="rank"/>
<xsl:for-each select="key('byRank',rank)">
<xsl:sort select="name"/>
<xsl:if test="count(rank)>0">
<p class="nospace"><font color="#800000"><b>Rank<xsl:value-of select="rank"/></b></font></p>
<xsl:for-each select="types[types=$typeParam]">
<li id="mylist"><b><xsl:value-of select="../name"/></b></li>
</xsl:for-each>
<br/>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
The result I get from this is I do indeed get the subset of my xml that I want but it also displays all of the various rank values. I want to limit it to just the ranks of the type that is specified in $typeParam.
I have tried moving the for-each statement to earlier in the code as well as modifying the if statement to select for $typeParam but neither works. I have also tried concat-ing my key with rank and type but that doesn't seem to work either (It only works if the type in $typeParam is the first child under types).
Thanks
jeff
This stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="kItemByRank" match="item" use="rank"/>
<xsl:param name="pType" select="'Old'"/>
<xsl:template match="entry">
<xsl:for-each select="item[count(.|key('kItemByRank',rank)[1])=1]">
<xsl:sort select="rank" data-type="number"/>
<xsl:variable name="vGroup" select="key('kItemByRank',rank)[
types/type = $pType
]"/>
<xsl:if test="$vGroup">
<p class="nospace">
<font color="#800000">
<b>
<xsl:value-of select="concat('Rank ',rank)"/>
</b>
</font>
</p>
<xsl:apply-templates select="$vGroup">
<xsl:sort select="name"/>
</xsl:apply-templates>
<br/>
</xsl:if>
</xsl:for-each>
</xsl:template>
<xsl:template match="item">
<li id="mylist">
<b>
<xsl:value-of select="name"/>
</b>
</li>
</xsl:template>
</xsl:stylesheet>
Output:
<p class="nospace">
<font color="#800000">
<b>Rank 1</b>
</font>
</p>
<li id="mylist">
<b>Widget 3</b>
</li>
<br />
<p class="nospace">
<font color="#800000">
<b>Rank 2</b>
</font>
</p>
<li id="mylist">
<b>Widget 1</b>
</li>
<br />
And whit pType param set to 'Old', output:
<p class="nospace">
<font color="#800000">
<b>Rank 2</b>
</font>
</p>
<li id="mylist">
<b>Widget 1</b>
</li>
<li id="mylist">
<b>Widget 2</b>
</li>
<br />

Including an attribute of an unrelated element in an XPath

I have the following XML file:
<phonebook>
<departments>
<department id="1" parent="" title="Rabit Hole" address="" email="" index=""/>
<department id="2" parent="" title="Big Pond" address="" email="" index=""/>
</departments>
<employees>
<employee id="1" fname="Daffy" lname="Duck" title="Admin" email="daffy.duck#example.com" department="2" room="" />
<employee id="2" fname="Bugs" lname="Bunny" title="Programmer" email="bugs.bunny#example.com" department="1" room="" />
</employees>
</phonebook>
When displaying it, I want to show the contact details for an employee as well as the title of the department where he works. Here's what I've got in the template:
<xsl:for-each select="phonebook/employees/employee">
<xsl:sort select="#lname" />
<tr>
<td>
<span class="lname"><xsl:value-of select="#lname"/></span>
<xsl:text> </xsl:text>
<span class="fname"><xsl:value-of select="#fname"/></span>
</td>
<td><xsl:value-of select="#title"/></td>
<td>
<xsl:value-of select="/phonebook/departments/department[#id='{#department}']/#title"/>
</td>
<td><xsl:value-of select="#email"/></td>
</tr>
</xsl:for-each>
The problem is that the following rule doesn't seem to work:
<xsl:value-of select="/phonebook/departments/department[#id='{#department}']/#title"/>
I guess this is because the XSLT engine looks for the department property in the department element, and not in the employee element. However, I don't have an idea how to fix it. Could anyone give me a hint on that?
Lots of ways, but a nice reusable one is to use a key.
Definition:
<xsl:key name="dept" match="/phonebook/departments/department" use="#id"/>
Usage (where the current node is an <employee>:
<xsl:value-of select="key('dept', #department)/#title"/>
The {} in the select are not supported, I prefer using a variable, but anothor option would be using current(). But I would probably just use something like:
<xsl:variable name="departmentId" select="#department" />
<xsl:value-of select="/phonebook/departments/department[#id=$departmentId]/#title"/>