XSLT - For-each-group - xslt

This is a really basic question, but I'm not understanding how the for-each-group works. I want to combine adjacent top-level sections that don't have a sub section into a list. If there are sections WITH subsections I want to treat those differently, leaving the top level intact and combining the sub-sections into a list. I don't want to mix these up.
Source XML
<?xml version="1.0" encoding="UTF-8"?>
<body>
<sec>
<title>A1</title>
<p>Stuff A 1</p>
</sec>
<sec>
<title>A2</title>
<p>Stuff A 2</p>
</sec>
<sec>
<title>A3</title>
<p>Stuff A 3</p>
<sec>
<title>B1</title>
<p>Stuff B1</p>
</sec>
<sec>
<title>B2</title>
<p>Stuff B2</p>
</sec>
</sec>
<sec>
<title>A4</title>
<p>Stuff A 4</p>
</sec>
</body>
Desired Result
<body>
<list>
<list-item><title>A1</title><p>Stuff A 1</p></list-item>
<list-item><title>A2</title><p>Stuff A 2</p></list-item>
</list>
<sec>
<title>A3</title>
<p>Stuff A 3</p>
<list>
<list-item><title>B1</title><p>Stuff B1</p></list-item>
<list-item><title>B2</title><p>Stuff B2</p></list-item>
</list>
</sec>
<list>
<list-item><title>A4</title><p>Stuff A 4</p></list-item>
</list>
</body>
XSLT fragment
This is definitely not correct. Also, it is not the only way I've tried, just the least messy to post. The way I think for-each-group should work I keep getting the error An empty sequence is not allowed as the #group-adjacent attribute of xsl:for-each-group. So this is just a snippet to get someone who knows what they are doing started.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<!-- Identity Template -->
<xsl:template match="#*|node()" name="default" mode="#all">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:apply-templates mode="#current"/>
</xsl:copy>
</xsl:template>
<!-- Make top level body tag -->
<xsl:template match="body">
<body>
<xsl:for-each-group select="sec[not(sec)]" group-adjacent=".">
<list>
<xsl:apply-templates select="current-group()"/>
</list>
</xsl:for-each-group>
</body>
</xsl:template>
<xsl:template match="sec[not(sec)]">
<list-item>
<xsl:copy-of select="*"/>
</list-item>
</xsl:template>
</xsl:stylesheet>

Try
<xsl:for-each-group select="sec" group-adjacent="exists(child::sec)">
which will give a group of sec elements that have a sec child, followed by a group that don't, and so on.
Within the for-each-group you may need to do <xsl:choose><xsl:when test="child::sec">... to apply different processing to the two kinds of group.

Related

XSLT - Remove duplicates from mapped results

