I am using XSLT to generate my HTML.
I am having below xml and I want to write condition if there is only one city node inside a country node I want to write some condition, please see the below xml.
There are two xmls.
1) destinations.xml
<?xml version="1.0"?>
<list type="Destinations">
<resources location="include/xml/locations.xml">
<publication>232</publication>
</resources>
<destination id="594904" title="Maldives" url="/destinations_offers/destinations/asiapacific/maldives/maldives.aspx" thumbnail="/99/english/images/square_tcm481-594879.jpg" FeaturedDestination="true">
<city id="192513" />
</destination>
<destination id="594089" title="New Delhi" url="/destinations_offers/destinations/asiapacific/india/newdelhi.aspx" thumbnail="/99/english/images/sydney_tcm481-594346.jpg" FeaturedDestination="true" NewestDestination="true">
<city id="192460" />
</destination>
</list>
For eample In the above xml there is city id = 192513 for maldives and it is alone node in locations.xml this will be checked in below locations.xml and if that id is alone in that country node then I need to call specific condition.
<?xml version="1.0"?>
<list type="Locations">
<region id="192393" code="ASIA" name="Asia & the Pacific" shortname="Asia & the Pacific">
<country id="192395" code="AU" name="Australia" shortname="Australia">
<city id="192397" code="BNE" name="Brisbane" shortname="Brisbane">
<airport id="192399" code="BNE" name="Brisbane International Airport" shortname="Brisbane"></airport>
</city>
<city id="192409" code="SYD" name="Sydney" shortname="Sydney">
<airport id="192411" code="SYD" name="Kingsford Smith Airport" shortname="Sydney"></airport>
</city>
</country>
<country id="192511" code="MV" name="Maldives" shortname="Maldives">
<city id="192513" code="MLE" name="Male" shortname="Male">
<airport id="192515" code="MLE" name="Male International Airport" shortname="Male"></airport>
</city>
</country>
</region>
</list>
Please suggest!
Thanks.
Use:
count($vLocations/*/*/country[city[#id = $vDestCity/#id]]/city) = 1
In this expression $vLocations is the XML document with top element <list type="Locations"> and $vDestCity is the <city> element we are interested in from the XML document with top element <list type="Destinations">
To see this in action:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="my:my">
<xsl:output method="text"/>
<my:locations>
<list type="Locations">
<region id="192393" code="ASIA"
name="Asia & the Pacific"
shortname="Asia & the Pacific">
<country id="192395" code="AU" name="Australia"
shortname="Australia">
<city id="192397" code="BNE" name="Brisbane"
shortname="Brisbane">
<airport id="192399" code="BNE"
name="Brisbane International Airport"
shortname="Brisbane">
</airport>
</city>
<city id="192409" code="SYD" name="Sydney"
shortname="Sydney">
<airport id="192411" code="SYD"
name="Kingsford Smith Airport"
shortname="Sydney">
</airport>
</city>
</country>
<country id="192511" code="MV" name="Maldives"
shortname="Maldives">
<city id="192513" code="MLE" name="Male"
shortname="Male">
<airport id="192515" code="MLE"
name="Male International Airport"
shortname="Male">
</airport>
</city>
</country>
</region>
</list>
</my:locations>
<xsl:variable name="vLocations"
select="document('')/*/my:locations"/>
<xsl:variable name="vDestCity1"
select="/*/destination/city[#id=192513]"/>
<xsl:variable name="vDestCity2"
select="/*/destination/city[#id=192397]"/>
<xsl:template match="/">
<xsl:value-of select=
"count($vLocations/*/*/country
[city[#id = $vDestCity1/#id]]/city
) = 1
"/>
:
<xsl:text/>
<xsl:value-of select=
"count($vLocations/*/*/country
[city[#id = $vDestCity2/#id]]/city
) = 1
"/>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the provided destinations.xml:
<list type="Destinations">
<resources location="include/xml/locations.xml">
<publication>232</publication>
</resources>
<destination id="594904" title="Maldives" url="/destinations_offers/destinations/asiapacific/maldives/maldives.aspx" thumbnail="/99/english/images/square_tcm481-594879.jpg" FeaturedDestination="true">
<city id="192513" />
</destination>
<destination id="594089" title="New Delhi" url="/destinations_offers/destinations/asiapacific/india/newdelhi.aspx" thumbnail="/99/english/images/sydney_tcm481-594346.jpg" FeaturedDestination="true" NewestDestination="true">
<city id="192460" />
</destination>
</list>
The wanted, correct result is produced:
true
:
false
for example Australia: count(//country[#id='192395']/city)
Related
I'm getting this error in Chrome console:
Seems to be a problem with XSLT or SEF as other SEF works.
I complie SEF with the xslt3 tool on Node.js version v14.17.3.
I can run the XSLT with Saxon 9.8.12-EE in oXygen with no problem.
Maybe the problem with maps, xsl:include or xsl:import?
Here is the XSLT code:
<stylesheet exclude-result-prefixes="xs xd dme functx dita mei map array" extension-element-prefixes="ixsl" version="3.0" xmlns="http://www.w3.org/1999/XSL/Transform" xmlns:array="http://www.w3.org/2005/xpath-functions/array" xmlns:dita="http://dita-ot.sourceforge.net" xmlns:dme="http://www.mozarteum.at/ns/dme" xmlns:functx="http://www.functx.com" xmlns:ixsl="http://saxonica.com/ns/interactiveXSLT" xmlns:map="http://www.w3.org/2005/xpath-functions/map" xmlns:mei="http://www.music-encoding.org/ns/mei" xmlns:xd="http://www.oxygenxml.com/ns/doc/xsl" xmlns:xi="http://www.w3.org/2001/XInclude" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xpath-default-namespace="http://www.music-encoding.org/ns/mei">
<xi:include href="docs.xsl" xpointer="element(/1/1)"/>
<import href="changeLog.xsl"/>
<!--This parameter is a needed for XSpec. Cf. https://github.com/xspec/xspec/wiki/Global-Context-Item-->
<param name="global-context-item" select="."/>
<param name="source"/>
<include href="../modules/identity-transform.xsl"/>
<xd:doc>
<xd:desc>
<xd:p>Replace current scoreDef with the alternative. Note that this should be defined in the following-sibling <choice>.</xd:p>
</xd:desc>
</xd:doc>
<template match="scoreDef[not(#corresp = concat('#', $source))]">
<copy-of select="following-sibling::choice[#type = 'scoring']/*[#corresp = concat('#', $source)]/scoreDef"/>
</template>
<xd:doc>
<xd:desc/>
</xd:doc>
<template match="choice[#type = 'scoring']"/>
<xd:doc>
<xd:desc>
<xd:p>Map staffDef#n of the new scoreDef and #dme.parts.</xd:p>
</xd:desc>
</xd:doc>
<variable as="map(xs:string, item())*" name="map_staves_order">
<for-each select="$global-context-item//choice[#type = 'scoring']/*[#corresp = concat('#', $source)]/scoreDef">
<map>
<for-each select="descendant::staffDef">
<map-entry key="string(#n)" select="#dme.parts"/>
</for-each>
</map>
</for-each>
</variable>
<xd:doc>
<xd:desc>
<xd:p>Get the maximum number of staves.</xd:p>
</xd:desc>
</xd:doc>
<variable as="xs:integer" name="count_staves" select="
max(for $a in map:keys($map_staves_order)
return
if (matches($a, '\d')) then
xs:integer($a)
else
())"/>
<xd:doc>
<xd:desc>
<xd:p>Key: old #n.</xd:p>
<xd:p>Value: new #n</xd:p>
</xd:desc>
</xd:doc>
<variable as="map(xs:string, xs:string)*" name="map_new_old_staves">
<map>
<for-each select="map:keys($map_staves_order)">
<variable name="current_key" select="."/>
<map-entry key="$global-context-item//measure[#n = 1]/staff[#dme.parts = map:get($map_staves_order, $current_key)]/#n/string()" select="."/>
</for-each>
</map>
</variable>
<xd:doc>
<xd:desc>
<xd:p>Copies elements before first staff without changes, e.g. <tempo></xd:p>
<xd:p>Copies staves accordingly to the new score order. Changes the #n-attribute accordingly</xd:p>
<xd:p>Copies ControlEvents and updates the #staff attribute if neccessary.</xd:p>
</xd:desc>
</xd:doc>
<template match="measure">
<copy>
<apply-templates select="#*"/>
<apply-templates select="child::staff[1]/preceding-sibling::*"/>
<variable as="element()" name="currentMeasure" select="."/>
<for-each select="1 to $count_staves">
<variable as="xs:string" name="currentItem" select="string()"/>
<for-each select="$currentMeasure/child::staff[map:get($map_staves_order, $currentItem) = #dme.parts]">
<copy>
<attribute name="n" select="$currentItem"/>
<apply-templates select="(#* except #n) | node()"/>
</copy>
</for-each>
</for-each>
<call-template name="update_staff_controlEvents">
<with-param name="elements" select="child::staff[last()]/following-sibling::*"/>
</call-template>
</copy>
</template>
<xd:doc>
<xd:desc>
<xd:p>Receives one or more elements (controlEvents) and updates their #staff according to the $map_new_old_staves variable.</xd:p>
<xd:p>Recursive template</xd:p>
</xd:desc>
<xd:param name="elements"/>
</xd:doc>
<template name="update_staff_controlEvents">
<param name="elements"/>
<for-each select="$elements">
<choose>
<when test="#staff">
<copy>
<attribute name="staff" select="map:get($map_new_old_staves, #staff)"/>
<apply-templates select="#* except #staff"/>
<if test="node()">
<call-template name="update_staff_controlEvents">
<with-param name="elements" select="node()"/>
</call-template>
</if>
</copy>
</when>
<otherwise>
<copy>
<apply-templates select="#*"/>
<if test="node()">
<call-template name="update_staff_controlEvents">
<with-param name="elements" select="node()"/>
</call-template>
</if>
</copy>
</otherwise>
</choose>
</for-each>
</template>
</stylesheet>
JS which I run on Console:
var options = {
stylesheetLocation: "./assets/xsl/origScoring.sef.json",
sourceText: xmlString,
stylesheetParams: {"source": "source_A"},
destination: "document"
}
var result = SaxonJS.transform(options);
result.principalResult
Owing to the Saxon developers I could easily solve my issue by adding namespace prefixes in the XPath expressions when creating maps. Originally, I used xpath-default-namespace. Fo instance, here on the elements measure and staff should be:
<map-entry key="$global-context-item//mei:measure[#n = 1]/mei:staff[#dme.parts = map:get($map_staves_order, $current_key)]/#n/string()" select="."/>
instead of
<map-entry key="$global-context-item//measure[#n = 1]/staff[#dme.parts = map:get($map_staves_order, $current_key)]/#n/string()" select="."/>
I have a large XML file to transform using XSLT to append the integer position of sibling node . I’m using XSLT3 streaming and accumulators. I did get desired output. However, my code looks so lengthy that I’m unable to simplify my code. I also need to group same sibling nodes as sibling nodes in the source xml is not grouped always. Could someone help me here please?
Requirement: Sibling nodes such as Positions, Payments etc.. need to be appended with their corresponding integer position such as <Locations1>, <Locations2>etc.<Payments1>,< Payments2> etc..
Now that I have declared two accumulators, each for each sibling nodes. However, my source XML has many sibling nodes.. I’m not sure if I need to use as many accumulators and template match as my sibling nodes.
Input XML
``
<?xml version="1.0" encoding="UTF-8"?>
<Members>
<Member>
<Name>
<fname>Fred</fname>
<id>1234</id>
</Name>
<Locations>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations>
<Locations>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations>
<Payments>
<amount>1000</amount>
<currency>USD</currency>
</Payments>
<Payments>
<amount>1000</amount>
<currency>USD</currency>
</Payments>
<Locations>
<name>New York</name>
<days>5</days>
<hours>40</hours>
</Locations>
<Locations>
<name>Boston</name>
<days>4</days>
<hours>32</hours>
</Locations>
</Member>
<Member>
<Name>
<fname>Jack</fname>
<id>4567</id>
</Name>
<Locations>
<name>New York</name>
<days>5</days>
<hours>30</hours>
</Locations>
<Locations>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations>
<Payments>
<amount>1500</amount>
<currency>USD</currency>
</Payments>
<Payments>
<amount>1800</amount>
<currency>USD</currency>
</Payments>
</Member>
</Members>
``
Expected Output
``
<?xml version="1.0" encoding="UTF-8"?>
<Members>
<Member>
<Name>
<fname>Fred</fname>
<id>1234</id>
</Name>
<Locations_1>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations_1>
<Locations_2>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations_2>
<Locations_3>
<name>New York</name>
<days>5</days>
<hours>40</hours>
</Locations_3>
<Locations_4>
<name>Boston</name>
<days>4</days>
<hours>32</hours>
</Locations_4>
<Payments_1>
<amount>1000</amount>
<currency>USD</currency>
</Payments_1>
<Payments_2>
<amount>1000</amount>
<currency>USD</currency>
</Payments_2>
</Member>
<Member>
<Name>
<fname>Jack</fname>
<id>4567</id>
</Name>
<Locations_1>
<name>New York</name>
<days>5</days>
<hours>30</hours>
</Locations_1>
<Locations_2>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations_2>
<Payments_1>
<amount>1500</amount>
<currency>USD</currency>
</Payments_1>
<Payments_2>
<amount>1800</amount>
<currency>USD</currency>
</Payments_2>
</Member>
</Members>
``
Current code
``
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs" version="3.0">
<xsl:output method="xml" indent="yes"/>
<xsl:mode streamable="yes" on-no-match="shallow-copy" use-accumulators="#all"/>
<xsl:accumulator name="loc-count" as="xs:integer" initial-value="0" streamable="yes">
<xsl:accumulator-rule match="Member" select="0"/>
<xsl:accumulator-rule match="Member/Locations" select="$value + 1"/>
</xsl:accumulator>
<xsl:accumulator name="pay-count" as="xs:integer" initial-value="0" streamable="yes">
<xsl:accumulator-rule match="Member" select="0"/>
<xsl:accumulator-rule match="Member/Payments" select="$value + 1"/>
</xsl:accumulator>
<xsl:template match="Locations">
<xsl:element name="Locations_{accumulator-before('loc-count')}">
<xsl:copy-of select="#* | node()"/>
</xsl:element>
</xsl:template>
<xsl:template match="Payments">
<xsl:element name="Payments_{accumulator-before('pay-count')}">
<xsl:copy-of select="#* | node()"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
``
Current Output
<?xml version="1.0" encoding="UTF-8"?>
<Members>
<Member>
<Name>
<fname>Fred</fname>
<id>1234</id>
</Name>
<Locations_1>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations_1>
<Locations_2>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations_2>
<Payments_1>
<amount>1000</amount>
<currency>USD</currency>
</Payments_1>
<Payments_2>
<amount>1000</amount>
<currency>USD</currency>
</Payments_2>
<Locations_3>
<name>New York</name>
<days>5</days>
<hours>40</hours>
</Locations_3>
<Locations_4>
<name>Boston</name>
<days>4</days>
<hours>32</hours>
</Locations_4>
</Member>
<Member>
<Name>
<fname>Jack</fname>
<id>4567</id>
</Name>
<Locations_1>
<name>New York</name>
<days>5</days>
<hours>30</hours>
</Locations_1>
<Locations_2>
<name>Chicago</name>
<days>3</days>
<hours>24</hours>
</Locations_2>
<Payments_1>
<amount>1500</amount>
<currency>USD</currency>
</Payments_1>
<Payments_2>
<amount>1800</amount>
<currency>USD</currency>
</Payments_2>
</Member>
</Members>
If you want to group the Member child elements by node-name() then I think you need to wrap the xsl:for-each-group into xsl:fork:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="#all" version="3.0">
<xsl:strip-space elements="*"/>
<xsl:output indent="yes"/>
<xsl:mode on-no-match="shallow-copy" streamable="yes" use-accumulators="counters"/>
<xsl:accumulator name="counters" as="map(xs:QName, xs:integer)" initial-value="map{}" streamable="yes">
<xsl:accumulator-rule match="Member" select="map{}"/>
<xsl:accumulator-rule match="Member/*"
select="map:put($value, node-name(), if (map:contains($value, node-name())) then map:get($value, node-name()) + 1 else 1)"/>
</xsl:accumulator>
<xsl:template match="Member">
<xsl:copy>
<xsl:fork>
<xsl:for-each-group select="*" group-by="node-name()">
<xsl:apply-templates select="current-group()"/>
</xsl:for-each-group>
</xsl:fork>
</xsl:copy>
</xsl:template>
<xsl:template match="Member/*">
<xsl:element name="{node-name()}_{accumulator-before('counters')(node-name())}">
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
This approach only shows the grouping, it doesn't try to special case Name elements or some other way to not output an index if there is only one such element.
Firstly, my sympathy. XML that uses names like Payments_1 and Payments_2 is really bad news, someone is going to hate you for generating it like this. But if that's the kind of XML you've been told to produce, I guess it's not your job to question it.
As far as the requirements are concerned, you haven't made it clear whether the various kinds of sibling nodes are always grouped as in your example (all Locations, then all Payments, etc), or whether they can be interleaved.
One way you might be able to reduce the volume of code is by having a single accumulator holding a map. The map would use element names as the key and the current sibling count for that element as the value.
<accumulator name="counters" as="map(xs:QName, xs:integer)" initial-value="map{}">
<xsl:accumulator-rule match="Member" select="map{}"/>
<xsl:accumulator-rule match="Member/*" select="map:put($value, node-name(.), if (map:contains($value, node-name(.)) then map:get($value, node-name(.))+1 else 1"/>
</accumulator>
<xsl:template match="Members/*">
<xsl:element name="{name()}_{accumulator-before('counters')(node-name(.))}">
....
Another way to do the conditional map:put is
map:put($value, node-name(.), ($value(node-name(.)), 0)[1] + 1)
I am trying to sort a list of categorized xml elements, using XSLT 2.0. Each element has a unique ID and the categorization is defined in another list containing these and more elements. Here's an example of a starting XML document. The section that I want sorted is /Atlas/VisitedCities. It should be sorted according to area of the world and date of the visit:
<?xml version="1.0" encoding="UTF-8"?>
<Atlas>
<Cities>
<City id="1" worldPart="Africa">
<Name>Luxor</Name>
<Founded>-3200</Founded>
<Location>Egypt</Location>
</City>
<City id="2" worldPart="Africa">
<Name>Tripoli</Name>
<Founded>-700</Founded>
<Location>Libya</Location>
</City>
<City id="3" worldPart="Americas">
<Name>Cholula</Name>
<Founded>-200</Founded>
<Location>Mexico</Location>
</City>
<City id="4" worldPart="Americas">
<Name>Flores</Name>
<Founded>-1000</Founded>
<Location>Guatemala</Location>
</City>
<City id="5" worldPart="Europe">
<Name>Argos</Name>
<Founded>-5000</Founded>
<Location>Greece</Location>
</City>
<City id="6" worldPart="Europe">
<Name>Athens</Name>
<Founded>-4000</Founded>
<Location>Greece</Location>
</City>
</Cities>
<VisitedCities lastUpdate="2018-09-10">
<VisitedCity cityID="6">
<Date>1883-08-26</Date>
<Visitor>Dora</Visitor>
</VisitedCity>
<VisitedCity cityID="3">
<Date>1907-01-02</Date>
<Visitor>Nemo</Visitor>
</VisitedCity>
<VisitedCity cityID="4">
<Date>1940-02-08</Date>
<Visitor>Jimenez</Visitor>
</VisitedCity>
<VisitedCity cityID="2">
<Date>1886-06-10</Date>
<Visitor>James T. Kirk</Visitor>
</VisitedCity>
</VisitedCities>
</Atlas>
The wanted output is this:
<?xml version="1.0" encoding="UTF-8"?>
<Atlas>
<Cities>
<City id="1" worldPart="Africa">
<Name>Luxor</Name>
<Founded>-3200</Founded>
<Location>Egypt</Location>
</City>
<City id="2" worldPart="Africa">
<Name>Tripoli</Name>
<Founded>-700</Founded>
<Location>Libya</Location>
</City>
<City id="3" worldPart="Americas">
<Name>Cholula</Name>
<Founded>-200</Founded>
<Location>Mexico</Location>
</City>
<City id="4" worldPart="Americas">
<Name>Flores</Name>
<Founded>-1000</Founded>
<Location>Guatemala</Location>
</City>
<City id="5" worldPart="Europe">
<Name>Argos</Name>
<Founded>-5000</Founded>
<Location>Greece</Location>
</City>
<City id="6" worldPart="Europe">
<Name>Athens</Name>
<Founded>-4000</Founded>
<Location>Greece</Location>
</City>
</Cities>
<VisitedCities lastUpdate="2018-09-10">
<VisitedCity cityID="2">
<Date>1886-06-10</Date>
<Visitor>James T. Kirk</Visitor>
</VisitedCity>
<VisitedCity cityID="6">
<Date>1883-08-26</Date>
<Visitor>Dora</Visitor>
</VisitedCity>
<VisitedCity cityID="3">
<Date>1907-01-02</Date>
<Visitor>Nemo</Visitor>
</VisitedCity>
<VisitedCity cityID="4">
<Date>1940-02-08</Date>
<Visitor>Jimenez</Visitor>
</VisitedCity>
</VisitedCities>
</Atlas>
The stylesheet (XSLT 2.0) that I am struggling with looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">
<!-- Format output -->
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*" />
<!-- Copy everything that does not match later templates. -->
<xsl:template match="node()|#*" priority="-1">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:variable name="city.list" select="/Atlas/Cities"/>
<xsl:variable name="sort-order" as="element()*">
<wPart>Africa</wPart>
<wPart>Europe</wPart>
<wPart>Americas</wPart>
</xsl:variable>
<xsl:template match="/Atlas/VisitedCities">
<xsl:variable name="city-list" select="."/>
<xsl:copy>
<xsl:apply-templates select="#*"/>
<xsl:for-each select="$sort-order">
<xsl:variable name="this-wpart" select="./text()"/>
<!-- How to select VisitedCity based on info in other list??? -->
<xsl:apply-templates select="$city-list/VisitedCity[$city.list/City[#cityID=$city-list/VisitedCity/#cityID]/#worldPart=$this-wpart]">
<xsl:sort select="./Date"/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
I think I understand why this stylesheet will not work (it does not sort at all), as I don't know how to make the selection in the (last) apply-templates. I don't see how to refer to the outermost elements from the inner parts of this expression.
It might suffice to set up a key to reference the City elements by the id attribute to then, in the xsl:sort select expression reference the worldPart attribute. Additionally you could replace the for-each on your continent order with an index-of() call with
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output method="xml" indent="yes"/>
<xsl:key name="city-by-id" match="Cities/City" use="#id"/>
<xsl:variable name="sort-order" as="element()*">
<wPart>Africa</wPart>
<wPart>Europe</wPart>
<wPart>Americas</wPart>
</xsl:variable>
<xsl:template match="VisitedCities">
<xsl:copy>
<xsl:apply-templates select="VisitedCity">
<xsl:sort select="index-of($sort-order, key('city-by-id', #cityID)/#worldPart)"/>
<xsl:sort select="Date"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/eiZQaFJ
That complete example is XSLT 3 but to use it with XSLT 2 you would just replace the xsl:mode declaration in there with your template you have prefixed with the comment <!-- Copy everything that does not match later templates. -->, that is, with the identity transformation template.
I'm struggling with a simple recursive count problem based on distinct values. From a list such as:
<List>
<Place continent="Asia" country="India" region="UP">Lucknow</Place>
<Place continent="Europe" country="France" region="Provence">Marseille</Place>
<Place continent="Europe" country="Greece" region="Arcadia">Dimitsana</Place>
<Place continent="Europe" country="Italy" region="Sicily">Sicily</Place>
<Place continent="Asia" country="India" region="Maharastra">Pune</Place>
<Place continent="Asia" country="China">China</Place>
<Place continent="Europe" country="France" region="Provence">Marseille</Place>
<Place continent="America" country="Brasil" region="Pará">Belém</Place>
</List>
I'd like to retrieve an output based on distinct values for the continent, country and region attributes (in some cases optional), that would count recursively each token, as follows:
<Places>
<continent name="America" count="1">
<country name="Brasil" count="1">
<region name="Pará" count="1">
<city name="Belém" count="1"/>
</region>
</country>
</continent>
<continent name="Asia" count="3">
<country name="India" count="2">
<region name="Maharastra" count="1">
<city name="Pune" count="1"/>
</region>
<region name="UP" count="1">
<city name="Lucknow" count="1"/>
</region>
</country>
<country name="China" count="1"/>
</continent>
<continent name="Europe" count="4">
<country name="France" count="2">
<region name="Provence" count="2">
<city name="Marseille" count="2"/>
</region>
</country>
<country name="Greece" count="1">
<region name="Arcadia" count="1">
<city name="Dimitsana" count="1"/>
</region>
</country>
<country name="Italy" count="1">
<region name="Sicily" count="1"/>
</country>
</continent>
I can manage to retrieve each set of distinct values (continents, countries, regions) independently, with, e.g.:
<xsl:template match="List">
<Places>
<xsl:for-each-group select="Place" group-by="#continent">
<continent>
<xsl:attribute name="name">
<xsl:value-of select="#continent"/>
</xsl:attribute>
<xsl:attribute name="count">
<xsl:value-of select="count(current-group())"/>
</xsl:attribute>
</continent>
</xsl:for-each-group>
</Places>
— but, although I suppose I'm missing something very basic, I can't manage to get the nested grouping and count for countries inside continents, etc. Many thanks in advance.
I'm new in xml, sorry for the dumb question. I'm trying to create xsl template to convert Source xml to Destination. Actually it's almost done but I don't know how to enumerate <creator>'s correctly (creator#affil in Destination xml).
Source:
<?xml version="1.0" encoding="iso-8859-1"?>
<CREATORS>
<PERSON ROLE="CONTACT">
<PREFIX>Prof</PREFIX>
<FIRST_NAME>Mike</FIRST_NAME>
<MIDDLE_INITIALS>J</MIDDLE_INITIALS>
<LAST_NAME>Petrov</LAST_NAME>
<SUFFIX/>
<POSITION>Director</POSITION>
<EMAIL_1>pontorez#pontorez.ru</EMAIL_1>
<EMAIL_2>webmaster#pontorez.ru</EMAIL_2>
<URL>www.pontorez.ru</URL>
<MOBILE_PHONE/>
<ADDRESS>
<DEPARTMENT/>
<ORGANISATION>Example</ORGANISATION>
<ADDRESS_1>Finch Pavilion</ADDRESS_1>
<ADDRESS_2>Middle Way</ADDRESS_2>
<CITY>Oxford</CITY>
<ZIP>OX9 7LG</ZIP>
<REGION/>
<COUNTRY CODE="GB">UK</COUNTRY>
<PHONE_1>+380 6245 716300</PHONE_1>
<PHONE_2/>
<FAX_1>+380 6245 716311</FAX_1>
<FAX_2/>
</ADDRESS>
<FOOTNOTE/>
</PERSON>
<PERSON ID="7091" ROLE="AUTHOR">
<PREFIX>Prof</PREFIX>
<FIRST_NAME>Mike</FIRST_NAME>
<MIDDLE_INITIALS>J</MIDDLE_INITIALS>
<LAST_NAME>Petrov</LAST_NAME>
<SUFFIX/>
<POSITION>Director</POSITION>
<EMAIL_1>pontorez#pontorez.ru</EMAIL_1>
<EMAIL_2>webmaster#pontorez.ru</EMAIL_2>
<URL>www.pontorez.ru</URL>
<MOBILE_PHONE/>
<ADDRESS>
<DEPARTMENT/>
<ORGANISATION>Example</ORGANISATION>
<ADDRESS_1>Finch Pavilion</ADDRESS_1>
<ADDRESS_2>Middle Way</ADDRESS_2>
<CITY>Oxford</CITY>
<ZIP>OX9 7LG</ZIP>
<REGION/>
<COUNTRY CODE="GB">UK</COUNTRY>
<PHONE_1>+380 6245 716300</PHONE_1>
<PHONE_2/>
<FAX_1>+380 6245 716311</FAX_1>
<FAX_2/>
</ADDRESS>
<FOOTNOTE>Author footnote</FOOTNOTE>
</PERSON>
<PERSON ID="7094" ROLE="AUTHOR">
<PREFIX>Mrs</PREFIX>
<FIRST_NAME>Anne</FIRST_NAME>
<MIDDLE_INITIALS/>
<LAST_NAME>Spencer</LAST_NAME>
<SUFFIX/>
<POSITION/>
<EMAIL_1>aspencer#Hardware.co.uk</EMAIL_1>
<EMAIL_2/>
<URL/>
<MOBILE_PHONE/>
<ADDRESS>
<DEPARTMENT/>
<ORGANISATION>Example</ORGANISATION>
<ADDRESS_1>SEO R&D Programme</ADDRESS_1>
<ADDRESS_2>Finch Pavilion, Middle Way</ADDRESS_2>
<CITY>Oxford</CITY>
<ZIP>OX9 7LG</ZIP>
<REGION>Oxfordshire</REGION>
<COUNTRY CODE="GB">UK</COUNTRY>
<PHONE_1>+380 6245 716300</PHONE_1>
<PHONE_2/>
<FAX_1>+380 6245 716311</FAX_1>
<FAX_2/>
</ADDRESS>
<FOOTNOTE/>
</PERSON>
<PERSON ID="15756" ROLE="AUTHOR">
<PREFIX>Mr</PREFIX>
<FIRST_NAME>Ed</FIRST_NAME>
<MIDDLE_INITIALS/>
<LAST_NAME>Gantos</LAST_NAME>
<SUFFIX/>
<POSITION>Senior Medical Statistician</POSITION>
<EMAIL_1>santos#xxx.org.uk</EMAIL_1>
<EMAIL_2/>
<URL>http://www.isds.sxo.ac.uk/</URL>
<MOBILE_PHONE/>
<ADDRESS>
<DEPARTMENT>Head of SEO Support</DEPARTMENT>
<ORGANISATION>Centre for Statistics in Software</ORGANISATION>
<ADDRESS_1>Pearson College</ADDRESS_1>
<ADDRESS_2>Linton Road</ADDRESS_2>
<CITY>Oxford</CITY>
<ZIP>OX9 6UD</ZIP>
<REGION/>
<COUNTRY CODE="GB">UK</COUNTRY>
<PHONE_1>+380 6245 112404</PHONE_1>
<PHONE_2/>
<FAX_1>+380 6245 112424</FAX_1>
<FAX_2/>
</ADDRESS>
<FOOTNOTE/>
</PERSON>
<PERSON ID="7092" ROLE="AUTHOR">
<PREFIX>Dr</PREFIX>
<FIRST_NAME>Sherry</FIRST_NAME>
<MIDDLE_INITIALS/>
<LAST_NAME>Wilson</LAST_NAME>
<SUFFIX/>
<POSITION/>
<EMAIL_1>wilson#Hardware.co.uk</EMAIL_1>
<EMAIL_2/>
<URL/>
<MOBILE_PHONE/>
<ADDRESS>
<DEPARTMENT/>
<ORGANISATION>Example</ORGANISATION>
<ADDRESS_1>SEO R&D Programme</ADDRESS_1>
<ADDRESS_2>Finch Pavilion, Middle Way</ADDRESS_2>
<CITY>Oxford</CITY>
<ZIP>OX9 7LG</ZIP>
<REGION>Oxfordshire</REGION>
<COUNTRY CODE="GB">UK</COUNTRY>
<PHONE_1>+380 6245 716300</PHONE_1>
<PHONE_2/>
<FAX_1>+380 6245 716311</FAX_1>
<FAX_2/>
</ADDRESS>
<FOOTNOTE/>
</PERSON>
<GROUP ID="????">
<GROUP_NAME>Bond team</GROUP_NAME>
<CONTACT_PERSON>Monica Bond</CONTACT_PERSON>
<EMAIL_1>mk#Hardware.dk</EMAIL_1>
<URL/>
<ADDRESS>
<DEPARTMENT>Nordic Hardware Centre</DEPARTMENT>
<ORGANISATION>Creyts, Dept 7512</ORGANISATION>
<ADDRESS_1>Blegdamsvej 219</ADDRESS_1>
<ADDRESS_2/>
<CITY>Copenhagen</CITY>
<ZIP>2900</ZIP>
<REGION/>
<COUNTRY CODE="DK">Denmark</COUNTRY>
<PHONE_1>+415 7667 7110</PHONE_1>
<PHONE_2>+415 1234 9429</PHONE_2>
<FAX_1>+415 7667 7007</FAX_1>
<FAX_2/>
</ADDRESS>
<FOOTNOTE>Group footnote</FOOTNOTE>
</GROUP>
<PERSON ID="3" ROLE="AUTHOR">
<PREFIX>Ms</PREFIX>
<FIRST_NAME>Monica</FIRST_NAME>
<MIDDLE_INITIALS/>
<LAST_NAME>Bond</LAST_NAME>
<SUFFIX/>
<POSITION>Director of the Hardware Information Management System</POSITION>
<EMAIL_1>mk#Hardware.dk</EMAIL_1>
<EMAIL_2/>
<URL>www.cc-ims.net</URL>
<MOBILE_PHONE/>
<ADDRESS>
<DEPARTMENT>Nordic Hardware Centre</DEPARTMENT>
<ORGANISATION>Creyts, Dept 7512</ORGANISATION>
<ADDRESS_1>Blegdamsvej 219</ADDRESS_1>
<ADDRESS_2/>
<CITY>Copenhagen</CITY>
<ZIP>2900</ZIP>
<REGION/>
<COUNTRY CODE="DK">Denmark</COUNTRY>
<PHONE_1>+415 7667 7110</PHONE_1>
<PHONE_2>+415 1234 9429</PHONE_2>
<FAX_1>+415 7667 7007</FAX_1>
<FAX_2/>
</ADDRESS>
<FOOTNOTE/>
</PERSON>
</CREATORS>
Destination:
<?xml version="1.0" encoding="utf-8"?>
<creatorGroup>
<creator affil="CD004002-aff-0001" creatorRole="author">
<forenames>Mike</forenames>
<surnamePrefix>J</surnamePrefix>
<surname>Petrov</surname>
<note id="CD004002-note-0001">
<p>Author footnote</p>
</note>
</creator>
<creator affil="CD004002-aff-0001" creatorRole="author">
<forenames>Anne</forenames>
<surname>Spencer</surname>
</creator>
<creator affil="CD004002-aff-0002" creatorRole="author">
<forenames>Ed</forenames>
<surname>Gantos</surname>
</creator>
<creator affil="CD004002-aff-0001" creatorRole="author">
<forenames>Sherry</forenames>
<surname>Wilson</surname>
</creator>
<creator affil="CD004002-aff-0003" type="collaboration" creatorRole="author">
<collab>Bond team</collab>
</creator>
<creator affil="CD004002-aff-0003" creatorRole="author">
<forenames>Monica</forenames>
<surname>Bond</surname>
</creator>
<creator affil="CD004002-aff-0001" creatorRole="contact">
<honorifics>Prof</honorifics>
<forenames>Mike</forenames>
<surnamePrefix>J</surnamePrefix>
<surname>Petrov</surname>
<jobTitle>Director</jobTitle>
<email>pontorez#pontorez.ru</email>
<email>webmaster#pontorez.ru</email>
</creator>
<affiliation id="CD004002-aff-0001" countryCode="GB">
<orgName>Example</orgName>
<address>
<street>Finch Pavilion</street>
<street>Middle Way</street>
<city>Oxford</city>
<country>UK</country>
<postCode>OX9 7LG</postCode>
</address>
</affiliation>
<affiliation id="CD004002-aff-0002" countryCode="GB">
<orgName>Centre for Statistics in Software</orgName>
<orgDiv>Head of SEO Support</orgDiv>
<address>
<street>Pearson College</street>
<street>Linton Road</street>
<city>Oxford</city>
<country>UK</country>
<postCode>OX9 6UD</postCode>
</address>
</affiliation>
<affiliation id="CD004002-aff-0003" countryCode="DK">
<collabContact>Monica Bond</collabContact>
<orgName>Creyts, Dept 7512</orgName>
<orgDiv>Nordic Hardware Centre</orgDiv>
<address>
<street>Blegdamsvej 219</street>
<city>Copenhagen</city>
<country>Denmark</country>
<postCode>2900</postCode>
</address>
</affiliation>
</creatorGroup>
XSL stylesheet that I created:
<?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:key name="affiliations" match="ORGANISATION" use="."/>
<xsl:template name="kreator">
<xsl:variable name="affid">
<!-- The numbering problem is in the below line: -->
<xsl:number format="0001" value="position()"/>
</xsl:variable>
<xsl:variable name="role">
<xsl:choose>
<xsl:when test="#ROLE='AUTHOR' or name()='GROUP'">author</xsl:when>
<xsl:otherwise>contact</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<creator affil="CD004002-aff-{$affid}">
<xsl:if test="GROUP_NAME!=''">
<xsl:attribute name="type">collaboration</xsl:attribute>
</xsl:if>
<xsl:attribute name="creatorRole">
<xsl:value-of select="$role"/>
</xsl:attribute>
<xsl:if test="$role='contact'">
<honorifics>
<xsl:value-of select="PREFIX"/>
</honorifics>
</xsl:if>
<xsl:if test="GROUP_NAME!=''">
<collab>
<xsl:value-of select="GROUP_NAME"/>
</collab>
</xsl:if>
<xsl:if test="FIRST_NAME">
<forenames>
<xsl:value-of select="FIRST_NAME"/>
</forenames>
</xsl:if>
<xsl:if test="MIDDLE_INITIALS!=''">
<surnamePrefix>
<xsl:value-of select="MIDDLE_INITIALS"/>
</surnamePrefix>
</xsl:if>
<xsl:if test="LAST_NAME">
<surname>
<xsl:value-of select="LAST_NAME"/>
</surname>
</xsl:if>
<xsl:if test="FOOTNOTE!='' and name()='PERSON'">
<note id="CD004002-note-{$affid}">
<p>
<xsl:value-of select="FOOTNOTE"/>
</p>
</note>
</xsl:if>
<xsl:if test="$role='contact'">
<jobTitle><xsl:value-of select="POSITION"/></jobTitle>
<email><xsl:value-of select="EMAIL_1"/></email>
<email><xsl:value-of select="EMAIL_2"/></email>
</xsl:if>
</creator>
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template match="/CREATORS">
<xmp>
<creatorGroup>
<!-- list of creators: -->
<xsl:for-each select="PERSON[#ROLE='AUTHOR'] | GROUP">
<xsl:call-template name="kreator"/>
</xsl:for-each>
<xsl:for-each select="PERSON[#ROLE='CONTACT']">
<xsl:call-template name="kreator"/>
</xsl:for-each>
<!-- list of affiliations: -->
<xsl:for-each select="//ORGANISATION[generate-id(.)=generate-id(key('affiliations', .))]">
<affiliation>
<xsl:attribute name="id">
CD004002-aff-000<xsl:value-of select="position()"/>
</xsl:attribute>
<xsl:attribute name="countryCode">
<xsl:value-of select="../COUNTRY/#CODE"/>
</xsl:attribute>
<xsl:if test="../../CONTACT_PERSON">
<collabContact>
<xsl:value-of select="../../CONTACT_PERSON"/>
</collabContact>
</xsl:if>
<orgName>
<xsl:value-of select="."/>
</orgName>
<xsl:if test="../DEPARTMENT!=''">
<orgDiv>
<xsl:value-of select="../DEPARTMENT"/>
</orgDiv>
</xsl:if>
<address>
<street>
<xsl:value-of select="../ADDRESS_1"/>
</street>
<xsl:if test="../ADDRESS_2!=''">
<street>
<xsl:value-of select="../ADDRESS_2"/>
</street>
</xsl:if>
<city>
<xsl:value-of select="../CITY"/>
</city>
<country>
<xsl:value-of select="../COUNTRY"/>
</country>
<postCode>
<xsl:value-of select="../ZIP"/>
</postCode>
</address>
</affiliation>
<xsl:text>
</xsl:text>
</xsl:for-each>
</creatorGroup>
</xmp>
</xsl:template>
</xsl:stylesheet>
I.e. I want to get <creator> numbering like the below:
<creator affil="CD004002-aff-0001" creatorRole="author"/>
<creator affil="CD004002-aff-0001" creatorRole="author"/>
<creator affil="CD004002-aff-0002" creatorRole="author"/>
<creator affil="CD004002-aff-0001" creatorRole="author"/>
<creator affil="CD004002-aff-0003" type="collaboration" creatorRole="author"/>
<creator affil="CD004002-aff-0003" creatorRole="author"/>
<creator affil="CD004002-aff-0001" creatorRole="contact"/>
While my current XSL stylesheet produces wrong #affil's:
<creator affil="CD004002-aff-0004" creatorRole="author"/>
<creator affil="CD004002-aff-0004" creatorRole="author"/>
<creator affil="CD004002-aff-0001" creatorRole="author"/>
<creator affil="CD004002-aff-0004" creatorRole="author"/>
<creator affil="CD004002-aff-0002" type="collaboration" creatorRole="author"/>
<creator affil="CD004002-aff-0002" creatorRole="author"/>
<creator affil="CD004002-aff-0004" creatorRole="contact"/>
Can you help please? Thanks.
Your problem is that when you do:
<xsl:call-template name="kreator" />
then you do it in the context of the <PERSON> or <GROUP> you are currently handling.
This means that position() will tell you the position of those elements. It cannot magically know that you are interested in the position of the first <ORGANISATION> that matches the current one.
And this means you must iterate all <ORGANISATION>s much the same way you do it to calculate the <affiliation> attribute #id:
<xsl:variable name="affid">
<xsl:variable name="org" select="ADDRESS/ORGANISATION" />
<xsl:for-each select="//ORGANISATION[
generate-id()
=
generate-id(key('affiliations', .)[1])
]">
<xsl:if test=". = $org">
<xsl:number format="0001" value="position()"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
Note the use of <xsl:if> to make sure that the variable eventually contains only one value, even though you are looking through all of them.
For larger documents, you could introduce another key:
<xsl:key name="organisations" match="ORGANISATION" use="'all'" />
and use that as a drop-in replacement for all the comparatively inefficient "//ORGANISATION" expressions, e.g. instead of:
<xsl:for-each select="//ORGANISATION">
<!-- ... -->
</xsl:for-each>
use:
<xsl:for-each select="key('organisations', 'all')">
<!-- ... -->
</xsl:for-each>