XML Transform with XSLT - xslt-2.0

I have a XML file like below:
<NOP>A focus on individual exhibitors is essential, though, in order to
understand how these figures influenced American (and global) culture and
how audiences were attracted to movies and movie theaters. Charles Musser
writes in his examination of Lyman Howe’s career that “a focus on
exhibition lends itself to industrial history precisely because it must
address the economic basis of the motion picture industry—the showman’s
ability to bring patrons through the front door.â€<ENREF>1</ENREF> In order
to understand the artistic and managerial influences of showmen like Samuel
Lionel Rothafel (“Roxyâ€) and Sidney Patrick Grauman, one must analyze
their construction of stardom, their ethnic heritage and cultural background,
their facility with music, theater, film, and other performing arts, and the
ways in which they motivated patrons to enter their “front door.â€
</NOP>
and I'm using the below XSLT with XML spy but it's not working for 'ENREF'. Any help
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes"/>
<xsl:template match="NOP">
<div class="no-indent"><span><xsl:value-of select="."/></span></div>
</xsl:template>
<xsl:template match="ENREF">
<small>
<sup>
<xsl:element name="a">
<xsl:attribute name="id">enref-<xsl:value-of select="."/></xsl:attribute>
<xsl:attribute name="href">#fn<xsl:value-of select="."/></xsl:attribute>
<xsl:value-of select="."/>
</xsl:element>
</sup>
</small>
</xsl:template>
</xsl:stylesheet>

It does not suffice to write a template matching a certain element, you also need to ensure the element is processed. You have not shown us the result you want so I have to guess what you want to achieve but for a start try changing
<xsl:template match="NOP">
<div class="no-indent"><span><xsl:value-of select="."/></span></div>
</xsl:template>
to
<xsl:template match="NOP">
<div class="no-indent"><span><xsl:apply-templates/></span></div>
</xsl:template>
That way the child nodes of the NOP element are processed, either by the built-in templates (which ensure with <xsl:apply-templates/> that the processing is kept up for grandchildren and further descendants) or by your templates (like the one for the ENREF element).

Related

xslt2: sequence of attribute nodes

This is not really a question but an astonishing xslt2 experience that I like to share.
Take the snippet (subtract one set from another)
<xsl:variable name="v" as="node()*">
<e a="a"/>
<e a="b"/>
<e a="c"/>
<e a="d"/>
</xsl:variable>
<xsl:message select="$v/#a[not(.=('b','c'))]"/>
<ee>
<xsl:sequence select="$v/#a[not(.=('b','c'))]"/>
</ee>
What should I expect to get?
I expected a d at the console and
<ee>a d</ee>
at the output.
What I got is
<?attribute name="a" value="a"?><?attribute name="a" value="d"?>
at the console and
<ee a="d"/>
at the output. I should have known to take $v/#a as a sequence of attribute nodes to predict the output.
In order to get what I wanted, I had to convert the sequence of attributes to a sequence of strings like:
<xsl:variable name="w" select="$v/#a[not(.=('b','c'))]" as="xs:string*"/>
Questions:
Is there any use of sequences of attributes (or is it just an interesting effect of the node set concept)?
If so, would I be able to enter statically a sequence of attributes like I am able to enter a sequence of strings: ('a','b','c','d')
Is there any inline syntax to convert a sequence of attributes to a sequence of strings? (In order to achieve the same result omitting the variable w)
It seems to be an elegant way for creating attributes using xsl:sequence. Or would that be a misuse of xslt2, not covered by the standard?
As for "Is there any inline syntax to convert a sequence of attributes to a sequence of strings", you can simply add a step $v/#a[not(.=('b','c'))]/string(). Or use a for $a in $v/#a[not(.=('b','c'))] return string($a) and of course in XPath 3 $v/#a[not(.=('b','c'))]!string().
I am not sure what the question about the "use of sequences of attributes" is about, in particular as it then mentions the XPath 1 concept of node sets. If you want to write a function or template to return some original attribute nodes from an input then xsl:sequence allows that. Of course, inside a sequence constructor like the contents of an element, if you look at 10) in https://www.w3.org/TR/xslt20/#constructing-complex-content, in the end a copy of the attribute is created.
As for creating a sequence of attributes, you can't do that in XPath which can't create new nodes, you can however do that in XSLT:
<xsl:variable name="att-sequence" as="attribute()*">
<xsl:attribute name="a" select="1"/>
<xsl:attribute name="b" select="2"/>
<xsl:attribute name="c" select="3"/>
</xsl:variable>
then you can use it elsewhere, as in
<xsl:template match="/*">
<xsl:copy>
<element>
<xsl:sequence select="$att-sequence"/>
</element>
<element>
<xsl:value-of select="$att-sequence"/>
</element>
</xsl:copy>
</xsl:template>
and will get
<example>
<element a="1" b="2" c="3"/>
<element>1 2 3</element>
</example>
http://xsltfiddle.liberty-development.net/jyyiVhg
XQuery has a more compact syntax and in contrast to XPath allows expressions to create new nodes:
let $att-sequence as attribute()* := (attribute a {1}, attribute b {2}, attribute c {3})
return
<example>
<element>{$att-sequence}</element>
<element>{data($att-sequence)}</element>
</example>
http://xqueryfiddle.liberty-development.net/948Fn56

