xsl to group nodes between other nodes? - xslt

I can't figure out how create xsl to group some nodes between other nodes. Basically, everytime I see a 'SPLIT' I have to end the div and create a new one.
The xml looks like this:
<data name="a" />
<data name="b" />
<data name="c" />
<data name="SPLIT" />
<data name="d" />
<data name="e" />
<data name="SPLIT" />
<data name="f" />
<data name="g" />
<data name="h" />
The output needs to look like this
<div>
a
b
c
</div>
<div>
d
e
</div>
<div>
f
g
h
</div>
I know how to do this by 'cheating', but would like to know if there is a proper way to do it:
<div>
<xsl:for-each select="data">
<xsl:choose>
<xsl:when test="#name='SPLIT'">
<xsl:text disable-output-escaping="yes"> </div> <div></xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="#name"/>
</xsl:otherwise>
</xsl:for-each>
</div>

This stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:template match="node()">
<xsl:apply-templates
select="node()[1]|following-sibling::node()[1]"/>
</xsl:template>
<xsl:template match="data">
<div>
<xsl:call-template name="open"/>
</div>
<xsl:apply-templates
select="following-sibling::data[#name='SPLIT'][1]
/following-sibling::node()[1]"/>
</xsl:template>
<xsl:template match="data" mode="open" name="open">
<xsl:value-of select="concat(#name,'
')"/>
<xsl:apply-templates select="following-sibling::node()[1]"
mode="open"/>
</xsl:template>
<xsl:template match="data[#name='SPLIT']" mode="open"/>
</xsl:stylesheet>
Output:
<div>
a
b
c
</div>
<div>
d
e
</div>
<div>
f
g
h
</div>
Note: Fine grained traversal.

This transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kFollowing" match="data[not(#name='SPLIT')]"
use="generate-id(preceding-sibling::data[#name='SPLIT'][1])"/>
<xsl:template match="node()|#*" name="identity">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="data[#name='SPLIT']" name="regularSplit">
<div>
<xsl:apply-templates mode="copy"
select="key('kFollowing', generate-id())"/>
</div>
</xsl:template>
<xsl:template match="data[#name='SPLIT'][
not(preceding-sibling::data[#name='SPLIT'])]">
<div>
<xsl:apply-templates mode="copy"
select="preceding-sibling::data"/>
</div>
<xsl:call-template name="regularSplit"/>
</xsl:template>
<xsl:template match="*" mode="copy">
<xsl:call-template name="identity"/>
</xsl:template>
<xsl:template match="data[not(#name='SPLIT')]"/>
</xsl:stylesheet>
when applied on the provided XML document (wrapped into a top element to become well-formed):
<t>
<data name="a" />
<data name="b" />
<data name="c" />
<data name="SPLIT" />
<data name="d" />
<data name="e" />
<data name="SPLIT" />
<data name="f" />
<data name="g" />
<data name="h" />
</t>
produces the wanted, correct result:
<t>
<div>
<data name="a"></data>
<data name="b"></data>
<data name="c"></data>
</div>
<div>
<data name="d"></data>
<data name="e"></data>
</div>
<div>
<data name="f"></data>
<data name="g"></data>
<data name="h"></data>
</div>
</t>
Do note: Keys are used to specify conveniently and verry efficiently all "non-SPLIT" elements that follow immediately a "SPLIT" element.

Related

Can I DRY this XSLT for nested categories?

XSLT available is 1.0.
The XML and XSLT below is for building a dropdown navigation menu for nested categories. The level of categories may vary.
Sample XML:
<data>
<categories-nav>
<section id="11" handle="categories-1">Categories 1</section>
<entry id="65">
<name handle="air-rifles">Air Rifles</name>
<subcategories field-id="50" subsection-id="12" items="2">
<item id="66" quantity="1">
<name handle="rifles">Rifles</name>
<active>Yes</active>
<subcategories field-id="57" subsection-id="13" items="2">
<item id="67" quantity="1">
<name handle="b2-series">B2 Series</name>
<active>Yes</active>
</item>
<item id="112" quantity="1">
<name handle="junior-supergrade">Junior Supergrade</name>
<active>Yes</active>
</item>
</subcategories>
</item>
<item id="111" quantity="1">
<name handle="accessories">Accessories</name>
<active>Yes</active>
<subcategories field-id="57" subsection-id="13" items="0" />
</item>
</subcategories>
</entry>
<entry id="118">
<name handle="pistols">Pistols</name>
</entry>
<entry id="58">
<name handle="bb-softair-guns">BB Softair Guns</name>
</entry>
</categories-nav>
</data>
My current XSLT:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template name="categories-nav-entries" mode="navigation">
<li class="{name/#handle}-{#id}">
<xsl:value-of select="name"/>
<xsl:apply-templates select="subcategories" mode="navigation"/>
</li>
</xsl:template>
<!-- level 1 -->
<xsl:template match="/data/categories-nav" mode="navigation">
<ul>
<xsl:apply-templates select="entry" mode="navigation"/>
</ul>
</xsl:template>
<xsl:template match="/data/categories-nav/entry" mode="navigation">
<xsl:call-template name="categories-nav-entries" mode="navigation"/>
</xsl:template>
<!-- level 2 -->
<xsl:template match="/data/categories-nav/entry/subcategories" mode="navigation">
<ul>
<xsl:apply-templates select="item" mode="navigation"/>
</ul>
</xsl:template>
<xsl:template match="/data/categories-nav/entry/subcategories/item" mode="navigation">
<xsl:call-template name="categories-nav-entries" mode="navigation"/>
</xsl:template>
<!-- level 3 -->
<xsl:template match="/data/categories-nav/entry/subcategories/item/subcategories" mode="navigation">
<ul>
<xsl:apply-templates select="item" mode="navigation"/>
</ul>
</xsl:template>
<xsl:template match="/data/categories-nav/entry/subcategories/item/subcategories/item" mode="navigation">
<xsl:call-template name="categories-nav-entries" mode="navigation"/>
</xsl:template>
</xsl:stylesheet>
The only difference between the matches for the three different levels is repeated subcategories and item nodes.
I've successfully got the lis in their own named template, but is there a way I can avoid matching the three levels separately?
Also, it seems that because I'm using a mode on the initial match, I also have to use that mode on all subsequent matches - is that correct?
Edit: here's what I came up with after using relative paths as #michael's answer:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template name="categories-nav-list">
<li class="{name/#handle}">
<xsl:value-of select="name"/>
<xsl:apply-templates select="subcategories[#items > 0]"/>
</li>
</xsl:template>
<xsl:template match="categories-nav">
<ul>
<xsl:apply-templates select="entry"/>
</ul>
</xsl:template>
<xsl:template match="categories-nav/entry">
<xsl:call-template name="categories-nav-list"/>
</xsl:template>
<xsl:template match="categories-nav//subcategories[item/active='Yes']">
<ul>
<xsl:apply-templates select="item"/>
</ul>
</xsl:template>
<xsl:template match="categories-nav//subcategories/item[active='Yes']" priority="1">
<xsl:call-template name="categories-nav-list"/>
</xsl:template>
<xsl:template match="categories-nav//subcategories/item" priority="0"/>
</xsl:stylesheet>
You don't need to put absolute paths in each #match attribute.
Why not something like this:
<xsl:template match="subcategories">
<ul>
<xsl:apply-templates select="item"/>
</ul>
</xsl:template>
<xsl:template match="item">
<xsl:call-template name="categories-nav-entries"/>
</xsl:template>

Extreme XSLT XML Flattening

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>

Using xsl param (if exists) to replcae attribute value

I would like an xsl that replaces the value attribute of the data elements only if the relevant param names are passed (they are passed from the calling java program).
Input
<applicationVariables applicationServer="tomcat">
<data name="HOST" value="localhost"/>
<data name="PORT" value="8080"/>
<data name="SIZE" value="1000"/>
</applicationVariables>
So for example if passing in a param HOST1=myHost and PORT=9080 the output should be:
<applicationVariables applicationServer="tomcat">
<data name="HOST" value="myHost"/>
<data name="PORT" value="9080"/>
<data name="SIZE" value="1000"/>
</applicationVariables>
Note that HOST and PORT where replaced but SIZE was not replaced because there was no parameter with name SIZE
I don't want a hardcoded check for each name, as below:
<xsl:when test="not($HOST)"> <!-- parameter has not been supplied -->
<xsl:attribute name="value"><xsl:value-of select="#value"/></xsl:attribute>
</xsl:when>
<xsl:otherwise> <!--parameter has been supplied -->
<xsl:attribute name="value"><xsl:value-of select="$HOST"/></xsl:attribute>
</xsl:otherwise>
I want a generic way of saying: replace the value attribute only if a param with the same name exists.
But how do i check if a param with name = #name exists?
In such case it is much better to pass all params as elements of a single <xsl:param>:
<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="pParams">
<p name="HOST">myHost</p>
<p name="PORT">9080</p>
</xsl:param>
<xsl:variable name="vParams" select=
"document('')/*/xsl:param[#name='pParams']/*"/>
<xsl:template match="node()|#*" name="identity">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="#value">
<xsl:attribute name="value">
<xsl:value-of select=
"$vParams[#name=current()/../#name]
|
current()[not($vParams[#name=current()/../#name])]
"/>
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<applicationVariables applicationServer="tomcat">
<data name="HOST" value="localhost"/>
<data name="PORT" value="8080"/>
<data name="SIZE" value="1000"/>
</applicationVariables>
the wanted, correct result is produced:
<applicationVariables applicationServer="tomcat">
<data name="HOST" value="myHost"></data>
<data name="PORT" value="9080"></data>
<data name="SIZE" value="1000"></data>
</applicationVariables>
You can combine logical conditions with xsl:if.

Creating a menu using xslt for Umbraco

I've created a menu in umbraco using XSLT. The menu is using the usual ul and li elements and I'm displaying only the first level of the menu. The aim is to create a menu that expands to show the sub menu when I click a parent node (in the top level).
I am after the xslt I would need to expose the sub menu when clicked.
I think I would need to make use of ancestor-or-self to detect the current menu and parent menu and display them and also the $currentPage variable.
I have the following xslt:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xsl:stylesheet [ <!ENTITY nbsp " "> ]>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxml="urn:schemas-microsoft-com:xslt"
xmlns:umbraco.library="urn:umbraco.library" xmlns:Exslt.ExsltCommon="urn:Exslt.ExsltCommon" xmlns:Exslt.ExsltDatesAndTimes="urn:Exslt.ExsltDatesAndTimes" xmlns:Exslt.ExsltMath="urn:Exslt.ExsltMath" xmlns:Exslt.ExsltRegularExpressions="urn:Exslt.ExsltRegularExpressions" xmlns:Exslt.ExsltStrings="urn:Exslt.ExsltStrings" xmlns:Exslt.ExsltSets="urn:Exslt.ExsltSets" xmlns:tagsLib="urn:tagsLib" xmlns:urlLib="urn:urlLib"
exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets tagsLib urlLib ">
<xsl:output method="xml" omit-xml-declaration="yes"/>
<xsl:param name="currentPage"/>
<xsl:template match="/">
<div id="kb-categories">
<h3>Categories</h3>
<xsl:call-template name="drawNodes">
<xsl:with-param name="parent" select="$currentPage/ancestor-or-self::node [#level=1]"/>
</xsl:call-template>
</div>
</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 $parent/#level = 1">
<ul class="kb-menuLevel1" >
<xsl:for-each select="$parent/node [string(./data [#alias='showInMenu']) = 1]">
<li>
<a href="/kb{umbraco.library:NiceUrl(#id)}">
<xsl:value-of select="#nodeName"/>
</a>
<xsl:variable name="level" select="#level" />
<xsl:if test="(count(./node [string(./data [#alias='showInMenu']) = '1']) > 0)">
<xsl:call-template name="drawNodes">
<xsl:with-param name="parent" select="."/>
</xsl:call-template>
</xsl:if>
</li>
</xsl:for-each>
</ul>
</xsl:if>
<xsl:if test="(umbraco.library:IsProtected($parent/#id, $parent/#path) = 0 or (umbraco.library:IsProtected($parent/#id, $parent/#path) = 1)) and $parent/#level > 1">
<ul class="kb-menuLevel{#level}" style="display: none;">
<xsl:for-each select="$parent/node [string(./data [#alias='showInMenu']) = 1]">
<li>
<a href="/kb{umbraco.library:NiceUrl(#id)}">
<xsl:value-of select="#nodeName"/>
</a>
<xsl:variable name="level" select="#level" />
<xsl:if test="(count(./node [string(./data [#alias='showInMenu']) = '1']) > 0)">
<xsl:call-template name="drawNodes">
<xsl:with-param name="parent" select="."/>
</xsl:call-template>
</xsl:if>
</li>
</xsl:for-each>
</ul>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
I suspect this could be improved using apply-templates, but I'm not yet up to speed with that (this being only the second day of my learning xslt).
My menu:
Menu Item 1
Menu Item 2
Menu Item 3
Menu Item 4
when I click on Menu Item 2 I will be taken to the page for menu Item 2 and the submenu will also be displayed:
Menu Item 1
Menu Item 2
-- Menu Item 2.1
-- Menu Item 2.2
Menu Item 3
Menu Item 4
and so on down the nested menu.
Here is some sample xml for the above.
<root>
<node id="1" nodeTypeAlias="kbHomepage" nodeName="Home" level="1">
<data alias="introduction">
<![CDATA[<p>Welcome</p>]]>
</data>
<node id="2" nodeTypeAlias="guide" nodeName="Menu Item 1" level="2">
<data alias="bodyText">
<![CDATA[<p>This is some text</p>]]>
</data>
<data alias="showInMenu">1</data>
<data alias="menuName">Menu Item 1</data>
</node>
<node id="3" nodeTypeAlias="guide" nodeName="Menu Item 2" level="2">
<data alias="bodyText">
<![CDATA[<p>This is some text</p>]]>
</data>
<data alias="showInMenu">1</data>
<data alias="menuName">Menu Item 2</data>
<node id="4" nodeTypeAlias="guide" nodeName="Menu Item 2.1" level="3">
<data alias="bodyText">
<![CDATA[<p>Some Text</p>]]>
</data>
<data alias="showInMenu">1</data>
<data alias="menuName">Menu Item 2.1</data>
</node>
<node id="5" nodeTypeAlias="guide" nodeName="Menu Item 2.2" level="3">
<data alias="bodyText">
<![CDATA[<p>Some Text</p>]]>
</data>
<data alias="showInMenu">1</data>
<data alias="menuName">Menu Item 2.2</data>
<node id="6" nodeTypeAlias="guide" nodeName="Item 2.2.1 Guide" level="4">
<data alias="bodyText">
<![CDATA[<p>Some Text</p>]]>
</data>
<data alias="showInMenu">0</data>
<data alias="menuName"></data>
</node>
</node>
</node>
<node id="8" nodeTypeAlias="guide" nodeName="Menu Item 3" level="2">
<data alias="bodyText">
<![CDATA[<p>This is some text</p>]]>
</data>
<data alias="showInMenu">1</data>
<data alias="menuName">Menu Item 3</data>
</node>
<node id="9" nodeTypeAlias="guide" nodeName="Menu Item 4" level="2">
<data alias="bodyText">
<![CDATA[<p>This is some text</p>]]>
</data>
<data alias="showInMenu">1</data>
<data alias="menuName">Menu Item 4</data>
</node>
</node>
<node id="7" nodeTypeAlias="someAlias" nodeName="Some Other Page" level="1">
<data alias="bodyText">
<![CDATA[<p>This is some text</p>]]>
</data>
</node>
</root>
edit: the following almost does what I need :
<xsl:variable name="visibleChidren" select="node[data[#alias='showInMenu'] = 1 and (#level = 2 or descendant-or-self::*[generate-id($currentPage) = generate-id(.)] or preceding-sibling::*[generate-id($currentPage) = generate-id(.)] or following-sibling::*[generate-id($currentPage) = generate-id(.)])]" />
I just need to also include the direct children from the current page.
I tried (with my very limited knowledge about Umbraco) to clean up your code a bit and remove the redundancy. It looks as though it would work with the XML sample you provided, but I cannot really test it against Umbraco.
<!DOCTYPE xsl:stylesheet [ <!ENTITY nbsp " "> ]>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxml="urn:schemas-microsoft-com:xslt"
xmlns:umbraco.library="urn:umbraco.library" xmlns:Exslt.ExsltCommon="urn:Exslt.ExsltCommon" xmlns:Exslt.ExsltDatesAndTimes="urn:Exslt.ExsltDatesAndTimes" xmlns:Exslt.ExsltMath="urn:Exslt.ExsltMath" xmlns:Exslt.ExsltRegularExpressions="urn:Exslt.ExsltRegularExpressions" xmlns:Exslt.ExsltStrings="urn:Exslt.ExsltStrings" xmlns:Exslt.ExsltSets="urn:Exslt.ExsltSets" xmlns:tagsLib="urn:tagsLib" xmlns:urlLib="urn:urlLib"
exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets tagsLib urlLib ">
<xsl:output method="xml" omit-xml-declaration="yes" encoding="utf-8" />
<xsl:param name="currentPage" />
<xsl:template match="/">
<div id="kb-categories">
<h3>Categories</h3>
<xsl:apply-templates mode="list" select="/root/node[#nodeTypeAlias='kbHomepage']" />
</div>
</xsl:template>
<!-- matches anything with <node> children and creates an <ul> -->
<xsl:template match="*[node]" mode="list">
<!-- prepare a list of all visible children -->
<xsl:variable name="visibleChidren" select="node[
data[#alias='showInMenu'] = 1
and (
not(umbraco.library:IsProtected(#id, #path))
or umbraco.library:IsLoggedOn()
)
]" />
<!-- prepare a CSS class for the "selected path" -->
<xsl:variable name="display">
<xsl:if test=".//node[generate-id() = generate-id($currentPage)]">
<xsl:text>visible</xsl:text>
</xsl:if>
</xsl:variable>
<xsl:if test="$visibleChidren">
<ul class="menu kb-menuLevel{$visibleChidren[1]/#level} {$display}">
<xsl:apply-templates mode="item" select="$visibleChidren" />
</ul>
</xsl:if>
</xsl:template>
<!-- matches <node> elements and turns them into list items -->
<xsl:template match="node" mode="item">
<li>
<xsl:if test="generate-id() = generate-id($currentPage)">
<xsl:attribute name="class">selected</xsl:attribute>
</xsl:if>
<a href="/kb{{umbraco.library:NiceUrl(#id)}}">
<xsl:value-of select="#nodeName" />
</a>
<!-- if there are any child nodes, render them -->
<xsl:if test="node">
<xsl:apply-templates mode="list" select="." />
</xsl:if>
</li>
</xsl:template>
</xsl:stylesheet>
Gives you the following. Note that I have escaped the attribute value template in <a href... - remove the double curlies above to enable them again:
<div id="kb-categories">
<h3>Categories</h3>
<ul class="menu kb-menuLevel2 visible">
<li>
Menu Item 1
</li>
<li>
Menu Item 2
<ul class="menu kb-menuLevel3 visible">
<li class="selected">
Menu Item 2.1
</li>
<li>
Menu Item 2.2
</li>
</ul>
</li>
<li>
Menu Item 3
</li>
<li>
Menu Item 4
</li>
</ul>
</div>
Now you could do in CSS:
ul.menu {
display: hidden;
}
ul.menu.visible {
display: block;
}
ul.menu li.selected {
font-weight: bold;
}
Does that help you?
I figured out what I need to do what I want. The key line being:
<xsl:variable name="visibleChidren" select="node[data[#alias='showInMenu'] = 1 and (#level = 2 or descendant-or-self::*[generate-id($currentPage) = generate-id(.)] or preceding-sibling::*[generate-id($currentPage) = generate-id(.)] or following-sibling::*[generate-id($currentPage) = generate-id(.)] or parent::*[generate-id($currentPage) = generate-id(.)])]" />
From the entire xslt:
<!DOCTYPE xsl:stylesheet [ <!ENTITY nbsp " "> ]>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxml="urn:schemas-microsoft-com:xslt"
xmlns:umbraco.library="urn:umbraco.library" xmlns:Exslt.ExsltCommon="urn:Exslt.ExsltCommon" xmlns:Exslt.ExsltDatesAndTimes="urn:Exslt.ExsltDatesAndTimes" xmlns:Exslt.ExsltMath="urn:Exslt.ExsltMath" xmlns:Exslt.ExsltRegularExpressions="urn:Exslt.ExsltRegularExpressions" xmlns:Exslt.ExsltStrings="urn:Exslt.ExsltStrings" xmlns:Exslt.ExsltSets="urn:Exslt.ExsltSets" xmlns:tagsLib="urn:tagsLib" xmlns:urlLib="urn:urlLib"
exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets tagsLib urlLib ">
<xsl:output method="xml" omit-xml-declaration="yes"/>
<xsl:param name="currentPage"/>
<xsl:variable name="currentLevel" select="$currentPage/#level" />
<xsl:template match="/">
<div id="kb-categories">
<h3>Categories</h3>
<xsl:apply-templates mode="list" select="$currentPage/ancestor-or-self::node [#nodeTypeAlias = 'kbHomepage']" />
</div>
</xsl:template>
<!-- matches anything with <node> children and makes a list out of them -->
<xsl:template match="node" mode="list">
<!-- select only sub-nodes that have 'showInMenu' = 1 -->
<xsl:variable name="visibleChidren" select="node[data[#alias='showInMenu'] = 1 and (#level = 2 or descendant-or-self::*[generate-id($currentPage) = generate-id(.)] or preceding-sibling::*[generate-id($currentPage) = generate-id(.)] or following-sibling::*[generate-id($currentPage) = generate-id(.)] or parent::*[generate-id($currentPage) = generate-id(.)])]" />
<xsl:if test="$visibleChidren">
<ul>
<xsl:apply-templates mode="item" select="$visibleChidren" />
</ul>
</xsl:if>
</xsl:template>
<xsl:template match="node" mode="item">
<li>
<a href="/kb{umbraco.library:NiceUrl(#id)}">
<xsl:value-of select="#nodeName"/>
</a>
<xsl:apply-templates mode="list" select="." />
</li>
</xsl:template>
</xsl:stylesheet>
Or you could solve yourself a lot of hacking about in XSLT and use the following navigation package from our.umbraco.org
This I think does everything you need and no need to get your hands dirty in the murky world of XSLT.
http://our.umbraco.org/projects/cogworks---flexible-navigation

Grouping problem in XSLT

I need to group the following XML doc to show:
Parent Item Qty
----------------------------------
TopLevelAsy 1
SubAsy Part15 4
Top Assembly Part19 2
Top Assembly Part15 2
Top Assembly SubAsy 2
But what I get using my XSL is:
Parent Item Qty
----------------------------------
TopLevelAsy 1
SubAsy Part15 2
SubAsy Part15 2
Top Assembly Part19 2
Top Assembly Part15 2
Top Assembly SubAsy 2
Her is my XML:
<DOCUMENT>
<ProductRevision id="id41" name="Top Assembly" accessRefs="#id30" subType="ItemRevision" masterRef="#id47" revision="A"></ProductRevision>
<ProductRevision id="id15" name="PartA-15" accessRefs="#id30" subType="ItemRevision" masterRef="#id36" revision="A"></ProductRevision>
<ProductRevision id="id19" name="PartB-19" accessRefs="#id30" subType="ItemRevision" masterRef="#id46" revision="A"></ProductRevision>
<ProductRevision id="id48" name="SubAsy" accessRefs="#id30" subType="ItemRevision" masterRef="#id76" revision="A"></ProductRevision>
<ProductView id="id4" ruleRefs="#id2" rootRefs="id7" primaryOccurrenceRef="id7">
<Occurrence id="id7" instancedRef="#id41" occurrenceRefs="id15 id11 id17 id16 id18 id21">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/"></ApplicationRef>
<data>
<title>TopLevelAsy</title>
<year>1985</year>
</data>
</Occurrence>
<Occurrence id="id11" instancedRef="#id19" parentRef="#id7">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/yBEAAAV4xLJc5D/"></ApplicationRef>
<data>
<title>Part19</title>
<year>1988</year>
</data>
</Occurrence>
<Occurrence id="id15" instancedRef="#id15" parentRef="#id7">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/sdljfjdkLJc5D/"></ApplicationRef>
<data>
<title>Part15</title>
<year>1988</year>
</data>
</Occurrence>
<Occurrence id="id17" instancedRef="#id19" parentRef="#id7">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/yBEAAAV4xLJc5D/"></ApplicationRef>
<data>
<title>Part19</title>
<year>1988</year>
</data>
</Occurrence>
<Occurrence id="id16" instancedRef="#id15" parentRef="#id7">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/sdljfjdkLJc5D/"></ApplicationRef>
<data>
<title>Part15</title>
<year>1988</year>
</data>
</Occurrence>
<!-- sub assembly Second occurrence -->
<Occurrence id="id21" instancedRef="#id48" parentRef="#id7" occurrenceRefs="id153 id135">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/wesdjdLJc5D/"></ApplicationRef>
<data>
<title>Sub Assembly</title>
<year>1985</year>
</data>
</Occurrence>
<Occurrence id="id153" instancedRef="#id15" parentRef="#id21">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/wesdjdLJc5D/jkdsdwV4xLJc5D/"></ApplicationRef>
<data>
<title>Part15</title>
<year>1988</year>
</data>
</Occurrence>
<Occurrence id="id135" instancedRef="#id15" parentRef="#id21">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/wesdjdLJc5D/jkdsdwV4xLJc5D/"></ApplicationRef>
<data>
<title>Part15</title>
<year>1988</year>
</data>
</Occurrence>
<!-- sub assembly first occurrence -->
<Occurrence id="id18" instancedRef="#id48" parentRef="#id7" occurrenceRefs="id53 id35">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/wesdjdLJc5D/"></ApplicationRef>
<data>
<title>Sub Assembly</title>
<year>1985</year>
</data>
</Occurrence>
<Occurrence id="id53" instancedRef="#id15" parentRef="#id18">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/wesdjdLJc5D/vdsfdwV4xLJc5D/"></ApplicationRef>
<data>
<title>Part15</title>
<year>1988</year>
</data>
</Occurrence>
<Occurrence id="id35" instancedRef="#id15" parentRef="#id18">
<ApplicationRef application="CAD" label="i9BAAAV4xLJc5D/wesdjdLJc5D/vdsfdwV4xLJc5D/"></ApplicationRef>
<data>
<title>Part15</title>
<year>1988</year>
</data>
</Occurrence>
</ProductView>
</DOCUMENT>
The XSLT i have written is
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="no" />
<!-- <xsl:key name="byref" match="Occurrence" use="#instancedRef"/> -->
<xsl:key name="byid" match="Occurrence" use="concat(#title,#instancedRef)" />
<xsl:key name="byRef" match="Occurrence" use="#instancedRef" />
<xsl:template match="/">
<table border="1">
<!-- generate the keys for instance occurance-->
<!-- generate the keys for parent id -->
<xsl:for-each select="DOCUMENT/ProductView/Occurrence[generate-id(.)=generate-id(key('byid', concat(#instancedRef,#title))[1])]">
<xsl:sort select="#parentRef" />
<xsl:variable name="pRef" select="#parentRef" />
<xsl:variable name="instRef" select="#instancedRef" />
<xsl:variable name="pdOccId" select="substring-after($pRef,'#')" />
<xsl:variable name="pdRevIdTag" select="//DOCUMENT/ProductView/Occurrence[#id=$pdOccId]/#instancedRef" />
<xsl:variable name="pdRevId" select="substring-after($pdRevIdTag,'#')" />
<xsl:variable name="parentlabeltag" select="ApplicationRef/#label" />
<tr>
<td>
<xsl:text>Parent: </xsl:text>
<xsl:value-of select="//DOCUMENT/ProductRevision[#id=$pdRevId]/#name" />
</td>
<td align="right">
<xsl:value-of select="data/title" />
<xsl:text> </xsl:text>
</td>
<td>
<xsl:value-of select="count(key('byid', concat(#instancedRef,#title)))" />
</td>
</tr>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>
Any help will be great. Please help me figure this out. Thank you.
George,
Here's the solution:
You need to use 2 xslts for the same. The first xslt will generate an easily parseable xml. The second xslt will use this xml as its input to achieve the desired result.
Xslt1:
Your xml will be the input to this xslt
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="bytitle" match="Occurrence" use="data/title"/>
<xsl:template match="/">
<root>
<xsl:for-each select="DOCUMENT/ProductView/Occurrence[not(#parentRef)]">
<xsl:element name="Data">
<xsl:attribute name="parentTitle" />
<xsl:attribute name="childTitle">
<xsl:value-of select="data/title"/>
</xsl:attribute>
</xsl:element>
</xsl:for-each>
<xsl:for-each select="DOCUMENT/ProductView/Occurrence[generate-id(.)=generate-id(key('bytitle', data/title)[1])]">
<xsl:sort select="#id"/>
<xsl:variable name="title" select="data/title" />
<xsl:variable name="driver" select="//DOCUMENT/ProductView/Occurrence[data/title = $title]/#id"/>
<xsl:for-each select="//DOCUMENT/ProductView/Occurrence[substring-after(#parentRef,'#') = $driver/.]">
<xsl:element name="Data">
<xsl:attribute name="parentTitle">
<xsl:value-of select="$title"/>
</xsl:attribute>
<xsl:attribute name="childTitle">
<xsl:value-of select="data/title"/>
</xsl:attribute>
</xsl:element>
</xsl:for-each>
</xsl:for-each>
</root>
</xsl:template>
</xsl:stylesheet>
Output of this xslt:
<?xml version="1.0" encoding="utf-8"?>
<root>
<Data parentTitle="" childTitle="TopLevelAsy" />
<Data parentTitle="Sub Assembly" childTitle="Part15" />
<Data parentTitle="Sub Assembly" childTitle="Part15" />
<Data parentTitle="Sub Assembly" childTitle="Part15" />
<Data parentTitle="Sub Assembly" childTitle="Part15" />
<Data parentTitle="TopLevelAsy" childTitle="Part19" />
<Data parentTitle="TopLevelAsy" childTitle="Part15" />
<Data parentTitle="TopLevelAsy" childTitle="Part19" />
<Data parentTitle="TopLevelAsy" childTitle="Part15" />
<Data parentTitle="TopLevelAsy" childTitle="Sub Assembly" />
<Data parentTitle="TopLevelAsy" childTitle="Sub Assembly" />
</root>
Xslt2:
The output of xslt 1 should be the input to this xslt
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes"/>
<xsl:key name="bytitle" match="Data" use="concat(#parentTitle, #childTitle)"/>
<xsl:template match="/">
<table border="1">
<xsl:for-each select="/root/Data[generate-id(.)=generate-id(key('bytitle', concat(#parentTitle, #childTitle))[1])]">
<xsl:sort select="#parentTitle"/>
<xsl:variable name="parentTitle" select="#parentTitle"/>
<xsl:variable name="childTitle" select="#childTitle"/>
<tr>
<td>
<xsl:value-of select="#parentTitle"/>
</td>
<td>
<xsl:value-of select="#childTitle"/>
</td>
<td>
<xsl:value-of select="count(//root/Data[#parentTitle=$parentTitle and #childTitle = $childTitle])"/>
</td>
</tr>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>
Output:
TopLevelAsy 1
Sub Assembly Part15 4
TopLevelAsy Part19 2
TopLevelAsy Part15 2
TopLevelAsy Sub Assembly 2
George, you are getting 2 rows for sub assembly as there are two elements for sub-assembly with different ids (id21 and id18) and out of the four Part15s, two belong to id21 and two belong to id18.
Since you are grouping according to the parent id, your output seems correct. If you want to have both the sub assembly elements grouped, then you need to specify the parent title rather than parent id in your concatenated key.
I just tweaked your xslt to see the grouping. This is how they are grouped. As you can see the Parent Ref for the 2nd and 3rd rows are different
Instance Ref:#id41 Parent Ref: Parent: TopLevelAsy 1
Instance Ref:#id15 Parent Ref:#id18 Parent: SubAsy Part15 2
Instance Ref:#id15 Parent Ref:#id21 Parent: SubAsy Part15 2
Instance Ref:#id19 Parent Ref:#id7 Parent: Top Assembly Part19 2
Instance Ref:#id15 Parent Ref:#id7 Parent: Top Assembly Part15 2
Instance Ref:#id48 Parent Ref:#id7 Parent: Top Assembly Sub Assembly 2
Edit
This is not a complete solution but you might have to do something on these lines:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="no"/>
<!-- <xsl:key name="byref" match="Occurrence" use="#instancedRef"/> -->
<xsl:key name="bytitle" match="Occurrence" use="data/title"/>
<xsl:template match="/">
<table border="1">
<xsl:for-each select="DOCUMENT/ProductView/Occurrence[generate-id(.)=generate-id(key('bytitle', data/title)[1])]">
<xsl:sort select="#id"/>
<xsl:variable name="title" select="data/title" />
<br />
Title:<xsl:value-of select="$title"/>
<xsl:variable name="driver" select="//DOCUMENT/ProductView/Occurrence[data/title = $title]/#id"/>
Driver:<xsl:value-of select="$driver/."/>
<!--<xsl:for-each select ="$driver">
<xsl:value-of select="."/>
</xsl:for-each>-->
<xsl:for-each select="//DOCUMENT/ProductView/Occurrence[substring-after(#parentRef,'#') = $driver/.]">
Child:<xsl:value-of select="data/title"/>
</xsl:for-each>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>