This isn't quite the threads on removing duplicates I've found on this forum.
I have a key/value map and I want to remove duplicates from the final results of the mapping.
Source Document:
<article>
<subject code="T020-060"/>
<subject code="T020-010"/>
<subject code="T090"/>
</article>
Mapping:
<xsl:variable name="topicalMap">
<topic MapCode="T020-060">Value 1</topic>
<topic MapCode="T020-010">Value 1</topic>
<topic MapCode="T090">Value 3</topic>
</xsl:variable>
Desired Result:
<article>
<topic>Value 1</topic>
<topic>Value 3</topic>
</article>
XSLT I'm working with (note, it has a testing tags and code to make sure the mapping works):
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="xml" encoding="utf8" indent="yes" exclude-result-prefixes="#all"/>
<xsl:template match="article">
<article>
<xsl:for-each-group select="subject" group-by="$topicalMap/topic[#MapCode = #code]">
<test-group>
<code>Current code: <xsl:value-of select="#code"/></code>
<topic>Current keyword: <xsl:value-of
select="$topicalMap/topic[#MapCode = #code]"/></topic>
</test-group>
</xsl:for-each-group>
<simple-mapping><xsl:apply-templates/></simple-mapping>
</article>
</xsl:template>
<!-- Simple Mapping Topics -->
<xsl:template match="subject">
<xsl:variable name="ArticleCode" select="#code"/>
<topic>
<xsl:value-of select="$topicalMap/topic[#MapCode = $ArticleCode]"/>
</topic>
</xsl:template>
<!-- Keyword Map -->
<xsl:variable name="topicalMap">
<topic MapCode="T020-060">Value 1</topic>
<topic MapCode="T020-010">Value 1</topic>
<topic MapCode="T090">Value 3</topic>
</xsl:variable>
</xsl:stylesheet>
Doing the group-by that way produces nothing. If I duplicate the topics in the source document and do group-by="#code" that works to remove before applying the mapping. But I want to remove resultant duplicate values not duplicate keys.
Simple-mapping stuff is just to show working code.
Use
<xsl:for-each-group select="subject" group-by="$topicalMap/topic[#MapCode = current()/#code]">
<topic>
<xsl:value-of select="current-grouping-key()"/>
</topic>
</xsl:for-each-group>
or better yet
<xsl:key name="map" match="topic" use="#MapCode"/>
<xsl:template match="article">
<article>
<xsl:for-each-group select="subject" group-by="key('map', #code, $topicalMap)">
<topic>
<xsl:value-of select="current-grouping-key()"/>
</topic>
</xsl:for-each-group>
</article>
</xsl:template>

Recursively replacing elements in XSLT

I need to replace the <tref> element with other tags from elsewhere in my document. For example, I have:
<tref id="57236"/>
and
<Topic>
<ID>57236</ID>
<Text>
<p id="4">
<cs id="56792">1090-189-01 </cs>
<href id="57237">
<cs id="56792">Document Name</cs>
</href>
</p>
</Text>
</Topic>
Obtaining the following is not a problem:
<p id="4">
<cs id="56792">1090-189-01 </cs>
<href id="57237">
<cs id="56792">Document Name</cs>
</href>
</p>
With this stylesheet:
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="tref">
<xsl:variable name="NodeID"><xsl:value-of select="#id"/></xsl:variable>
<xsl:copy-of select="//Topic[ID = $NodeID]/Text/p/node()"/>
</xsl:template>
What I cannot do is replacing trefs nested into other trefs. For example, consider the following:
<tref id="57236"/>
and:
<Topic>
<ID>57236</ID>
<Text>
<p id="251">
<tref id="37287"/>
</p>
</Text>
</Topic>
My stylesheet duly replaces the tref with the content of the tag - which also contains a tref:
<p id="251">
<tref id="37287"/>
</p>
My current solution is to call <xsl:template match="tref"> from two different stylesheets. It does the job, but it is not very elegant, and what if trefs are nested at an even deeper level? And recursion is the bread and butter of XSLT.
Is there a solution to recursively replace all trefs as in XSLT?
Instead of using xsl:copy-of, use xsl:apply-templates
<xsl:apply-templates select="//Topic[ID = $NodeID]/Text/p/node()"/>
Or, to eliminate the use of the varianle
<xsl:apply-templates select="//Topic[ID = current()/#id]/Text/p/node()"/>
Note you can make use of an xsl:key to look-up the Topic elements
<xsl:key name="topic" match="Topic" use="ID" />
Then you can write this
<xsl:apply-templates select="key('topic', #id)/Text/p/node()"/>
Be wary of infinite recursion if you have a tref referring to a Topic that is an ancestor of it.

Merging XSLTs into one flle and calling each template separately matching specific content

