XSLT 2.0 - select following text() based on position - xslt-2.0

I'm stuck on trying to determine the correct predicate to access the text() following /document/meta/aff/sup/italic based on the value of /document/meta/contrib-group/contrib/ref/sup/italic.
Honestly I'm not even sure if I'm phrasing the question correctly. I know that there are text nodes in /document/meta/aff/, but I'm not sure how to get to them in the correct sequence. In the example XSLT below I've started to try using position() to determine the correct text, but I think I need additional predicates.
Thanks in advance for your time & trouble!
trailing-text-xml:
<?xml version="1.0" encoding="UTF-8"?>
<document>
<meta>
<contrib-group>
<contrib type="author">
<name>
<surname>Smith</surname>
<given-name>Alan</given-name>
</name>
<ref type="aff">
<sup>
<italic>a</italic>
</sup>
</ref>
</contrib>
<contrib type="author">
<name>
<surname>Jones</surname>
<given-name>Beatrice</given-name>
</name>
<ref type="aff">
<sup>
<italic>b</italic>
</sup>
</ref>
</contrib>
<contrib type="author">
<name>
<surname>Richardson</surname>
<given-name>Clarence</given-name>
</name>
<ref type="aff">
<sup>
<italic>c</italic>
</sup>
</ref>
</contrib>
</contrib-group>
<aff>
<sup>
<italic>a</italic>
</sup>An Institutional Name
<sup>
<italic>b</italic>
</sup>An Institutional Name
<sup>
<italic>c</italic>
</sup>An Institutional Name
</aff>
</meta>
</document>
trailing-text-xslt:
<?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="2.0">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:apply-templates select="document/meta/contrib-group/contrib[#type='author']"/>
</xsl:template>
<xsl:template match="document/meta/contrib-group/contrib[#type='author']">
<xsl:variable name="vPosition"
select="position()"/>
<xsl:text>Name: </xsl:text>
<xsl:value-of select="concat(name/given-name, ' ', name/surname)"/>
<xsl:text> Affiliation: </xsl:text>
<xsl:choose>
<xsl:when test="ref/sup/italic = /document/meta/aff/sup/italic">
<xsl:value-of select="concat(
$vPosition,
' ',
ref/sup/italic)"/>
</xsl:when>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>

I think you want to define a key and cross-reference:
<?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="2.0">
<xsl:output method="text"/>
<xsl:key name="aff" match="/document/meta/aff/sup[italic]" use="italic"/>
<xsl:template match="/">
<xsl:apply-templates select="document/meta/contrib-group/contrib[#type='author']"/>
</xsl:template>
<xsl:template match="document/meta/contrib-group/contrib[#type='author']">
<xsl:variable name="vPosition"
select="position()"/>
<xsl:text>Name: </xsl:text>
<xsl:value-of select="concat(name/given-name, ' ', name/surname)"/>
<xsl:text> Affiliation: </xsl:text>
<xsl:choose>
<xsl:when test="key('aff', ref/sup/italic)">
<xsl:value-of select="concat(
$vPosition,
' ',
key('aff', ref/sup/italic)/following-sibling::text()[1])"/>
</xsl:when>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
With your input I get the result
Name: Alan Smith Affiliation: 1 An Institutional Name
Name: Beatrice Jones Affiliation: 2 An Institutional Name
Name: Clarence Richardson Affiliation: 3 An Institutional Name
Obviously the xsl:choose is a bit convoluted but you had that in there so I left it as posted and only changed the code to show how to cross-reference based on italic.

Related

How to access previous and next item based on a condition