How the "as" attribute of xsl:template affects the result of xsl:apply-templates

Given this source document:
<things>
<thing><duck>Eider</duck></thing>
<thing><duck>Mallard</duck></thing>
<thing><duck>Muscovy</duck></thing>
</things>
I require the following output
Fat Eider, Fat Mallard, Fat Muscovy
which I can indeed get with this XSL transform:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" >
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:value-of separator=", ">
<xsl:apply-templates select="//duck"/>
</xsl:value-of>
</xsl:template>
<xsl:template match="duck" as="xs:string">
<xsl:value-of select="concat('Fat ', .)"/>
</xsl:template>
</xsl:stylesheet>
However, I have three questions:
Question 1. (specific)
If I remove as="xs:string" from the duck template, I get the following output:
Fat EiderFat MallardFat Muscovy
Why? My understanding is that in XSLT 2.0 the result of xsl:apply-templates is always a sequence, and that xsl:value-of inserts its separator between the items in the sequence. So why does the sequence seem to "collapse" when the template has no as attribute? Bonus points for pointing me towards appropriate pages of Michael Kay's excellent "XSLT 2.0 and XPath 2.0, 4th Edition" book.
Question 2. (vague!)
As a novice user of XSLT, it seems to me that there are probably many ways to solve this problem. Can you put forward a good solution that takes a different approach? How do you choose between approaches?
Question 3.
Debugging. Can you recommend how to dump out intermediate results that would indicate the difference between the presence and the absence of the as attribute to the template?
See http://www.w3.org/TR/xslt20/#value-of which says
The string value of the new text node may be defined either by using
the select attribute, or by the sequence constructor (see 5.7 Sequence
Constructors) that forms the content of the xsl:value-of element.
These are mutually exclusive, and one of them must be present. The way
in which the value is constructed is specified in 5.7.2 Constructing
Simple Content.
So we need to look at http://www.w3.org/TR/xslt20/#constructing-simple-content and that says "2. Adjacent text nodes in the sequence are merged into a single text node.". So that is what is happening without the as="xs:string", the sequence constructor inside the xsl:value-of creates adjacent text nodes which are merged into a single text node. If you have as="xs:string" or did <xsl:sequence select="concat('Fat ', .)"/> the sequence constructor is of a sequence of primitive string values.

Why does index-of() return multiple values when applied to a sequence of unique nodes?

