EXSLT function (in lxml) returns no result - xslt

I'm trying to write a EXSLT function, but for some reason it doesn't seem to return the result. The function is supposed to look up a node in another document:
<func:function name="toc:element">
<xsl:param name="id" />
<xsl:for-each select="$toc">
<func:result select="key('id', $id)" />
</xsl:for-each>
</func:function>
With xsl:message I can see that it indeed receives a valid $id and the key() returns a single node; but when I call it from another template, it seems to produce no result.
Environment: libxml2/libxslt, not sure how to check the version; I'm using them from lxml v3.2.3.

Found a solution: if I add an intermediate variable, everything works.
<func:function name="toc:element">
<xsl:param name="id" />
<xsl:for-each select="$toc">
<xsl:variable name="result" select="key('id', $id)" />
<func:result select="$result" />
</xsl:for-each>
</func:function>
Apparently, the function result uses the original context to evaluate.

Related

Function in XSLT

I am using the following formula at a lot of places inside an xsl file.
<xsl:value-of select="format-number($Value div 1000000, '##.##')" />
Is there anyway I can create a function so I can keep the logic at one place and reuse it as per below example?
Example:
<xsl:value-of select="ConvertToMillionAndFormatNumber($Value)" />
There are no custom functions in XSLT 1.0 (unless your processor happens to support them as an extension), but you can use a named template:
<xsl:template name="ConvertToMillionAndFormatNumber">
<xsl:param name="Value" />
<xsl:value-of select="format-number($Value div 1000000, '##.##')" />
</xsl:template>
and call it as:
<xsl:call-template name="ConvertToMillionAndFormatNumber">
<xsl:with-param name="Value" select="your-value-here"/>
</xsl:call-template>

Attributes as arguments return empty values in xsl:contains() in XSLT