I have 2000 TEI-XML-files with letters between different people and a single person in a single folder. I can access the previous and next letter chronologically as the filename starts with the date of the sender (e.g. 2001-02-21.xml). But what I want to achieve is to create an XSLT (2 or 3, doesn't matter) that writes the next letter to or from the specific writer/receiver into the xml-file.
Say I have this:
<correspDesc>
<correspAction type="sent">
<persName key="CMvW">Carl Maria von Weber</persName>
<settlement>Dresden</settlement>
<date when="1817-06-23">23 June 1817</date>
</correspAction>
<correspAction type="received">
<persName key="CB">Caroline Brandt</persName>
<settlement>Prag</settlement>
</correspAction>
</correspDesc>
and i want to add this field as a third child after correspAction:
<correspContext>
<ref type="prev"
target="http://www.weber-gesamtausgabe.de/A041209">Previous letter of
<persName key="CMvW">Carl Maria von Weber</persName>
to <persName key="CB">Caroline Brandt</persName>:
<date from="1817-06-19" to="1817-06-20">June 19/20, 1817</date>
</ref>
<ref type="next"
target="http://www.weber-gesamtausgabe.de/A041217">Next letter of
<persName key="CMvW">Carl Maria von Weber</persName> to
<persName key="CB">Caroline Brandt</persName>:
<date when="1817-06-27">June 27, 1817</date>
</ref>
</correspContext>
how would I do it? In the example Caroline Brandt is the changing sender/receiver. So basically I would need a collection of all XML-files with //correspDesc//persName[#key='CB'] and in each file access the preceding and following one of the collection. How can I achieve that?
begin of the solution
#martin-honnen pointed me the way though I'm certain it is not the most elegant way.
1) I've used collection to copy all correspDesc into one file. I add an attribute 'lookup' with the key of the person and the date the letter was sent combined and an attribute with just 'person' to identify the letters of a correspondence.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output indent="yes" method="xml" encoding="utf-8" omit-xml-declaration="false"/>
<xsl:template match="/">
<xsl:element name="root">
<xsl:for-each select="collection('?select=*.xml;recurse=no')">
<xsl:element name="correspDesc">
<xsl:attribute name="lookup">
<xsl:choose>
<xsl:when test="//correspAction[#type='sent']/persName/#key='pmb2121'">
<xsl:value-of select="//correspAction[#type='received']/persName/#key"/>
<xsl:text>|</xsl:text>
<xsl:value-of select="//correspAction[#type='sent']/date/#when"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="//correspAction[#type='sent']/persName/#key"/>
<xsl:text>|</xsl:text>
<xsl:value-of select="//correspAction[#type='sent']/date/#when"/>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:attribute name="person">
<xsl:choose>
<xsl:when test="//correspAction[#type='sent']/persName/#key='pmb2121'">
<xsl:value-of select="//correspAction[#type='received']/persName/#key"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="//correspAction[#type='sent']/persName/#key"/>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:apply-templates select="//correspAction"/>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
2) I order the resulting list using the date-field
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<xsl:mode on-no-match="shallow-copy" />
<xsl:output indent="yes"
method="xml"
encoding="utf-8"
omit-xml-declaration="false"/>
<xsl:template match="root">
<xsl:element name="root">
<xsl:apply-templates select="correspDesc">
<xsl:sort select="correspAction[#type='sent']/date/#when" data-type="text" order="ascending"/>
</xsl:apply-templates>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
Now my next task is to use param and key to lookup the preceding and following entries.
<xsl:param name="correspList" select="document('correspList.xml')"/>
<xsl:key name="corresp-lookup" match="#lookup"/>
<xsl:key name="correspPerson-lookup" match="#person"/>
I am not sure how I will achieve that but I will update here once I have that code.

Identify values that dont match in all Nodes and Attributes: XSLT2.0