I've the below Sample XMLs.
XML1
<root num="1">
<abc></abc>
<cde></cde>
<def></def>
</root>
XML2
<root num="2">
<xyz></xyz>
<cft></cft>
<vft></vft>
</root>
XML3
<root num="3">
<dfg></dfg>
<mnb></mnb>
<gft></gft>
<root>
And i have 3 different XSLTs, each corresponding to XML.
I want to achieve the below.
Make a single XSLT and call the template based on the root number. something like the below.
<xsl:if test="root[#num="1"]>
<!--Call the template matching root 1-->
</xsl:if>
<xsl:if test="root[#num="2"]>
<!--Call the template matching root 2-->
</xsl:if>
<xsl:if test="root[#num="3"]>
<!--Call the template matching root 3-->
</xsl:if>
I just want to put all the XSLTs in a single XSLT, please let me know how can i do this.
Thanks
You can use element.
The element contains rules to apply when a specified node is matched.
The match attribute is used to associate the template with an XML element. The match attribute can also be used to define a template for a whole branch of the XML document (i.e. match="/" defines the whole document).
Note:  is a top-level element.
Example:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html>
<body>
<h2>My CD Collection</h2>
<xsl:apply-templates/>
</body>
</html>
</xsl:template>
<xsl:template match="cd">
<p>
<xsl:apply-templates select="title"/>
<xsl:apply-templates select="artist"/>
</p>
</xsl:template>
<xsl:template match="title">
Title: <span style="color:#ff0000">
<xsl:value-of select="."/></span>
<br />
</xsl:template>
<xsl:template match="artist">
Artist: <span style="color:#00ff00">
<xsl:value-of select="."/></span>
<br />
</xsl:template>
</xsl:stylesheet>
This was taken from http://www.w3schools.com/xsl/el_template.asp

With XSLT, how can I process normally, but hold some nodes until the end and then output them all at once (e.g. footnotes)?

I have an XSLT application which reads the internal format of Microsoft Word 2007/2010 zipped XML and translates it into HTML5 with XSLT. I am investigating how to add the ability to optionally read OpenOffice documents instead of MSWord.
Microsoft stores XML for footnote text separately from the XML of the document text, which happens to suit me because I want the footnotes in a block at the end of the output HTML page.
However, unfortunately for me, OpenOffice puts each footnote right next to its reference, inline with the text of the document. Here is a simple paragraph example:
<text:p text:style-name="Standard">The real breakthrough in aerial mapping
during World War II was trimetrogon
<text:note text:id="ftn0" text:note-class="footnote">
<text:note-citation>1</text:note-citation>
<text:note-body>
<text:p text:style-name="Footnote">Three separate cameras took three
photographs at once, a direct downward and an oblique on each side.</text:p>
</text:note-body>
</text:note>
photography, but the camera was large and heavy, so there were problems finding
the right aircraft to carry it.
</text:p>
My question is, can XSLT process the XML as normal, but hold each of the text:note items until the end of the document text, and then emit them all at one time?
You're thinking of your logic as being driven by the order of things in the input, but in XSLT you need to be driven by the order of things in the output. When you get to the point where you want to output the footnotes, go find the footnote text wherever it might be in the input. Admittedly that doesn't always play too well with the apply-templates recursive descent processing model, which is explicitly input-driven; but nevertheless, that's the way you have to do it.
Don't think of it as "holding" the text:note items, instead simply ignore them in the main pass and then gather them at the end with a //text:note and process them there, e.g.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
xmlns:text="whateveritshouldbe">
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()" />
</xsl:copy>
</xsl:template>
<!-- normal mode - replace text:note element by [reference] -->
<xsl:template match="text:note">
<xsl:value-of select="concat('[', text:note-citation, ']')" />
</xsl:template>
<xsl:template match="/">
<document>
<xsl:apply-templates select="*" />
<footnotes>
<xsl:apply-templates select="//text:note" mode="footnotes"/>
</footnotes>
</document>
</xsl:template>
<!-- special "footnotes" mode to de-activate the usual text:node template -->
<xsl:template match="#*|node()" mode="footnotes">
<xsl:copy>
<xsl:apply-templates select="#*|node()" mode="footnotes" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
You could use <xsl:apply-templates mode="..."/>. I'm not sure on the exact syntax and your use case, but maybe the example below will give you a clue on how to approach your problem.
Basic idea is to process your nodes twice. First iteration would be pretty much the same as now, and the second iteration only looks for footnotes and only outputs those. You differentiate those iteration by setting "mode" parameter.
Maybe this example will give you a clue how to approach your problem. Note that I used different tags that in your code, so the example would be simpler.
XSLT sheet:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes" />
<xsl:template match="doc">
<xml>
<!-- First iteration - skip footnotes -->
<doc>
<xsl:apply-templates select="text" />
</doc>
<!-- Second iteration, extract all footnotes.
'mode' = footnotes -->
<footnotes>
<xsl:apply-templates select="text" mode="footnotes" />
</footnotes>
</xml>
</xsl:template>
<!-- Note: no mode attribute -->
<xsl:template match="text">
<text>
<xsl:for-each select="p">
<p>
<xsl:value-of select="text()" />
</p>
</xsl:for-each>
</text>
</xsl:template>
<!-- Note: mode = footnotes -->
<xsl:template match="text" mode="footnotes">
<xsl:for-each select=".//footnote">
<footnote>
<xsl:value-of select="text()" />
</footnote>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Input XML:
<?xml version="1.0" encoding="UTF-8"?>
<doc>
<text>
<p>
some text
<footnote>footnote1</footnote>
</p>
<p>
other text
<footnote>footnote2</footnote>
</p>
</text>
<text>
<p>
some text2
<footnote>footnote3</footnote>
</p>
<p>
other text2
<footnote>footnote4</footnote>
</p>
</text>
</doc>
Output XML:
<?xml version="1.0" encoding="UTF-8"?>
<xml>
<!-- Output from first iteration -->
<doc>
<text>
<p>some text</p>
<p>other text</p>
</text>
<text>
<p>some text2</p>
<p>other text2</p>
</text>
</doc>
<!-- Output from second iteration -->
<footnotes>
<footnote>footnote1</footnote>
<footnote>footnote2</footnote>
<footnote>footnote3</footnote>
<footnote>footnote4</footnote>
</footnotes>
</xml>