The variable $authors is declared as follows:
<listAuthor>
<author catalogNumber="26A.18.1" fullName="Pjotr Domanski"/>
<author catalogNumber="26A.19.1" fullName="Hermine Rex"/>
<author catalogNumber="26A.19.2" fullName="Christoferus Hohenzell"/>
</listAuthor>
Further, the following XML input is given:
<h1:Block Type="obj">
<h1:Field Type="5000" Value="77772001" />
<h1:Field Type="5209" Value="26A.19.1 : Lazy Tennis"/>
</h1:Block>
The expected output is:
<h1:Block Type="obj">
<h1:Field Type="5000" Value="77772001" />
<h1:Field Type="5209" Value="26A.19.1 : Lazy Tennis"/>
<h1:Field Type="9904" Value="Hermine Rex"/>
</h1:Block>
Here is a snippet of my transformation:
<xsl:template match="h1:Block">
<h1:Block Type="obj">
<xsl:copy-of select="child::*"/>
<h1:Field Type="9904">
<xsl:attribute name="Value"
select="$authors/listAuthor/author[contains (h1:Field[#Type='5209']/#Value, #catalogNumber)]/#fullName"/>
</h1:Field>
</h1:Block>
</xsl:template>
The name of the author should be put in h1:Field[#Type='9904']/#Value, when #catalogNumber in $authors/author is contained in h1:Field[#Type='5209']. Unfortunately, the attribute #Value remains empty.
I found the following workaround by introducing a variable:
<xsl:template match="h1:Block">
<h1:Block Type="obj">
<xsl:copy-of select="child::*"/>
<xsl:variable name="value5209" select="h1:Field[#Type='5209']/#Value"/>
<h1:Field Type="9904">
<xsl:attribute name="Value"
select="$authors/listAuthor/author[contains ($value5209, #catalogNumber)]/#fullName"/>
</h1:Field>
</h1:Block>
</xsl:template>
In this case, it works fine. Does anyone have any idea why it doesn't work in the first case? I use Saxon Saxon-HE 9.6.
Inside the predicate the context node is the author element which does not have a h1:Field, so change $authors/listAuthor/author[contains (h1:Field[#Type='5209']/#Value, #catalogNumber)]/#fullName to $authors/listAuthor/author[contains (current()/h1:Field[#Type='5209']/#Value, #catalogNumber)]/#fullName to select the context node of the template.

Is xsl:sequence always non-empty?

I don't understand output from this stylesheet:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:apply-templates select="root/sub"/>
</xsl:template>
<xsl:template match="sub">
<xsl:variable name="seq">
<xsl:sequence select="*" />
</xsl:variable>
<xsl:message>
<xsl:value-of select="#id" />
<xsl:text>: </xsl:text>
<xsl:value-of select="count($seq)" />
</xsl:message>
</xsl:template>
</xsl:stylesheet>
when applied to following XML:
<root>
<sub id="empty" />
<sub id="one"><one/></sub>
<sub id="two"><one/><one/></sub>
<sub id="three"><one/><one/><one/></sub>
</root>
Output, written by xsl:message element, is:
empty: 1
one: 1
two: 1
three: 1
I expected this one instead:
empty: 0
one: 1
two: 2
three: 3
Why does count($seq) always return 1 in this case? How would you change variable definition, so that I can later test it for emptiness? (Simple <xsl:variable name='seq' select='*' /> would return expected answer, but is not an option ... I want to change between variable in this template, and test it for emptiness later).
Let me try to answer the "why" part of your question.
If you write the following statement:
<xsl:variable name="x" select="*" />
the variable $x contains the sequence of the child nodes of the current node. There's no implicit parent node in $x, because you use select. Now consider the following:
<xsl:variable name="x">
<content />
<content />
</xsl:variable>
where variable $x contains a sequence of one node: the parent node of content. Here, count($x) will always give you 1. To get the amount of content elements, you need to count the children of the $x implicit root node: count($x/content).
As a rule of thumb, you can remember that: if xsl:variable is itself an empty element with a select attribute, the result set will be assigned to the variable. If you use xsl:variable without the select attribute, you always create an implicit parent with the variable, and the contents of the variable are the children underneath it.
The same applies for your example with xsl:sequence as a child to xsl:variable. By having a non-empty xsl:variable you create an implicit parent for the sequence, which is why you always get 1 if you count the variable itself.
If you need both: a bundle of statements inside xsl:variable, but the behavior of the select attribute, you can workaround this by using the following:
<xsl:variable name="x" select="$y/*" />
<xsl:variable name="y">
<xsl:sequence select="foo" />
</xsl:variable>
which will now yield the expected amount for count($x), but whether or not that's really beneficial or makes your code clearer is arguable.
It works as you might expect if you either change the variable declaration to:
<xsl:variable name="seq" select="*"/>
or declare the type of the variable using 'as' attribute:
<xsl:variable name="seq" as="item()*">
<xsl:sequence select="*" />
</xsl:variable>
Not specifying any type information often leeds to surprising results in XSLT 2.0. If you are using Saxon, you can output how Saxon interprets the stylesheet using the explain extension attribute:
<xsl:template match="sub" saxon:explain="yes" xmlns:saxon="http://saxon.sf.net/">
<xsl:variable name="seq">
<xsl:sequence select="*" />
</xsl:variable>
<xsl:message>
<xsl:value-of select="#id" />
<xsl:text>: </xsl:text>
<xsl:value-of select="count($seq)" />
</xsl:message>
</xsl:template>
As you can see, Saxon constructs a document-node out of the sequence:
Optimized expression tree for template at line 6 in :
let $seq[refCount=1] as document-node() :=
document-constructor
child::element()
return
message
You are selecting the sub nodes, and then counting each node - so it'll always be 1. You need to count the children, for example:
<xsl:value-of select="count($seq/*)" />
will give you the output you're expecting.

Store a group of nodes as a variable in XSLT?

So here is my current code:
<xsl:variable name="address">
<xsl:value-of select="concat(/node1/node2/address.node/street, /node1/node2/address.node/city, /node1/node2/address.node/zip)" />
</xsl:variable>
Now, I'm trying to shorten this down to:
<xsl:variable name="addressNode">
<xsl:value-of select="/node1/node2/address.node" />
</xsl:variable>
<xsl:variable name="address">
<xsl:value-of select="concat($addressNode/street, $addressNode/city, $addressNode/zip)" />
</xsl:variable>
However this is not working at all as expected... could anyone point me in the right direction? I tried using copy-to instead of value-of for addressNode, but it still isn't working :(
When you use xsl:value-of inside xsl:variable you get a variable of type string, not a node. You should use use the select attribute of xsl:variable:
<xsl:variable name="addressNode" select="/node1/node2/address.node" />

Optional parameters when calling an XSL template

Is there way to call an XSL template with optional parameters?
For example:
<xsl:call-template name="test">
<xsl:with-param name="foo" select="'fooValue'" />
<xsl:with-param name="bar" select="'barValue'" />
</xsl:call-template>
And the resulting template definition:
<xsl:template name="foo">
<xsl:param name="foo" select="$foo" />
<xsl:param name="bar" select="$bar" />
<xsl:param name="baz" select="$baz" />
...possibly more params...
</xsl:template>
This code will gives me an error "Expression error: variable 'baz' not found." Is it possible to leave out the "baz" declaration?
Thank you,
Henry
You're using the xsl:param syntax wrong.
Do this instead:
<xsl:template name="foo">
<xsl:param name="foo" />
<xsl:param name="bar" />
<xsl:param name="baz" select="DEFAULT_VALUE" />
...possibly more params...
</xsl:template>
Param takes the value of the parameter passed using the xsl:with-param that matches the name of the xsl:param statement. If none is provided it takes the value of the select attribute full XPath.
More details can be found on W3School's entry on param.
Personally, I prefer doing the following:
<xsl:call-template name="test">
<xsl:with-param name="foo">
<xsl:text>fooValue</xsl:text>
</xsl:with-param>
I like using explicitly with text so that I can use XPath on my XSL to do searches. It has come in handy many times when doing analysis on an XSL I didn't write or didn't remember.
The value in the select part of the param element will be used if you don't pass a parameter.
You are getting an error because the variable or parameter $baz does not exist yet. It would have to be defined at the top level for it to work in your example, which is not what you wanted anyway.
Also if you are passing a literal value to a template then you should pass it like this.
<xsl:call-template name="test">
<xsl:with-param name="foo">fooValue</xsl:with-param>
Please do not use <xsl:param .../> if you do not need it to increase readability.
This works great:
<xsl:template name="inner">
<xsl:value-of select="$message" />
</xsl:template>
<xsl:template name="outer">
<xsl:call-template name="inner">
<xsl:with-param name="message" select="'Welcome'" />
</xsl:call-template>
</xsl:template>