I need to go over all the xml attributes and text nodes to identify existence of character from list and output the values the characters values that didnt match.
I am able to check the text() nodes but I am not able to perform a check on attributes.
<xsl:template match="#*|node()">
<xsl:variable name="getDelimitersToUseNodes" select="('$' ,'#' ,'*' ,'~')[not(contains(current(),.))]"/>
<xsl:variable name="getDelimitersToUseAttr" select="string-join(('$','#','*','~')[not(contains(#*/,.))],',')"/>
<xsl:variable name="getDelimitersToUse" select="concat(string-join($getDelimitersToUseNodes,','),',',string-join($getDelimitersToUseAttr,','))"/>
<!--xsl:variable name="delim" select="distinct-values($getDelimitersToUse,',')"/-->
<xsl:value-of select="$getDelimitersToUse"/>
</xsl:template>
My mocked up sample file is below
<?xml version="1.0"?>
<sample>
<test1 name="#theGoofy">My$#test</test1>
<test2 value="$##">description test2*</test2>
</sample>
You could process all those text and attribute nodes and make that same check as before. You haven't really said which output format you want, assuming text you could use
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:param name="characters" as="xs:string*" select="'$' ,'#' ,'*' ,'~'"/>
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<xsl:apply-templates select="//text() | //#*"/>
</xsl:template>
<xsl:template match="text() | #*">
<xsl:value-of select="'Text', ., 'does not contain', $characters[not(contains(current(), .))], '
'"/>
</xsl:template>
</xsl:stylesheet>
to get a result like
Text #theGoofy does not contain $ * ~
Text My$#test does not contain * ~
Text $## does not contain * ~
Text description test2* does not contain $ # ~
If you simply want to check all characters not contained in all text nodes and attribute nodes then an approach like
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:param name="characters" as="xs:string*" select="'$' ,'#' ,'*' ,'~'"/>
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="nodes-to-inspect" as="node()*" select="//text() | //#*"/>
<xsl:template match="/">
<xsl:value-of select="for $c in $characters return $c[not($nodes-to-inspect[contains(., $c)])]"/>
</xsl:template>
</xsl:stylesheet>
should do.

How to display latest date from N number of month in xslt?

