XSLT Sort elements using dictionary - xslt

I have an XML which is a list of countries with their codes:
<Countries>
<Country Code="DE" />
<Country Code="FR" />
</Countries>
I apply XSLT to this XML to transform into HTML and I want elements to be sorted alphabetically by country name rather than code.
So I defined CountryCodeDictionaryEN variable which is a dictionary of country codes and names. It works fine in <xsl:value-of /> but not inside <xsl:sort />
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt">
<xsl:variable name="CountryCodeDictionaryEN">
<item key="Germany"
value="DE" />
<item key="France"
value="FR" />
... all other countries ...
</xsl:variable>
<xsl:template match="/">
<table>
<xsl:for-each select="Countries/Country">
<xsl:sort select="msxsl:node-set($CountryCodeDictionaryEN)/item[#value = #Code]/#key"/>
<tr>
<td>
<xsl:variable name="Code" select="#Code"/>
<xsl:value-of select="msxsl:node-set($CountryCodeDictionaryEN)/item[#value = $Code]/#key"/>
</td>
</tr>
</table>
</xsl:template>
</xsl:stylesheet>

item[#value = #Code] compares the value attribute of an item against the Code attribute of the same item. Since the item elements don't have a Code attribute, the select expression will always result in an empty node set, so every node will get the same (empty string) sort key. And since xsl:sort is stable (items with the same sort key value retain their original relative ordering) the overall effect is no sorting.
You need to use current() to "break out" of the predicate and refer to the node being sort-ed:
<xsl:sort select="msxsl:node-set($CountryCodeDictionaryEN)
/item[#value = current()/#Code]/#key"/>
This will compare the value of the item against the Code of the Country.

Related

select xsl:variable values of its child-nodes

I have an .ods file and want to access the values of table-cells in table-rows by the value of the first column for the given row. So their heading in my case.
So the calc table looks like this:
First_Name | Last_Name
Peter | Parker
Emma | Stone
...
Here is my xslt-export-filter file:
SuperBasicExportFilter.xslt
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
exclude-result-prefixes="table text office"
>
<xsl:output
method="xml"
indent="yes"
encoding="UTF-8"
omit-xml-declaration="no"
/>
<xsl:template match="/">
<xsl:variable name="columnHeadings">
<xsl:for-each select="//table:table/table:table-row[not(preceding::table:table-row)]//table:table-cell">
<xsl:element name="heading">
<xsl:attribute name="name" select="text:p" />
<xsl:value-of select="position()" />
</xsl:element>
</xsl:for-each>
</xsl:variable>
<html>
<body>
<h1>Hello</h1>
<xsl:message>columnHeadings: <xsl:value-of select="$columnHeadings" /></xsl:message>
<table>
<xsl:for-each select="//table:table/table:table-row">
<xsl:if test="position() > 1">
<tr>
<td>
First Column Value
<xsl:value-of select="table:table-cell[1]/text:p" />
<!-- <xsl:value-of select="table:table-cell[$columnHeadings/heading[#name='First_Name']]/text:p" /> -->
</td>
<td>
Second Column Value
<xsl:value-of select="table:table-cell[2]/text:p" />
<!-- <xsl:value-of select="table:table-cell[$columnHeadings/heading[#name='Last_Name']]/text:p" /> -->
</td>
</tr>
</xsl:if>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
The message shows "columnHeadings: 1234567891011121314" and so on. So it is setting the position values correctly.
I tried getting the values based on the "name" attribute on the "heading" element.
But I can't get the values individually in any way. It seems I can't use the $columnHeadings with any XPath expression. It just returns "Xpath evaluation returned no result".
I tried
wrapping the "heading" elements with a "columnHeadings" element inside the variable and setting the "as" value of the variable to "element()"
using the "node-set" function (after importing the "exslt" ns)
using <xsl:variable name="columnHeadingsNode" select="document('')//xsl:variable[#name = 'columnHeadings']" /> to then get the value
using the xsl:key element like <xsl:key name="columnHeadings" match="//table:table/table:table-row[not(preceding::table:table-row)]//table:table-cell" use="text:p" /> - but this way I can't access it based on the "name"
What other things can I try to access the variable contents with a xpath expression?
Is it even possible to access the values like table:table-cell[$columnHeadings/heading[#name='Last_Name']]?
Answers to comments:
Which XSLT processor are you using?
I'm using whatever libreoffice 7.4.5.1 is using.
Can I change that?
The xsl:vendor is "libxslt" and the version is "1.0" according to the <xsl:value-of select="system-property('xsl:vendor')"/> and xsl:version values.
Do you get an error on <xsl:attribute name="name" select="text:p" />?
I actually do not for some reason. The test runs through without errors. I get a new browser tab with the produced xml output and no errors.
I tried ticking the "The filter needs XSLT 2.0 processor" but then I can't test run the filter anymore and don't get any output.
What's the overall purpose of this exercise?
I want to be able to select the values in the columns by their respective column heading, instead of the index, because I want to make it as portable as possible. At least I think that would help to achieve that goal. I have 184 columns. The column names won't change as likely as the index of the column, I believe.
The intent <xsl:attribute name="name" select="text:p" /> fails to create an attribute with a value in XSLT 1; it should raise an error but it seems your XSLT processor kind of ignores the select.
So try
<xsl:attribute name="name">
<xsl:value-of select="text:p"/>
</xsl:attribute>
instead.
That way, I would think that e.g. <xsl:variable name="columnHeadings-ns" select="exsl:node-set($columnHeadings)" xmlns:exsl="http://exslt.org/common"/> should allow you to use e.g. <xsl:value-of select="table:table-cell[$columnHeadings-ns/heading[#name='First_Name']]/text:p"/>.

Parse dynamic XML into html table

I have a problem when parsing dynamic xml data into a html table. The XML is as follow.
<results>
<result id="1" desc="Voltage and current">
<measure desc="VOLT" value="1.0" />
<measure desc="AMPERE" value="2.0" />
</result>
<result id="2" desc="Current-1">
<measure desc="AMPERE" value="5.0" />
</result>
</results>
from which I would like a html table like:
ID DESC VOLT AMPERE
1 Voltage and current 1.0 2.0
2 Current-1 5.0
Notice the empty cell at second voltage column. ID and DESC is taken from result/#id and result/#desc and the rest of the column names should come from measure/#desc
No column name should be duplicate, I managed to code that far, but when I start adding my measures I need to match each measure/#desc to correct column in the table. I tried double nested loops to first match all unique column names, and then loop all measures again to match the column header. But the xslt parser threw a NPE on me!
Sorry that I can't show any code as it is on a non-connected computer.
I've browsed so many Q/A here on SO but to no help for my specific problem.
Thanks in advance
Note: I am able to change the XML format in any way to make parsing easier if anyone come up with a neater format.
If you are using XSLT1.0, you can use a technique called 'Muenchian' grouping to get the distinct measure descriptions, which will form the basis of your head row, and also be used to output the values of each row.
Firstly, you define a key to look up measure elements by their #desc attribute
<xsl:key name="measures" match="measure" use="#desc" />
Then, to get the distinct measure descriptions you can iterate over the measure elements that appear first in the group for their given #desc attribute
<xsl:apply-templates
select="result/measure[generate-id() = generate-id(key('measures', #desc)[1])]"
mode="header" />
Then, for your header, you would simply have a template to output the description.
<xsl:template match="measure" mode="header">
<th>
<xsl:value-of select="#desc" />
</th>
</xsl:template>
For each result row, would do a similar thing, and iterate over all distinct measure values, but the only difference is you would have to pass in the current result element as a parameter, for later use.
<xsl:apply-templates
select="/results/result/measure[generate-id() = generate-id(key('measures', #desc)[1])]"
mode="data">
<xsl:with-param name="result" select="." />
</xsl:apply-templates>
Then, in the template that matched the measure this time, you could access the measure within the result element with a matching #desc attribute (and id there is no such attribute, nothing is output for the cell)
<xsl:template match="measure" mode="data">
<xsl:param name="result" />
<td>
<xsl:value-of select="$result/measure[#desc = current()/#desc]/#value" />
</td>
</xsl:template>
Here is the full XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="measures" match="measure" use="#desc" />
<xsl:template match="/results">
<table>
<tr>
<th>ID</th>
<th>DESC</th>
<xsl:apply-templates select="result/measure[generate-id() = generate-id(key('measures', #desc)[1])]" mode="header" />
</tr>
<xsl:apply-templates select="result" />
</table>
</xsl:template>
<xsl:template match="result">
<tr>
<td><xsl:value-of select="#id" /></td>
<td><xsl:value-of select="#desc" /></td>
<xsl:apply-templates select="/results/result/measure[generate-id() = generate-id(key('measures', #desc)[1])]" mode="data">
<xsl:with-param name="result" select="." />
</xsl:apply-templates>
</tr>
</xsl:template>
<xsl:template match="measure" mode="header">
<th>
<xsl:value-of select="#desc" />
</th>
</xsl:template>
<xsl:template match="measure" mode="data">
<xsl:param name="result" />
<td>
<xsl:value-of select="$result/measure[#desc = current()/#desc]/#value" />
</td>
</xsl:template>
</xsl:stylesheet>
Note the use of the mode attributes because you have two templates matching the measure element, which function in different ways.
When applied to your input XML, the following is output
<table>
<tr>
<th>ID</th>
<th>DESC</th>
<th>VOLT</th>
<th>AMPERE</th>
</tr>
<tr>
<td>1</td>
<td>Voltage and current</td>
<td>1.0</td>
<td>2.0</td>
</tr>
<tr>
<td>2</td>
<td>Current-1</td>
<td/>
<td>5.0</td>
</tr>
</table>

XSLT: Is it possible to select unique IDs into variable ordered by a criteria other than the ID itself?

Imagine the following xml
<elements>
<element>
<elementID>0x1000</elementID>
<elementSort>1</elementSort>
<elementName>Master Joda</elementName>
<modifyDate>1979-01-01</modifyDate>
</element>
<element>
<elementID>0x1000</elementID>
<elementSort>1</elementSort>
<elementName>Master Yoda</elementName>
<modifyDate>1979-01-05</modifyDate>
</element>
<element>
<elementID>0x2000</elementID>
<elementSort>2</elementSort>
<elementName>Luke Skywalker</elementName>
<modifyDate>1979-01-08</modifyDate>
</element>
</elements>
I use the following xslt to select a list of unique IDs into a variable
<xsl:variable name="ids"
select="elements/element/elementID[not(.=following::elementID)]" />
Then i let xslt build some html for each ID (the output will be a horizontal list of elements per ID)
<xsl:for-each select="$ids">
<xsl:variable name="elementID" select="." />
<div class="itemContainer clear" style="width:{$containerWidth}">
<xsl:for-each select="/elements/element[elementID=$elementID]">
<xsl:sort select="modifyDate" />
<xsl:call-template name="elementTemplate" />
</xsl:for-each>
</div>
</xsl:for-each>
The problem is: how can i sort the elements in the first level of the for-each nesting (the IDs) without having the Element by which i want to sort in the list itself (the ID list).
In practical terms: how can i sort by Jedi hierarchy (master -> pupil), if elementSort 1 means master and elementSort 2 means pupil, having multiple elements per hierarchy in each row, which are then ordered by modifyDate.
Instead of:
<xsl:for-each select="/elements/element/elementID[elementID=$elementID]">
<xsl:sort select="modifyDate" />
<xsl:call-template name="elementTemplate" />
</xsl:for-each>
BTW, the above is obviously incorrect, because an elementID element doesn't have any elementID child at all.
use:
<xsl:for-each select="/elements/element[elementID=$elementID]">
<xsl:sort select="elementSort" data-type="number" />
<xsl:sort select="modifyDate" />
<xsl:call-template name="elementTemplate" />
</xsl:for-each>
I have found a solution though it is probably not very good "xslt-design".
I store list of the unique elementSorts in an additional variable and put another for-each around the original first one. Then i combine the conditions (uniqueness and sortID) while setting the variable holding the unique element IDs.
<xsl:variable name="ids"
select="elements/element/elementID[not(.=following::elementID) and ../elementSort=$sort]" />
edit:
probably even better:
<xsl:variable name="ids"
select="elements/element[elementSort=$sort]/elementID[not(.=following::elementID)]" />
another note
if there are other nodes in the xml document that can contain elementID nodes, than you would have to specify the following:: clause as follows to avoid unwanted behaviour:
<xsl:variable name="ids"
select="elements/element[elementSort=$sort]/elementID[not(.=following::element/elementID)]" />
That way only elementIDs inside of element nodes are taken into consideration for evaluating the uniqueness, comes in handy if there were nodes that relate to the element nodes by elementID.

XSLT matching PAGEID to an element ID

How would I match two separate numbers in an XML document? There are multiple <PgIndexElementInfo> elements in my XML document, each representing a different navigation element, each with a unique <ID>. Later in the document a <PageID> specifies a number that sometimes matches an <ID> used above. How could I go about matching the <PageID> to the <ID> specified above?
<Element>
<Content>
<PgIndexElementInfo>
<ElementData>
<Items>
<PgIndexElementItem>
<ID>1455917</ID>
</PgIndexElementItem>
</Items>
</ElementData>
</PgIndexElementInfo>
</Content>
</Element>
<Element>
<Content>
<CustomElementInfo>
<PageID>1455917</PageID>
</CustomElementInfo>
</Content>
</Element>
EDIT:
I added the solution below to my code. The xsl:apply-templates that is present is used to recreate the nested lists that are lost between HTML and XML. What I now need to do is match the PageID to the ID of a <PgIndexElementItem> and add a CSS class to the <ul> it is a part of. I hope that makes sense.
<xsl:key name="kIDByValue" match="ID" use="."/>
<xsl:template match="PageID[key('kIDByValue',.)]">
<xsl:apply-templates select="//PgIndexElementItem[not(contains(Description, '.'))]" />
</xsl:template>
<xsl:template match="PgIndexElementItem">
<li>
<xsl:value-of select="Title"/>
<xsl:variable name="prefix" select="concat(Description, '.')"/>
<xsl:variable name="childOptions"
select="../PgIndexElementItem[starts-with(Description, $prefix)
and not(contains(substring-after(Description, $prefix), '.'))]"/>
<xsl:if test="$childOptions">
<ul>
<xsl:apply-templates select="$childOptions" />
</ul>
</xsl:if>
</li>
</xsl:template>
The XSLT way for dealing with cross references is with keys.
Matching: A rule matching every PageID element that it has been referenced by an ID element.
<xsl:key name="kIDByValue" match="ID" use="."/>
<xsl:template match="PageID[key('kIDByValue',.)]">
<!-- Template content -->
</xsl:template>
Selecting: A expression selecting every PageID element with specific value.
<xsl:key name="kPageIDByValue" match="PageID" use="."/>
<xsl:template match="ID">
<xsl:apply-templates select="key('kPageIDByValue',.)"/>
</xsl:template>

How do I get an XML node with xpath in a loop, based on an attribute with the same name as the attribute in the tree I'm searching?

I cant really formulate that better, so I'll go with an example instead:
XML:
<root>
<foo>
<bar id="1">sdf</bar>
<bar id="2">sdooo</bar
</foo>
<feng>
<heppa id="4">hihi</heppa>
<heppa id="2">sseeapeea</heppa>
<heppa id="1">....</heppa>
</feng>
</root>
XSLT:
<xsl:for-each select="/root/foo/bar">
<p>
<xsl:value-of select="." />: <xsl:value-of select="/root/feng/heppa[#id = #id]" />
</p>
</xsl:for-each>
Desired output:
<p>sdf: ....</p>
<p>sdooo: sseeapeea</p>
Actual output:
<p>sdf: hihi</p>
<p>sdooo: hihi</p>
For selecting nodes with XPath 1.0 only, you need to do a node set comparison:
/root/feng/heppa[#id=/root/foo/bar/#id]
Of course, this has NxM complexity (as the others XSLT solutions)
With XSLT 1.0 you should use keys because there are cross references:
<xsl:key name="kBarById" select="bar" use="#id"/>
<xsl:template match="/root/feng/heppa[key('kBarById',#id)]">
<p>
<xsl:value-of select="concat(key('kBarById',#id),': ',.)"/>
</p>
</xsl:template>
I assume you mean /root/foo/bar since /root/foo elements don't have id.
You're comparing the #id with itself, so of course it's true for the first node examined. You can use current() to reference the current node in an expression:
<xsl:for-each select="/root/foo/bar">
<p>
<xsl:value-of select="." />: <xsl:value-of select="/root/feng/heppa[#id = current()/#id]" />
</p>
</xsl:for-each>
Another solution is to read the id attribute into a variable.
<xsl:for-each select="/root/foo/bar">
<xsl:variable name="id" select="#id"/>
<p>
<xsl:value-of select="." />: <xsl:value-of select="/root/feng/heppa[#id = $id]" />
</p>
</xsl:for-each>
This might be handier, if your real use case is more complicated and you need to use the value of the id multiple times in this for-each section.