This glossary derives the index from the first letter of each entry. I'm trying to work out how to show only the unique values. Have looked into preceding-sibling and position() but cannot seem to find the correct way to. I'm constrained to using XSLT 1.0 and attributes.
glossary.xml
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="glossary.xsl"?>
<include>
<file name="data.xml"/>
</include>
data.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<glossary>
<entry term="cantaloupe" definition="A kind of melon"/>
<entry term="banana" definition="A tropical yellow fruit"/>
<entry term="apple" definition="A red fruit with seeds"/>
<entry term="orange" definition="An orange citrus fruit"/>
<entry term="Cherry" definition="A red fruit that grows in clusters "/>
<entry term="cranberry" definition="A sour berry enjoyed at Thanksgiving"/>
<entry term="avocado" definition="A mellow fruit enjoyed in guacamole"/>
</glossary>
glossary.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" doctype-system="about:legacy-compat" encoding="UTF-8" indent="yes" />
<xsl:template match="/">
<html>
<head></head>
<body>
<!-- Index: how to show unique values? -->
<xsl:for-each select="document('data.xml')/glossary/entry" >
<xsl:sort select="#term" data-type="text" order="ascending" case-order="upper-first"/>
<xsl:variable name="initial" select="substring(#term,1,1)" />
<xsl:value-of select="$initial" /> |
</xsl:for-each>
<!-- Glossary -->
<dl>
<xsl:for-each select="document('data.xml')/glossary/entry" >
<xsl:sort select="#term" data-type="text" order="ascending" case-order="upper-first"/>
<xsl:variable name="initial" select="substring(#term,1,1)" />
<!-- Alphabetical header: how to only the first instance of each letter? -->
<a name="{$initial}"><h1><xsl:value-of select="$initial" /></h1></a>
<dt><xsl:apply-templates select="#term"/></dt>
<dd><xsl:apply-templates select="#definition"/></dd>
</xsl:for-each>
</dl>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
Output so far
a | a | b | c | C | c | o |
a
apple
A red fruit with seeds
a
avocado
A mellow fruit enjoyed in guacamole
b
banana
A tropical yellow fruit
c
cantaloupe
A kind of melon
C
Cherry
A red fruit that grows in clusters
c
cranberry
A sour berry enjoyed at Thanksgiving
o
orange
An orange citrus fruit
Desired output
a | b | c | o
a
apple
A red fruit with seeds
avocado
A mellow fruit enjoyed in guacamole
b
banana
A tropical yellow fruit
c
cantaloupe
A kind of melon
Cherry
A red fruit that grows in clusters
cranberry
A sour berry enjoyed at Thanksgiving
o
orange
An orange citrus fruit
This is an example of a grouping problem and in XSLT 1.0, the established way to do grouping is to use Muenchian Grouping. Unfortunately, your scenario requires finding the lower-case of characters on top of that, and that's a bit messy in XSLT 1.0.
Nonetheless, I've produced a solution and it goes as follows:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" doctype-system="about:legacy-compat"
encoding="UTF-8" indent="yes" />
<xsl:key name="kEntryInitial" match="entry/#term"
use="translate(substring(., 1, 1),
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'abcdefghijklmnopqrstuvwxyz')"/>
<xsl:template match="/">
<html>
<head></head>
<body>
<!-- Jump into the data.xml DOM so that keys work -->
<xsl:apply-templates select="document('data.xml')/glossary" />
</body>
</html>
</xsl:template>
<xsl:template match="/glossary">
<!-- Select terms with distinct initials (case invariant) -->
<xsl:variable name="termsByDistinctInitial"
select="entry/#term[generate-id() =
generate-id(key('kEntryInitial',
translate(substring(., 1, 1),
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'abcdefghijklmnopqrstuvwxyz'))[1])]" />
<!-- Header -->
<xsl:apply-templates select="$termsByDistinctInitial" mode="header">
<xsl:sort select="." data-type="text" order="ascending" />
</xsl:apply-templates>
<!-- Glossary -->
<dl>
<xsl:apply-templates select="$termsByDistinctInitial" mode="main">
<xsl:sort select="." data-type="text" order="ascending" />
</xsl:apply-templates>
</dl>
</xsl:template>
<xsl:template match="#term" mode="header">
<xsl:variable name="initial">
<xsl:call-template name="ToLower">
<xsl:with-param name="value" select="substring(., 1, 1)" />
</xsl:call-template>
</xsl:variable>
<a href="#{$initial}">
<xsl:value-of select="$initial" />
</a>
<xsl:if test="position() != last()">
<xsl:text> |</xsl:text>
</xsl:if>
</xsl:template>
<xsl:template match="#term" mode="main">
<xsl:variable name="initial">
<xsl:call-template name="ToLower">
<xsl:with-param name="value" select="substring(., 1, 1)" />
</xsl:call-template>
</xsl:variable>
<a name="{$initial}">
<h1>
<xsl:value-of select="$initial" />
</h1>
</a>
<xsl:apply-templates select="key('kEntryInitial', $initial)/.." />
</xsl:template>
<xsl:template match="entry">
<dt>
<xsl:apply-templates select="#term"/>
</dt>
<dd>
<xsl:apply-templates select="#definition"/>
</dd>
</xsl:template>
<xsl:template name="ToLower">
<xsl:param name="value" />
<xsl:value-of select="translate(substring($value, 1, 1),
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'abcdefghijklmnopqrstuvwxyz')"/>
</xsl:template>
</xsl:stylesheet>
When run on your input XML, this produces the following:
<!DOCTYPE html SYSTEM "about:legacy-compat">
<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>a |b |c |o
<dl><a name="a"><h1>a</h1></a><dt>apple</dt>
<dd>A red fruit with seeds</dd>
<dt>avocado</dt>
<dd>A mellow fruit enjoyed in guacamole</dd><a name="b"><h1>b</h1></a><dt>banana</dt>
<dd>A tropical yellow fruit</dd><a name="c"><h1>c</h1></a><dt>cantaloupe</dt>
<dd>A kind of melon</dd>
<dt>Cherry</dt>
<dd>A red fruit that grows in clusters </dd>
<dt>cranberry</dt>
<dd>A sour berry enjoyed at Thanksgiving</dd><a name="o"><h1>o</h1></a><dt>orange</dt>
<dd>An orange citrus fruit</dd>
</dl>
</body>
</html>
One thing I'd suggest considering is using a simple XSLT to "prep" your glossary with initials:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="entry">
<xsl:copy>
<xsl:attribute name="initial">
<xsl:value-of select="translate(substring(#term, 1, 1),
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'abcdefghijklmnopqrstuvwxyz')"/>
</xsl:attribute>
<xsl:apply-templates select="#* | node()" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
This produces:
<glossary>
<entry initial="c" term="cantaloupe" definition="A kind of melon" />
<entry initial="b" term="banana" definition="A tropical yellow fruit" />
<entry initial="a" term="apple" definition="A red fruit with seeds" />
<entry initial="o" term="orange" definition="An orange citrus fruit" />
<entry initial="c" term="Cherry" definition="A red fruit that grows in clusters " />
<entry initial="c" term="cranberry" definition="A sour berry enjoyed at Thanksgiving" />
<entry initial="a" term="avocado" definition="A mellow fruit enjoyed in guacamole" />
</glossary>
then if you use this prepped version as the glossary, the main XSLT can be rid of all those ugly translate() functions and becomes a lot cleaner:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" doctype-system="about:legacy-compat"
encoding="UTF-8" indent="yes" />
<xsl:key name="kEntryInitial" match="entry/#initial" use="."/>
<xsl:template match="/">
<html>
<head></head>
<body>
<!-- Jump into the data.xml DOM so that keys work -->
<xsl:apply-templates select="document('data2.xml')/glossary" />
</body>
</html>
</xsl:template>
<xsl:template match="/glossary">
<!-- Select terms with distinct initials (case invariant) -->
<xsl:variable name="termsByDistinctInitial"
select="entry/#initial[generate-id() =
generate-id(key('kEntryInitial', .)[1])]" />
<!-- Header -->
<xsl:apply-templates select="$termsByDistinctInitial" mode="header">
<xsl:sort select="." data-type="text" order="ascending" />
</xsl:apply-templates>
<!-- Glossary -->
<dl>
<xsl:apply-templates select="$termsByDistinctInitial" mode="main">
<xsl:sort select="." data-type="text" order="ascending" />
</xsl:apply-templates>
</dl>
</xsl:template>
<xsl:template match="#initial" mode="header">
<a href="#{.}">
<xsl:value-of select="." />
</a>
<xsl:if test="position() != last()">
<xsl:text> |</xsl:text>
</xsl:if>
</xsl:template>
<xsl:template match="#initial" mode="main">
<a name="{.}">
<h1>
<xsl:value-of select="." />
</h1>
</a>
<xsl:apply-templates select="key('kEntryInitial', .)/.." />
</xsl:template>
<xsl:template match="entry">
<dt>
<xsl:apply-templates select="#term"/>
</dt>
<dd>
<xsl:apply-templates select="#definition"/>
</dd>
</xsl:template>
</xsl:stylesheet>
Of course, the final output is the same as the first example. If your XSLT processor supports the node-set() function, it's also possible to do both of these processing steps in a single XSLT.
The technique you need is called Muenchian grouping. First define a key that groups entry elements by the downcased first letter of their term
<xsl:key name="entryByInitial" match="entry" use="translate(substring(#term, 1, 1), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" />
Then you use a trick with generate-id to extract just the first element that matches each key
<xsl:for-each select="document('data.xml')">
<!-- iterate over the "groups" to build the top links -->
<xsl:for-each select="glossary/entry[generate-id() = generate-id(key('entryByInitial', translate(substring(#term, 1, 1), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'))[1])]">
<xsl:sort select="translate(#term, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" data-type="text" order="ascending"/>
<xsl:variable name="initial" select="translate(substring(#term, 1, 1), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" />
<!-- insert a leading | before all but the first link -->
<xsl:if test="position() > 1"> | </xsl:if>
<xsl:value-of select="$initial" />
</xsl:for-each>
<!-- iterate over the groups again -->
<xsl:for-each select="glossary/entry[generate-id() = generate-id(key('entryByInitial', translate(substring(#term, 1, 1), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'))[1])]">
<xsl:sort select="translate(#term, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" data-type="text" order="ascending"/>
<xsl:variable name="initial" select="translate(substring(#term, 1, 1), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" />
<a name="{$initial}"><h1><xsl:value-of select="$initial" /></h1></a>
<dl>
<!-- apply templates for all entries with this key value -->
<xsl:apply-templates select="key('entryByInitial', $initial)">
<xsl:sort select="translate(#term, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')" data-type="text" order="ascending"/>
</xsl:apply-templates>
</dl>
</xsl:for-each>
</xsl:for-each>
and define a separate template
<xsl:template match="entry">
<dt><xsl:apply-templates select="#term"/></dt>
<dd><xsl:apply-templates select="#definition"/></dd>
</xsl:template>
Related
I am acting on a set of documents that have a <DataTypes> area, which defines the structure of groups of primative datatypes and other structures, and a <Tags> area, which defines the values of instances of these datatypes.
Original XML
<?xml version="1.0" encoding="utf-8" ?>
<Program>
<DataTypes>
<DataType Name="String20">
<Member Name="LEN" DataType="INTEGER" Dimension="0" />
<Member Name="DATA" DataType="BYTE" Dimension="20" />
</DataType>
<DataType Name="UDT_Params">
<Member Name="InAlarm" DataType="BIT" Dimension="0" />
<Member Name="SetPoint" DataType="FLOAT" Dimension="0" />
<Member Name="DwellTime" DataType="INTEGER" Dimension="0" />
<Member Name="UserName" DataType="String20" Dimension="0" />
</DataType>
</DataTypes>
<Tags>
<Tag Name="MyParameters" DataType="UDT_Params">
<Data Name="InAlarm" DataType="BIT" Value="0" />
<Data Name="SetPoint" DataType="FLOAT" Value="4.5" />
<Data Name="DwellTime" DataType="INTEGER" Value="10" />
<Data Name="UserName" DataType="String20">
<Data Name="LEN" DataType="INTEGER" Value="3" />
<Data Name="DATA" DataType="String20" > <!--The system I'm working in shows strings as arrays of BYTES in DataType, -->
Bob <!--but calls them out as Strings when they are used as tags. I cannot change it.-->
</Data>
</Data>
</Tag>
</Tags>
</Program>
Stylesheet
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<!--Packing algorithm. Works fine on datatypes, but not on Tags.-->
<xsl:template name="pack-nodes">
<xsl:param name="nodes" />
<!--Omitted for brevity-->
</xsl:template>
<!--Pack DataTypes-->
<xsl:variable name="datatypes-packed">
<xsl:call-template name="pack-nodes">
<xsl:with-param name="nodes" select="/Program/DataTypes/DataType" />
</xsl:call-template>
</xsl:variable>
<!--Write DataTypes to output.-->
<xsl:template match="/Program/DataTypes">
<xsl:copy>
<xsl:for-each select="msxsl:node-set($datatypes-packed)">
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<!--Pack tags-->
<xsl:variable name="tags-packed">
<xsl:call-template name="pack-nodes">
<xsl:with-param name="nodes" select="/Program/Tags/Tag" />
</xsl:call-template>
</xsl:variable>
<!--Write Tags to output.-->
<xsl:template match="/Program/Tags">
<xsl:copy>
<xsl:for-each select="msxsl:node-set($tags-packed)">
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Result
<?xml version="1.0" encoding="utf-8"?>
<Program>
<DataTypes>
<DataType Name="String20">
<Member Name="LEN" DataType="INTEGER" Dimension="0"/>
<Member Name="DATA" DataType="BYTE" Dimension="20"/>
</DataType>
<DataType Name="Parameters" DataType="UDT_Params">
<Member Name="UserName" DataType="String20" Dimension="0"/>
<Member Name="SetPoint" DataType="FLOAT" Dimension="0"/>
<Member Name="DwellTime" DataType="INTEGER" Dimension="0"/>
<Member Name="InAlarm" DataType="BIT" Dimension="0"/>
</DataType>
</DataTypes>
<Tags>
<Tag Name="MyParameters" DataType="UDT_Params">
<Data Name="UserName" DataType="String20">
<Data Name="DATA" DataType="String20"> <!--Note that DATA comes before LEN -->
Bob
</Data>
<Data Name="LEN" DataType="INTEGER" Value="3"/>
</Data>
<Data Name="SetPoint" DataType="FLOAT" Value="4.5"/>
<Data Name="DwellTime" DataType="INTEGER" Value="10"/>
<Data Name="InAlarm" DataType="BIT" Value="0"/>
</Tag>
</Tags>
</Program>
My operations on the DataTypes section adds nodes and changes the node order. For the section to work correctly, the tag elements must match the contents and order of their respective datatypes, exactly.
If I keep a variable in memory of the final state of the DataSet nodes, is there a simple way to have the tag nodes look up their dataset (via the Structure and StructureMember #DataSet attributes, and sort their members accordingly?
I'm having trouble figuring out where to start.
NOTE: Transformation must be in XSLT 1.0. I'm using .Net, and don't want to introduce a lot of dependencies on external libraries.
It's a bit tricky in XSLT 1.0 (isn't everything?) but a technique that sometimes works is to construct a variable $tokens containing the list of tokens in the required order, for example "|Description|Name|ProcessEntityIndex|Severity|...", and then sort on select="string-length(substring-before($tokens, concat('|',#Name)))".
Using Michael Kay's suggesting for sorting, I ended up with the following:
Stylesheet
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
<xsl:output method="xml" indent="yes"/>
<!--Skipped to new part -->
<xsl:template name="sort-by-datatype">
<xsl:param name="tags" />
<xsl:param name="datatypes" />
<xsl:for-each select="msxsl:node-set($tags)">
<!--First, do an edge-check.-->
<xsl:variable name="edge">
<xsl:call-template name="edge-check">
<xsl:with-param name="n1" select="." />
</xsl:call-template>
</xsl:variable>
<xsl:choose>
<!--No children, nothing to sort. Just do a deep copy.-->
<xsl:when test="$edge = 'true'">
<xsl:copy-of select="."/>
</xsl:when>
<xsl:otherwise>
<!--Search for datatype in the DataTypes nodeset, and use it to create a list of members in order.-->
<xsl:variable name="tag-datatype" select="./#DataType" />
<xsl:variable name="tokens-untrimmed">
<xsl:for-each select="msxsl:node-set($datatypes)/DataType[#Name = $tag-datatype]/Member">
<xsl:value-of select="concat(' | ', #Name)"/>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="tokens" select="substring-after($tokens-untrimmed, '|')" />
<xsl:choose>
<!--If tokens string is empty (maybe because we couldn't find the datatype?), just copy the tag as it is, then recurse.-->
<xsl:when test="string-length($tokens) = 0">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:call-template name="sort-by-datatype">
<xsl:with-param name="tags" select="." />
<xsl:with-param name="datatypes" select="$datatypes" />
</xsl:call-template>
</xsl:copy>
</xsl:when>
<!--Otherwise, sort members in the same order as datatype-->
<xsl:otherwise>
<!--Build variable with sorted members.-->
<xsl:variable name="tag-members-sorted">
<xsl:for-each select="*">
<xsl:sort data-type="number" order="ascending" select="string-length(substring-before($tokens, concat(' | ', #Name)))" /> <!--Magic Sort Algorithm-->-->
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:variable>
<!--Copy the parent node node.-->
<xsl:copy>
<xsl:copy-of select="#*"/>
<!--Now sort and copy the children.-->
<xsl:for-each select="msxsl:node-set($tag-members-sorted)/*">
<!--Recurse. This copies the child node.-->
<xsl:call-template name="sort-by-datatype">
<xsl:with-param name="tags" select="." />
<xsl:with-param name="datatypes" select="$datatypes" />
</xsl:call-template>
</xsl:for-each>
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:template>
<!--Pack tags-->
<xsl:variable name="tags-packed">
<xsl:call-template name="sort-by-datatype">
<xsl:with-param name="tags" select="/Program/Tags/Tag" />
<xsl:with-param name="datatypes" select="$datatypes-packed" />
</xsl:call-template>
</xsl:variable>
<!--Skipped for brevity-->
</xsl:stylesheet>
I am using XSLT v1.0 and running it through Microsoft Visual Studio.
I have data (which will be different in every node) but is structured like this:
<ItemGroupData ItemGroupOID="DDG4">
<ItemDataString ItemOID="DDLOCC" AuditRecordID="AR.8452551">5,8,9,2,3</ItemDataString>
<ItemDataString ItemOID="DDLOCL" AuditRecordID="AR.8452551">1,7</ItemDataString>
<ItemDataString ItemOID="DDLOCR" AuditRecordID="AR.8452551">1</ItemDataString>
</ItemGroupData>
There can be any number of values separated by commas in each of the 3 fields.
I am trying to split the data so I can work with each individual integer, and have tried the method suggested by Dimitre Novatchev here: split function in xslt 1.0, but it keeps giving me the error:
"'template' is not a recognized extension element. An error occurred at (0,0).".
The new split/mark templates I created are inside my overall template, which is being used to convert my XML to a CSV file.
Can I have a template within a template? Or do I need to define it outside the main template? Bit of a N00b with XML so any help would be greatly appreciated.
My new templates (note the processedItem bit has been simplified for demonstration purposes):
<xsl:template match="mark">
<xsl:variable name="vrtfSplit">
<xsl:apply-templates/>
</xsl:variable>
<xsl:for-each select="ext:node-set($vrtfSplit)/*">
<processedItem>
<xsl:if test="$varLOCOID='DDLOCL'">
<xsl:value-of select="current() * 100"/>
</xsl:if>
<xsl:if test="$varLOCOID='DDLOCC'">
<xsl:value-of select="current() * 10"/>
</xsl:if>
<xsl:if test="$varLOCOID='DDLOCR'">
<xsl:value-of select="current() * 150"/>
</xsl:if>
</processedItem>
</xsl:for-each>
</xsl:template>
<xsl:template match="text()" name="split">
<xsl:param name="pText" select="."/>
<xsl:if test="string-length($pText) > 0">
<item>
<xsl:value-of select="substring-before(concat($pText, ','), ',')"/>
</item>
<xsl:call-template name="split">
<xsl:with-param name="pText" select="substring-after($pText, ',')"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
Consider the following example:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/ItemGroupData">
<output>
<xsl:apply-templates/>
</output>
</xsl:template>
<xsl:template match="ItemDataString">
<items>
<xsl:call-template name="tokenize-and-process">
<xsl:with-param name="text" select="."/>
<xsl:with-param name="factor">
<xsl:choose>
<xsl:when test="#ItemOID='DDLOCL'">100</xsl:when>
<xsl:when test="#ItemOID='DDLOCC'">10</xsl:when>
<xsl:when test="#ItemOID='DDLOCR'">150</xsl:when>
</xsl:choose>
</xsl:with-param>
</xsl:call-template>
</items>
</xsl:template>
<xsl:template name="tokenize-and-process">
<xsl:param name="text"/>
<xsl:param name="factor" select="1"/>
<xsl:param name="delimiter" select="','"/>
<xsl:variable name="token" select="substring-before(concat($text, $delimiter), $delimiter)" />
<item>
<xsl:value-of select="$token * $factor"/>
</item>
<xsl:if test="contains($text, $delimiter)">
<!-- recursive call -->
<xsl:call-template name="tokenize-and-process">
<xsl:with-param name="text" select="substring-after($text, $delimiter)"/>
<xsl:with-param name="factor" select="$factor"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Applied to your example input XML, the result will be:
Result
<?xml version="1.0" encoding="UTF-8"?>
<output>
<items>
<item>50</item>
<item>80</item>
<item>90</item>
<item>20</item>
<item>30</item>
</items>
<items>
<item>100</item>
<item>700</item>
</items>
<items>
<item>150</item>
</items>
</output>
P.S. No, a template cannot be a child of another template.
From the XML file :
<store >
<tools>
<tool IDT="T1">
<container>B1</container>
<container>B2</container>
</tool>
<tool IDT="T2">
<container>B1</container>
</tool>
<tool IDT="T3">
<container>B2</container>
</tool>
</tools>
<boxes>
<box IDB="B1" height="10" width="20" length="30" weight="4"/>
<box IDB="B2" height="5" width="40" length="30" weight="2"/>
</boxes>
</store>
I try to display for each box the list of tools that go into each box. For that, I wrote the following XSL:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:fn="http://www.w3.org/2005/xpath-functions">
<xsl:output
method="html"
encoding="UTF-8"
doctype-public="-//W3C//DTD HTML 4.01//EN"
doctype-system="http://www.w3.org/TR/html4/strict.dtd"
indent="yes" />
<xsl:template match="/">
<html>
<head>
<title>Boxes contents</title>
<link type="text/css" rel="stylesheet" href="styles.css" />
</head>
<body>
<h1>Boxes contents</h1>
<ul>
<xsl:apply-templates select="/store/boxes/box" />
</ul>
</body>
</html>
</xsl:template>
<xsl:template match="box" >
<li><xsl:text>Box </xsl:text>
<xsl:value-of select="#ID"/>
<xsl:text>contains the following tools : </xsl:text>
</li>
<xsl:call-template name="findTools" >
<xsl:with-param name="currentBOX" select="#IDB"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="findTools" >
<xsl:param name="currentBOX" />
<xsl:for-each select="/store/tools/tool/container" >
<xsl:if test="container = $currentBOX" >
<br><xsl:value-of select="#IDT"/></br>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
When I do it, I never see the tools. In debug under OXYGEN, I see that the IF is never true. I do not understand why? I start in XPath and XSLT, thanks for your help
You already are at a <container> element inside the <xsl:for-each>. There are no children, so selecting another <container> inside the <xsl:if> won't return anything.
You mean to execute your check from the <tool> node.
<xsl:for-each select="/store/tools/tool">
<xsl:if test="container = $currentBOX">
<xsl:value-of select="#IDT"/><br />
</xsl:if>
</xsl:for-each>
which is easier written as
<xsl:for-each select="/store/tools/tool[container = $currentBOX]">
<xsl:value-of select="#IDT"/><br />
</xsl:for-each>
Overall a more straight-forward way to write the two templates would be this:
<xsl:template match="box">
<li>
<xsl:text>Box </xsl:text>
<xsl:value-of select="#ID"/>
<xsl:text>contains the following tools : </xsl:text>
</li>
<xsl:apply-templates select="/store/tools/tool[container = current()/#IDB]" />
</xsl:template>
<xsl:template match="tool">
<xsl:value-of select="#IDT"/><br />
</xsl:template>
And alternatively you can use an <xsl:key> to index <tool> elements by their <container> value:
<xsl:key name="kToolByContainer" match="/store/tools/tool" use="container" />
<xsl:template match="box">
<li>
<xsl:text>Box </xsl:text>
<xsl:value-of select="#ID"/>
<xsl:text>contains the following tools : </xsl:text>
</li>
<xsl:apply-templates select="key('kToolByContainer', #IDB)" />
</xsl:template>
<xsl:template match="tool">
<xsl:value-of select="#IDT"/><br />
</xsl:template>
I have an xml structure like the following :
<doc>
<line void="false">
<lineNumber>1</lineNumber>
<info1>ddddd</info1>
<info2>aaaaa</info2>
</line>
<line void="true">
<lineNumber>2</lineNumber>
<voidLineNumber>1</voidLineNumber>
<voidValue>2.00</voidValue>
</line>
</doc>
I need one single set of data. I would like to select all the lines where void = false as well as the voidLineNumber and voidValue data from the line where void = true and the voidLineNumber = lineNumber from the original line.
Is this possible? Any help would be appreciated. Thanks
As Michael Kay noted, XPath itself can only be used to select nodes, not to transform them. You can do what you want with XSLT:
<?xml version="1.0" ?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" encoding="utf-8" indent="yes" />
<xsl:template match="doc">
<xsl:apply-templates select="line" />
</xsl:template>
<xsl:template match="line">
<xsl:variable name="voidvalue"><xsl:value-of select="#void" /></xsl:variable>
<xsl:if test="$voidvalue='false'">
<xsl:copy>
<xsl:copy-of select="#*" />
<xsl:apply-templates />
</xsl:copy>
</xsl:if>
</xsl:template>
<xsl:template match="*">
<xsl:element name="{name()}">
<xsl:if test="name(.)='lineNumber'">
<xsl:value-of select="."/>
</xsl:if>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
For debugging purposes it would be handy to output the full path of the context node from within a template, is there unabbreviated xpath or function to report this ?
Example Template:
<xsl:template match="#first">
<tr>
<td>
<xsl:value-of select="??WHAT TO PUT IN HERE??"/>
</td>
</tr>
</xsl:template>
Example (Abridged) input document:
<people>
<person>
<name first="alan">
...
The output from the template would be something like:
people / person / name / #first
Or something similar.
This transformation produces an XPath expression for the wanted node:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<xsl:variable name="vNode" select=
"/*/*[2]/*/#first"/>
<xsl:apply-templates select="$vNode" mode="path"/>
</xsl:template>
<xsl:template match="*" mode="path">
<xsl:value-of select="concat('/',name())"/>
<xsl:variable name="vnumPrecSiblings" select=
"count(preceding-sibling::*[name()=name(current())])"/>
<xsl:variable name="vnumFollSiblings" select=
"count(following-sibling::*[name()=name(current())])"/>
<xsl:if test="$vnumPrecSiblings or $vnumFollSiblings">
<xsl:value-of select=
"concat('[', $vnumPrecSiblings +1, ']')"/>
</xsl:if>
</xsl:template>
<xsl:template match="#*" mode="path">
<xsl:apply-templates select="ancestor::*" mode="path"/>
<xsl:value-of select="concat('/#', name())"/>
</xsl:template>
</xsl:stylesheet>
when applied on the following XML document:
<people>
<person>
<name first="betty" last="jones"/>
</person>
<person>
<name first="alan" last="smith"/>
</person>
</people>
the wanted, correct result is produced:
/people/person[2]/name/#first
Here's a stylesheet (of dubious value) that prints the path to every element and attribute in a document:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:strip-space elements="*" />
<xsl:template match="*">
<xsl:param name="pathToHere" select="''" />
<xsl:variable name="precSiblings"
select="count(preceding-sibling::*[name()=name(current())])" />
<xsl:variable name="follSiblings"
select="count(following-sibling::*[name()=name(current())])" />
<xsl:variable name="fullPath"
select="concat($pathToHere, '/', name(),
substring(concat('[', $precSiblings + 1, ']'),
1 div ($follSiblings or $precSiblings)))" />
<xsl:value-of select="concat($fullPath, '
')" />
<xsl:apply-templates select="#*|*">
<xsl:with-param name="pathToHere" select="$fullPath" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="#*">
<xsl:param name="pathToHere" select="''" />
<xsl:value-of select="concat($pathToHere, '/#', name(), '
')" />
</xsl:template>
</xsl:stylesheet>
When applied to this input:
<people>
<person>
<name first="betty" last="jones" />
</person>
<person>
<name first="alan" last="smith" />
</person>
<singleElement />
</people>
Produces:
/people
/people/person[1]
/people/person[1]/name
/people/person[1]/name/#first
/people/person[1]/name/#last
/people/person[2]
/people/person[2]/name
/people/person[2]/name/#first
/people/person[2]/name/#last
/people/singleElement