I need to display latest date in N number of months using xslt.
My input:
2016/10/18
2016//10/15
2016/09/29
2016/09/15
and so on.
My output should be like below:
2016/10/18
2016/09/29
Can anyone help me on this?
Given a string of dates in that format you first need to tokenize to extract the date values, then you need to convert to the xs:date format, then you can group by the month and select the maximum value in each group. Using XSLT 3.0 that can be done as follows:
<?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"
xmlns:math="http://www.w3.org/2005/xpath-functions/math"
exclude-result-prefixes="xs math"
version="3.0">
<xsl:param name="input" as="xs:string">2016/10/18 2016/10/15 2016/09/29 2016/09/15</xsl:param>
<xsl:variable name="dates" as="xs:date*"
select="tokenize($input, '\s+')!xs:date(replace(., '/', '-'))"/>
<xsl:variable name="max-dates" as="xs:date*">
<xsl:for-each-group select="$dates" group-by="month-from-date(.)">
<xsl:sort select="current-grouping-key()"/>
<xsl:sequence select="max(current-group())"/>
</xsl:for-each-group>
</xsl:variable>
<xsl:template name="main" match="/">
<xsl:value-of select="$max-dates" separator="
"/>
</xsl:template>
</xsl:stylesheet>
In XSLT 2.0 you need to rewrite the date sequence construction a bit:
<?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"
xmlns:math="http://www.w3.org/2005/xpath-functions/math"
exclude-result-prefixes="xs math"
version="2.0">
<xsl:param name="input" as="xs:string">2016/10/18 2016/10/15 2016/09/29 2016/09/15</xsl:param>
<xsl:variable name="dates" as="xs:date*"
select="for $dateString in tokenize($input, '\s+') return xs:date(replace($dateString, '/', '-'))"/>
<xsl:variable name="max-dates" as="xs:date*">
<xsl:for-each-group select="$dates" group-by="month-from-date(.)">
<xsl:sort select="current-grouping-key()"/>
<xsl:sequence select="max(current-group())"/>
</xsl:for-each-group>
</xsl:variable>
<xsl:template name="main" match="/">
<xsl:value-of select="$max-dates" separator="
"/>
</xsl:template>
</xsl:stylesheet>
I. Here is a short XSLT 2.0 solution:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/*">
<xsl:for-each-group select="d" group-by="substring(.,6,2)">
<xsl:sequence select="current-group()[. eq max(current-group()/string())][1]"/>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following XML document (unordered and multi-year dates -- to make it more interesting):
<t>
<d>2016/10/15</d>
<d>2016/09/15</d>
<d>2016/10/18</d>
<d>2016/09/29</d>
<d>2017/09/17</d>
</t>
the wanted, correct result is produced:
<d>2016/10/18</d>
<d>2017/09/17</d>
II. If the date that has the same month's highest day is wanted -- regardless of the year, this transformation:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/*">
<xsl:for-each-group select="d" group-by="substring(.,6,2)">
<xsl:sequence select=
"current-group()[substring(.,9,2) eq max(current-group()/substring(.,9,2))][1]"/>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the same XML document (above), the correct result is produced:
<d>2016/10/18</d>
<d>2016/09/29</d>
III. If the dates are given together as a string:
Just use the tokenize() standard XPath 2.0 fy=unction.
For example, the equivalent of the first transformation above becomes:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="vDates"
select="'2016/10/15 2016/09/15 2016/10/18 2016/09/29 2017/09/17'"/>
<xsl:template match="/">
<xsl:for-each-group select="tokenize($vDates, '\s+')[.]" group-by="substring(.,6,2)">
<xsl:sequence select="max(current-group())"/>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>

XSLT mapping and summing source children into a single target attribute

I have a source document with XML structure similar to this:
<FOO>
<BAR>x</BAR>
<BAR>y</BAR>
<BAR>z</BAR>
</FOO>
My target XML must have an attribute with a number that represents the numerical sum of x, y and z. Where x = 1, y = 2 and z = 3
NOTE: The x, y and z are not actually numbers in the source document. They are letters and need to be mapped to the numbers that they represent first.
In this case, the target should look something like:
<Target Sum=6>
</Target>
Anyone have an XSLT example that would do what I need?
Thanks in advance
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<Target Sum="{sum(FOO/BAR)}" />
</xsl:template>
EDIT:
This is a bit verbose, and there is probably a more elegant way to do it, but essentially I have a named template here that recursively calls itself to calculate the sum after the hard-coded mapping occurs:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<Target>
<xsl:attribute name="Sum">
<xsl:call-template name="GetSum">
<xsl:with-param name="CurrentNode" select="FOO/BAR[position()=1]"/>
</xsl:call-template>
</xsl:attribute>
</Target>
</xsl:template>
<xsl:template name="GetSum">
<xsl:param name="CurrentNode"/>
<xsl:param name="Number" select="0"/>
<xsl:variable name="Recursive_Result">
<xsl:variable name="MappedNumber">
<xsl:choose>
<xsl:when test="$CurrentNode/. = 'x'">1</xsl:when>
<xsl:when test="$CurrentNode/. = 'y'">2</xsl:when>
<xsl:when test="$CurrentNode/. = 'z'">3</xsl:when>
<xsl:otherwise>0</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<!-- if there is a next sibling, recursively call GetSum -->
<xsl:choose>
<xsl:when test="$CurrentNode/following-sibling::BAR[1]">
<xsl:call-template name="GetSum">
<xsl:with-param name="CurrentNode" select="$CurrentNode/following-sibling::BAR[1]"/>
<xsl:with-param name="Number">
<xsl:value-of select="$MappedNumber"/>
</xsl:with-param>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$MappedNumber"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<!-- add the recursive_result to the number passed into the template. this will eventually build a sum -->
<xsl:value-of select="$Recursive_Result + $Number"/>
</xsl:template>
</xsl:stylesheet>
A much simpler, shorter and efficient solution:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kMap" match="#value" use="../#key"/>
<xsl:variable name="vMaps" as="element()*">
<map key="x" value="1"/>
<map key="y" value="2"/>
<map key="z" value="3"/>
</xsl:variable>
<xsl:template match="/*">
<Target Sum="{sum(key('kMap', BAR, document('')))}"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<FOO>
<BAR>x</BAR>
<BAR>y</BAR>
<BAR>z</BAR>
</FOO>
the wanted, correct result is produced:
<Target Sum="6"/>
Explanation: Appropriate use of xsl:key, the 3rd argument of the key() function and AVT.

Complex XSL Transformation

I am still a beginner with XSLT but I am having a difficult task in hand.
I have a non-xml file which needs to be transformed. The format of the file is a s follows:
type1
type1line1
type1line2
type1line3
type2
type2line1
type2line2
type3
type3line1
type3line2
types (type1, type2, ...) are specified using certain codes which don't have a specific order. Each type has multiple line underneath.
So, I need to transform this file but the problem is that for each type I have to do a different transformation for each of it's underlying lines.
Now, I can read the string line by line and determine that a new type has begun but I don't know how to set a flag (indicating the type) to use it in the underlying lines.
Here is what I have right now:
<?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" version="2.0">
<xsl:param name="testString" as="xs:string">
type1
line1
line2
type1
line1
</xsl:param>
<xsl:template match="/">
<xsl:call-template name="main">
<xsl:with-param name="testString" select="$testString"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="main">
<xsl:param name="testString"/>
<xsl:variable name="iniFile" select="$testString"/>
<config>
<xsl:analyze-string select="$iniFile" regex="\n">
<xsl:non-matching-substring>
<item>
<xsl:choose>
<xsl:when test="starts-with(., 'type1')">
<!-- do a specific transformation-->
</xsl:when>
<xsl:when test="starts-with(., 'type2')">
<!-- do another transformation-->
</xsl:when>
</xsl:choose>
</item>
</xsl:non-matching-substring>
</xsl:analyze-string>
</config>
</xsl:template>
</xsl:stylesheet>
Any idea about how to solve the problem.
I think XSLT 2.1 will allow you to use its powerful stuff like for-each-group on sequences of atomic values like strings but with XSLT 2.0 you have such powerful features only for sequences of nodes so my first step when using XSLT 2.0 with plain string data I want to process/group is to create elements. So you could tokenize your data, wrap each token into some element and then use for-each-group group-starting-with to process each group starting with some pattern like '^type[0-9]+$'.
You haven't really told us what you want to with the data once you have identified a group so take the following as an example you could adapt:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs">
<xsl:output method="xml" indent="yes"/>
<xsl:param name="input" as="xs:string">type1
type1line1
type1line2
type1line3
type2
type2line1
type2line2
type3
type3line1
type3line2</xsl:param>
<xsl:template name="main">
<xsl:variable name="lines" as="element(item)*">
<xsl:for-each select="tokenize($input, '\n')">
<item><xsl:value-of select="."/></item>
</xsl:for-each>
</xsl:variable>
<xsl:for-each-group select="$lines" group-starting-with="item[matches(., '^type[0-9]+$')]">
<xsl:choose>
<xsl:when test=". = 'type1'">
<xsl:apply-templates select="current-group() except ." mode="m1"/>
</xsl:when>
<xsl:when test=". = 'type2'">
<xsl:apply-templates select="current-group() except ." mode="m2"/>
</xsl:when>
<xsl:when test=". = 'type3'">
<xsl:apply-templates select="current-group() except ." mode="m3"/>
</xsl:when>
</xsl:choose>
</xsl:for-each-group>
</xsl:template>
<xsl:template match="item" mode="m1">
<foo>
<xsl:value-of select="."/>
</foo>
</xsl:template>
<xsl:template match="item" mode="m2">
<bar>
<xsl:value-of select="."/>
</bar>
</xsl:template>
<xsl:template match="item" mode="m3">
<baz>
<xsl:value-of select="."/>
</baz>
</xsl:template>
</xsl:stylesheet>
When applied with Saxon 9 (command line options -it:main -xsl:sheet.xsl) the result is
<foo>type1line1</foo>
<foo>type1line2</foo>
<foo>type1line3</foo>
<bar>type2line1</bar>
<bar>type2line2</bar>
<baz>type3line1</baz>
<baz>type3line2</baz>

Resources