I'm using xpath2's index-of value to return the index of current() within a sorted sequence of nodes. Using SAXON, the sorted sequence of nodes are unique, yet index-of returns a sequence of two values.
This does not happen all the time, just very occasionally, but not for any reason I can find. Can someone please explain what is going on?
I have worked up a minimal example based on an example of data that routines gives this odd behavior.
The source data is:
<data>
<student userID="1" userName="user1"/>
<session startedOn="01/16/2012 15:01:18">
</session>
<session startedOn="11/16/2011 13:31:33">
</session>
</data>
My xsl document puts the session nodes into a sorted sequence $orderd at the very top of the root template:
<xsl:template match="/">
<xsl:variable name="nodes" as="node()*" select="/data/session"></xsl:variable>
<xsl:variable name="orderd" as="node()*">
<xsl:for-each select="$nodes">
<xsl:sort select="xs:dateTime(xs:dateTime(concat(substring(normalize-space(#startedOn),7,4),'-',substring(normalize-space(#startedOn),1,2),'-',substring(normalize-space(#startedOn),4,2),'T',substring(normalize-space(#startedOn),12,8)))
)" order="ascending"/>
<xsl:sequence select="."/>
</xsl:for-each>
</xsl:variable>
Since the nodes were already ordered by #startOn but in the opposite order, the sequence $orderd should be the same as document-ordered sequence $nodes, except in reverse order.
When I create output using a for-each statement, I find that somehow the two nodes are seen as identical when tested using index-of.
The code below is used to output data (and comes immediately after the chunk above):
<output>
<xsl:for-each select="$nodes">
<xsl:sort select="position()" order="descending"></xsl:sort>
<xsl:variable name="index" select="index-of($orderd,current())" as="xs:integer*"></xsl:variable>
<xsl:variable name="pos" select="position()"></xsl:variable>
<session reverse-documentOrder="{$pos}" sortedOrder="{$index}"/>
</xsl:for-each>
</output>
As the output (shown below) indicates, the index-of function is returning the sequence (1,2), meaning that it sees both nodes as identical. I have checked the expression used to sort the values, and it produces distinct and well-formed date-Time strings.
<output>
<session reverse=documentOrder="1"
sortedOrder="1 2"/>
<session reverse-documentOrder="2"
sortedOrder="1 2"/>
</output>
Not relying on the generate-id() function, which is XSLT function, but not XPath function, one can write a simple index-of() function that operates on node identity:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:my="my:my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="vNum3" select="/*/*[3]"/>
<xsl:variable name="vSeq" select="/*/*[1], /*/*[3], /*/*[3]"/>
<xsl:template match="/">
<xsl:sequence select="my:index-of($vSeq, $vNum3)"/>
</xsl:template>
<xsl:function name="my:index-of" as="xs:integer*">
<xsl:param name="pSeq" as="node()*"/>
<xsl:param name="pNode" as="node()"/>
<xsl:for-each select="$pSeq">
<xsl:if test=". is $pNode">
<xsl:sequence select="position()"/>
</xsl:if>
</xsl:for-each>
</xsl:function>
</xsl:stylesheet>
when this transformation is applied on the following XML document:
<nums>
<num>01</num>
<num>02</num>
<num>03</num>
<num>04</num>
<num>05</num>
<num>06</num>
<num>07</num>
<num>08</num>
<num>09</num>
<num>10</num>
</nums>
the wanted, correct result is returned:
2 3
Explanation: Use of the is operator.
The documentation http://www.w3.org/TR/xpath-functions/#func-index-of of index-of says "The items in the sequence $seqParam are compared with $srchParam under the rules for the eq operator. Values of type xs:untypedAtomic are compared as if they were of type xs:string.". So you are trying to compare untyped element nodes and that means they are compared as strings and both session elements have the same white space only string contents. That way both are compared as equal.
I am not sure what to suggest as I am not sure what you want to achieve but I hope the above explains the result you get.

XSLT 2.0 - looping over nodeset variable, but need to process other elements in loop as well

I have a XML like this:
<?xml version="1.0" encoding="UTF-8"?>
<nodes>
<n c="value2"/>
<n>Has a relation to node with value2</n>
<n>Has a relation to node with value2</n>
<n c="value"/>
<n>Has a relation to node with value</n>
<n c="value1"/>
<n>Has a relation to node with value1</n>
</nodes>
I sort all elements which have attributes in variable, then I iterate over this variable in for-each loop. But at the end of each loop, I need to print value of those elements which are below the currently processed element(in original XML) and have no atrribute.
That means: call apply-templates on <n> without attribute, but the "select" attr. in apply-templates does not work, probably because I´m now in variable loop.
Is there a solution for that?
Thanks
Here is the XSL:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="nodes">
<xsl:variable name="sorted">
<xsl:for-each select="n[#c]">
<xsl:sort select="#c"></xsl:sort>
<xsl:copy-of select="."></xsl:copy-of>
</xsl:for-each>
</xsl:variable>
<xsl:for-each select="$sorted/n">
<xsl:value-of select="#c"></xsl:value-of>
<xsl:apply-templates select="/nodes/n[2]"></xsl:apply-templates>
</xsl:for-each>
</xsl:template>
<xsl:template match="n[not(#c)]">
<xsl:value-of select="."></xsl:value-of>
</xsl:template>
</xsl:stylesheet>
This is just example,all this is a part of bigger project:)
Desired output with a more complicated XPAth(now even the simple one does not work) is:
Value
Has a relation to node with value
Value1
Has a relation to node with value1
Value2
Has a relation to node with value2
Has a relation to node with value2
Is it a bit clearer now?
Some thoughts: apply-templates without a select processes the child node of the current context node; in your input sample the n elements do not have any children at all. Furthermore in your variable you do a copy-of meaning you create new nodes that have no relation to the nodes in the input sample. So while I am not sure what you want to achieve your construction with apply-templates inside the for-each does not make sense, given the input sample you have posted and the variable you use.
I suspect you could use the XSLT 2.0 for-each-group group-starting-with as in
<xsl:template match="nodes">
<xsl:for-each-group select="n" group-starting-with="n[#c]">
<xsl:sort select="#c"/>
<xsl:value-of select="#c"/>
<xsl:apply-templates select="current-group() except ."/>
</xsl:for-each-group>
</xsl:template>
If that does not help then consider to post a small input sample with sample data and the corresponding output sample you want to create with XSLT 2.0, then we can make suggestions on how to achieve that.
[edit] Now that you have posted an output sample I post an enhanced version of my previous suggestion:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="text"/>
<xsl:template match="nodes">
<xsl:for-each-group select="n" group-starting-with="n[#c]">
<xsl:sort select="#c"/>
<xsl:value-of select="#c"/>
<xsl:text>
</xsl:text>
<xsl:apply-templates select="current-group() except ."/>
</xsl:for-each-group>
</xsl:template>
<xsl:template match="n[not(#c)]">
<xsl:value-of select="."/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
When I use Saxon 9.3 and run the stylesheet against your latest input sample the result is as follows:
value
Has a relation to node with value
value1
Has a relation to node with value1
value2
Has a relation to node with value2
Has a relation to node with value2
That is what you asked for I think so try that approach with your more complex real input.