XSLT - Grouping same items

I have the following XML:
<Info>
<Name>Dan</Name>
<Age>24</Age>
</Info>
<Info>
<Name>Tom</Name>
<Age>15</Age>
</Info>
<Info>
<Name>Dan</Name>
<Age>24</Age>
</Info>
<Info>
<Name>James</Name>
<Age>18</Age>
</Info>
And I need to produce the following HTML:
<ul class="data">
<li>Dan</li>
<li>Dan</li>
</ul>
<ul class="data">
<li>James</li>
</ul>
<ul class="data">
<li>Tom</li>
<li>Tom</li>
</ul>
As well as producing the output it needs to sort based on the Name also. Any help appreciated, started by looking at group-by but couldnt work out how to get it finished:
Pretty sure its wrong?
<xsl:for-each-group select="Info" group-by="#Name">??????
<xsl:for-each-group select="current-group()" sort-by="#Name">
I don't have an XSLT 2.0 parser, but to do it in XSLT 1.0 at least you could use Muenchian Grouping to do this...
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output encoding="utf-8" method="html" version="1.0"/>
<xsl:key name="Names" match="Name" use="."/>
<xsl:template match="/">
<xsl:for-each select="//Info[generate-id(Name) = generate-id(key('Names', Name)[1])]">
<xsl:sort select="Name" />
<ul class="data">
<xsl:for-each select="key('Names', Name)">
<li><xsl:value-of select="." /></li>
</xsl:for-each>
</ul>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
I'm not sure why your result has two 'Tom' elements, I assume you have an extra node in the XML that you didn't provide in your sample.
Anyway, the XSLT would look something like this:
<xsl:for-each-group select="Info" group-by="Name">
<xsl:sort select="current-grouping-key()"/>
<ul class="data">
<xsl:for-each select="current-group()/Name">
<li><xsl:value-of select="." /></li>
</xsl:for-each>
</ul>
</xsl:for-each-group>
I don't have an XSLT 2.0 parser handy to test it, but I think that should work.
I don't have an xslt 2.0 parser to test this, but at the very least you will need to change "#Name" to just "Name", since it is a subelement not an attribute.