Improving the performance of XSL

I am using the below XSL 2.0 code to find the ids of the text nodes that contains the list of indices that i give as input. the code works perfectly but in terms for performance it is taking a long time for huge files. Even for huge files if the index values are small then the result is quick in few ms. I am using saxon9he Java processor to execute the XSL.
<xsl:variable name="insert-data" as="element(data)*">
<xsl:for-each-group
select="doc($insert-file)/insert-data/data"
group-by="xsd:integer(#index)">
<xsl:sort select="current-grouping-key()"/>
<data
index="{current-grouping-key()}"
text-id="{generate-id(
$main-root/descendant::text()[
sum((preceding::text(), .)/string-length(.)) ge current-grouping-key()
][1]
)}">
<xsl:copy-of select="current-group()/node()"/>
</data>
</xsl:for-each-group>
</xsl:variable>
In the above solution if the index value is too huge say 270962 then the time taken for the XSL to execute is 83427ms. In huge files if the index value is huge say 4605415, 4605431 it takes several minutes to execute. Seems the computation of the variable "insert-data" takes time though it is a global variable and computed only once. Should the XSL be addessed or the processor? How can i improve the performance of the XSL.
I'd guess the problem is the generation of text-id, i.e. the expression
generate-id(
$main-root/descendant::text()[
sum((preceding::text(), .)/string-length(.)) ge current-grouping-key()
][1]
)
You are potentially recalculating a lot of sums here. I think the easiest path here would be to invert your approach: recurse across the text nodes in the document, aggregate the string length so far, and output data elements each time a new #index is reached. The following example illustrates the approach. Note that each unique #index and each text node is visited only once.
<xsl:variable name="insert-doc" select="doc($insert-file)"/>
<xsl:variable name="insert-data" as="element(data)*">
<xsl:call-template name="calculate-data"/>
</xsl:variable>
<xsl:key name="index" match="data" use="xsd:integer(#index)"/>
<xsl:template name="calculate-data">
<xsl:param name="text-nodes" select="$main-root//text()"/>
<xsl:param name="previous-lengths" select="0"/>
<xsl:param name="indexes" as="xsd:integer*">
<xsl:perform-sort
select="distinct-values(
$insert-doc/insert-data/data/#index/xsd:integer(.))">
<xsl:sort/>
</xsl:perform-sort>
</xsl:param>
<xsl:if test="$text-nodes">
<xsl:variable name="total-lengths"
select="$previous-lengths + string-length($text-nodes[1])"/>
<xsl:choose>
<xsl:when test="$total-lengths ge number($indexes[1])">
<data
index="{$indexes[1]}"
text-id="{generate-id($text-nodes[1])}">
<xsl:copy-of select="key('index', $indexes[1],
$insert-doc)"/>
</data>
<!-- Recursively move to the next index. -->
<xsl:call-template name="calculate-data">
<xsl:with-param
name="text-nodes"
select="$text-nodes"/>
<xsl:with-param
name="previous-lengths"
select="$previous-lengths"/>
<xsl:with-param
name="indexes"
select="subsequence($indexes, 2)"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<!-- Recursively move to the text node. -->
<xsl:call-template name="calculate-data">
<xsl:with-param
name="text-nodes"
select="subsequence($text-nodes, 2)"/>
<xsl:with-param
name="previous-lengths"
select="$total-lengths"/>
<xsl:with-param
name="indexes"
select="$indexes"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>

Resources