xsl:when test for processing instruction - xslt

I´m trying to create a choose statement, which detects an processing instruction
<xsl:choose>
<xsl:when test="chapter/descriptive/heading='processing-instruction("xm-replace_text")'">
<xsl:template match="chapter/descriptive/heading"/>
</xsl:when>
<xsl:otherwise>
<xsl:template match="chapter/descriptive/heading">
<fo:block
font-size="16pt"
font-weight="bold"
font-color="red"
space-before="5mm"
space-after="2mm">
<xsl:number
count="chapter | task | diagnosis | taskintervals | tools | lubrication | glossary"
format="1.1"
level="multiple"/>
 
<xsl:value-of select="."/>
</fo:block>
</xsl:template>
</xsl:otherwise>
</xsl:choose>
is it not possible to test for processing instructions like this?
edit: xml input (is the full xml needed?)
...
<chapter infoclass-1="description" prodclass-1="setup">
<descriptive prodclass-1="setup" infoclass-1="intro">
<heading><?xm-replace_text Themenangabe in Form einer Überschrift ?></heading>
...

No, a node test for a processing instruction is done literally e.g.
<xsl:template match="processing-instruction('xm-replace_text')">...</xsl:template>
would match a pi <?xm-replace_text ...?>.
With your example XML assuming you are trying to match the heading element containing that particular processing instruction then use
<xsl:template match="chapter/descriptive/heading[processing-instruction('xm-replace_text')]">...</xsl:template>
or
<xsl:template match="chapter/descriptive/heading/processing-instruction('xm-replace_text')">...</xsl:template>
if you want to match the processing instruction itself.

Related

Duplicates in a map

I currently have an XSLT function that loads key=value pairs from a text file into a map.
<xsl:function name="myns:loadMapping" as="map(*)">
<xsl:variable name="mapping" as="map(xs:string, xs:string)">
<xsl:map>
<xsl:for-each select="unparsed-text-lines($inputFile,$fileEncoding)">
<!-- Takes only lines which are in the form abc=xyz and are not comments (does not start with #) -->
<xsl:if test="contains(.,'=') and not(starts-with(.,'#'))">
<xsl:map-entry key="substring-before(.,'=')" select="substring-after(.,'=')"/>
</xsl:if>
</xsl:for-each>
</xsl:map>
</xsl:variable>
<xsl:sequence select="$mapping"/>
</xsl:function>
The function works fine unless the user tries to load a file containing duplicates, in which case the XSLT transform fails with an error (expected behaviour):
Error evaluating (map:merge(...)) on line xyz column xy of xyz.xsl:
XTDE3365: Duplicate key in constructed map: {keyInError}
Is there a way I could catch this case and keep the transformation from aborting, something like this :
<xsl:function name="myns:loadMapping" as="map(*)">
<xsl:variable name="mapping" as="map(xs:string, xs:string)">
<xsl:map>
<xsl:for-each select="unparsed-text-lines($inputFile,$fileEncoding)">
<!-- Takes only lines which are in the form abc=xyz and are not comments (does not start with #) -->
<xsl:if test="contains(.,'=') and not(starts-with(.,'#'))">
<xsl:choose>
<xsl:when test="...map contains key...">
<xsl:message>Map already contains key. Please check input file.</xsl:message>
</xsl:when>
<xsl:otherwise>
<xsl:map-entry key="substring-before(.,'=')" select="substring-after(.,'=')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:for-each>
</xsl:map>
</xsl:variable>
<xsl:sequence select="$mapping"/>
</xsl:function>
I see that there is something implemented for a future XSLT 4.0 release (Saxon - Controlling duplicates on xsl:map) but I would like to stick to XSLT 3.0 for the time being.
Thanks.
To add to Martin Honnen's suggestions, you could use xsl:iterate instead of xsl:for-each, passing the map as a parameter, which would allow you to inspect the map before adding another entry to it.
<xsl:iterate select="...">
<xsl:param name="map" select="map{}"/>
<xsl:choose>
<xsl:when test="map:contains($map, ...)">...</xsl:when>
<xsl:otherwise>
<xsl:next-iteration>
<xsl:with-param name="map" select="map:put($map, ..., ...)"/>
Well, both map:merge in XPath 3.1 or of course grouping with e.g.
<xsl:for-each-group select="unparsed-text-lines($inputFile,$fileEncoding)[contains(.,'=') and not(starts-with(.,'#'))]" group-by="substring-before(., '=')">
<xsl:map-entry key="current-grouping-key()" select="substring-after(., '=')"/>
<xsl:if test="current-group()[2]">
<xsl:message>..</xsl:message>
</xsl:if>
</xsl:for-each-group>
allow you more control than your approach without having to wait for XSLT 4 or trying to use experimental extensions.

idiomatic alternative to choose -> test -> value-of (XSLT 1.0)

In the work I do I seem to see a lot of code liek this..
<xsl:choose>
<xsl:when test="long_xpath_to_optional/#value1">
<xsl:value-of select="long_xpath_to_optional/#value"/>
</xsl:when>
<xsl:when test="another_long_xpath_to_optional/#value">
<xsl:value-of select="another_long_xpath_to_optional/#value"/>
</xsl:when>
<etc>
</etc>
<otherwise>
<xsl:value-of select="default_long_xpath_to_value"/>
</otherwise>
</xsl:choose>
its very long and very repetitive.
When I'm were working in some other (psuedo) language I would go
let values = concat(list(long_xpath_to_optional_value),list(another_long_xpath_to_optional_value))
let answer = tryhead(values,default_long_xpath_to_value)
i.e. create a list of values in priority order, and then take the head.
I only evaluate each path once
how would you do something similar in XSLT 1.0 (we can use node-sets).
I was wondering if you can create a node-set somehow
You can - but it's not going to be any shorter:
<xsl:variable name="values">
<xsl:apply-templates select="long_xpath_to_optional/#value" mode="values"/>
<xsl:apply-templates select="another_long_xpath_to_optional/#value" mode="values"/>
<xsl:apply-templates select="default_long_xpath_to_value/#value" mode="values"/>
</xsl:variable>
<xsl:value-of select="exsl:node-set($values)/value[1]" xmlns:exsl="http://exslt.org/common"/>
and then:
<xsl:template match="#value" mode="values">
<value>
<xsl:value-of select="."/>
</value>
</xsl:template>
But at least the repetition is eliminated.
Alternatively, you could do:
<xsl:template match="#value" mode="values">
<xsl:value-of select="."/>
<xsl:text>|</xsl:text>
</xsl:template>
and then:
<xsl:value-of select="substring-before($values, '|')"/>
To use variables you write
<xsl:variable name="value1" select="long_xpath_to_optional/#value1"/>
<xsl:variable name="value2" select="another_long_xpath_to_optional/#value"/>
<xsl:variable name="value3" select="default_long_xpath_to_value"/>
and then in XPath 2 or 3 all you would need is ($value1, $value2, $value3)[1] or head(($value1, $value2, $value3)) but in XSLT 1 with XPath 1 all you can write as a single expression is ($value1 | $value2 | $value3)[1] which sorts in document order so unless the document order is the same as your test order this wouldn't work to check the values; rather you would need to maintain the
<xsl:choose>
<xsl:when test="$value1">
<xsl:value-of select="$value1"/>
</xsl:when>
<xsl:when test="$value2">
<xsl:value-of select="$value2"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$value3"/>
</xsl:otherwise>
</xsl:choose>
Of course in XPath 2 you wouldn't really need the variables and could use (long_xpath_to_optional/#value1, another_long_xpath_to_optional/#value, default_long_xpath_to_value)[1] as well directly.

How to avoid creating duplicated footnotes?

I am parsing an XML that has two footnotes pointing to the same source, as in reference [1] in this example:
This is my parsing code:
<xsl:template match="//sup[#class='reference']/a">
<xsl:variable name="cite_number">
<xsl:value-of select="substring-after(substring-before(./text(),']'),'[')"/> <!-- to remove the [ ] characters -->
</xsl:variable>
<fo:footnote>
<fo:inline font-weight="bold"><fo:inline font-size="6pt" vertical-align="super"><xsl:value-of select="$cite_number"/></fo:inline></fo:inline>
<fo:footnote-body>
<xsl:variable name="cite_id"> <!-- variable to find the content of the cite -->
<xsl:value-of select="substring-after(#href,'#')"/> <!-- to remove the # character -->
</xsl:variable>
<fo:block color="#999999">
<xsl:value-of select="$cite_number"/>
<xsl:text>. </xsl:text>
<xsl:apply-templates select="ancestor::subchapter[#lang]//li[#id=$cite_id]/span[#class='reference-text']"/>
</fo:block>
</fo:footnote-body>
</fo:footnote>
</xsl:template>
The variable cite_number is the number of the cite (1, 2, 3, etc.). If there are two cites pointing at the same source, as shown in the image, the footnote is created twice.
What would be the way of having only one footnote for multiple repeated cites?
I solved it by adding a conditional that prints either the full footnote or just the superindex. In my case, which is MediaWiki pages, I check the id attribute of the parent to determine if there is only one instance of the cite not(contains(../#id,':')) or if there are more than one, and then look for the first one (contains(../#id,'-0')):
<xsl:template match="//sup[#class='reference']/a">
<xsl:variable name="cite_number">
<xsl:value-of select="substring-after(substring-before(./text(),']'),'[')"/> <!-- to remove the [ ] characters -->
</xsl:variable>
<xsl:choose>
<xsl:when test="not(contains(../#id,':')) or (contains(../#id,'-0'))]">
<fo:footnote>
<fo:inline font-weight="bold"><fo:inline font-size="6pt" vertical-align="super"><xsl:value-of select="$cite_number"/></fo:inline></fo:inline>
<fo:footnote-body>
<!-- footnote-body here -->
</fo:footnote-body>
</fo:footnote>
</xsl:when>
<xsl:otherwise>
<fo:inline font-weight="bold"><fo:inline font-size="6pt" vertical-align="super"><xsl:value-of select="$cite_number"/></fo:inline></fo:inline>
</xsl:otherwise>
</xsl:choose>
</xsl:template>

How to generate empty space in a fo:block if there is no valued in the extracted element?

I am using XSL-FO and FOP .95, whenever i write a code in xsl-fo i have to use this statement to generate an empty space:
<fo:block>
<xsl:choose>
<xsl:when test="normalize-space(Seller_Name)!=''">
<xsl:value-of select="normalize-space(Seller_Name)"/>
</xsl:when>
<xsl:otherwise><xsl:text> </xsl:text></xsl:otherwise>
</xsl:choose>
</fo:block>
I dont want to use these choose when conditions to generate an empty space to save the block collapse. is there any function or property which can be used here? I have tried line-feed-treatment and white-space-collapse but it didnt work. Please advise something.
IF you are happy with what you have above, why not template it. This would reduce the call to three lines:
<xsl:template name="blockwithblank">
<xsl:param name="field"/>
<fo:block>
<xsl:choose>
<xsl:when test="normalize-space($field)!=''">
<xsl:value-of select="$field"/>
</xsl:when>
<xsl:otherwise><xsl:text> </xsl:text></xsl:otherwise>
</xsl:choose>
</fo:block>
</xsl:template>
That above is once in the whole stylesheet, then each of the calls is only three lines:
<xsl:call-template name="blockwithblank">
<xsl:with-param name="field" select="Seller_Name"/>
</xsl:call-template>
I am not sure you can shorten it more than three lines each call.
Use two templates: one for the regular case and the other, with a higher priority, for the empty case:
<xsl:template match="Seller_Name">
<fo:block>
<xsl:value-of select="normalize-space()"/>
</fo:block>
</xsl:template>
<xsl:template match="Seller_Name[normalize-space() = '']" priority="5">
<fo:block> </fo:block>
</xsl:template>
Your XSLT would need to have a xsl:apply-templates at the appropriate point, but it would make the current template shorter.
If you're doing this a lot with multiple elements, you could match on multiple elements in each of these templates and save a lot of repetition.

Displaying xml nodes dynamically and applying style to specific nodes using recursion

Below is my xml file.
<xml>
<top>
<main>
<firstname>John</firstname>
<lastname>John</lastname>
<table></table>
<chapter>
<firstname>Alex</firstname>
<lastname>Robert</lastname>
<p>Sample text chap</p>
<figure name="f1.svg"></figure>
<chapter>
<firstname>Rebec</firstname>
<lastname></lastname>
<p>Sample text</p>
<figure name="f2.svg"></figure>
</chapter>
</chapter>
</main>
</top>
</xml>
Desired output:
<bold>John
table
<bold>Robert
Sample text chap
f1.svg
<bold> Rebec
Sample text
f2.svg
Explaination: I have written an xslt to do this. I need to fetch the xml nodes dynamically. I cannot write: xsl:apply-templates select='main/lastname'. Because my xml format could change anytime.
I have tried a logic to first fetch all the xml nodes using '$root/*'. Then if 'table' element is encountered, i use xsl:apply-templates select='current()[name() = 'TABLE']' and perform table creation operations.
This works fine. I get the desired output but my figure elements only displays f1.svg at every place in the output. f2.svg is not shown.
And how do I match only 'lastname' and make it bold?
I want to make the code as generic/modular as possible so that it loops through all the elements of the xml tree and does some formatting on the specific nodes.
Below is a recursive xslt. With this my data is getting repeated. I am writing recursive template because xslt is not sequential.
XSLT:
<xsl:call-template name="FetchNodes">
<xsl:with-param name="endIndex" select="$NumberOfNodes" />
<xsl:with-param name="startIndex" select="1" />
<xsl:with-param name="context" select="$root/*" />
</xsl:call-template>
<xsl:template name="FetchNodes">
<xsl:param name="endIndex" />
<xsl:param name="startIndex" />
<xsl:param name="context" />
<xsl:if test="$startIndex <= $endIndex">
<xsl:if test="$context[$startIndex][name() = 'table']"">
<xsl:apply-templates select="$context[$startIndex][name() = 'table']"" mode="table" />
</xsl:if>
<xsl:call-template name="FetchNodes">
<xsl:with-param name="endIndex" select="$endIndex" />
<xsl:with-param name="startIndex" select="$startIndex + 1"/>
<xsl:with-param name="context" select="$context" />
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template match="node()" mode="table">
<xsl:value-of select="node()" />
</xsl:template>
With the above xslt, something is incorrect in the xpath of apply templates. Output is not proper.
I want XSL FO output.
Can anybody suggest something?
The problem it displaying "f1.svg" instead of "f2.svg" is because of this line
<xsl:variable name="ImageName">
<xsl:value-of select="$root/*/chapter/figure/#name" />
</xsl:variable>
You are already positioned on a figure at this point, so you only need to use a relative xpath expression here. The one you are currently using is an absolute path and so will always return the first #name attribute regardless of your context. It should look this this
<xsl:variable name="ImageName">
<xsl:value-of select="#name" />
</xsl:variable>
Or better still, like this
<xsl:variable name="ImageName" select="#name" />
Having said, the code is in a template that is trying to match an element a FIGURE element, which does not exist in the XML you have shown us. You can actually simplify the template match to this, for example
<xsl:template match="figure" mode="figure">
As for making things bold, you can just add the font-weight attribute to any block you want to make bold. Something like this:
<xsl:choose>
<xsl:when test="self::lastname">
<fo:inline font-weight="bold"><xsl:value-of select="text()" /></fo:inline>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="text()" />
</xsl:otherwise>
</xsl:choose>
EDIT: Having said all that, you may not be taking the correct approach to the problem. It may be better to use template matching, taking advantage of XSLT's built-in template to navigate over the document. Essentially, just write a template for each element you want to match, and generate the output, and then carry on matching its children.
For example, to turn a chapter into an fo:block do this
<xsl:template match="chapter">
<fo:block>
<xsl:apply-templates/>
</fo:block>
</xsl:template>
To output the firstname in bold, do this
<xsl:template match="firstname">
<fo:inline font-weight="bold">
<xsl:value-of select="text()"/>
</fo:inline>
</xsl:template>
To turn a figure into an image, do this (Note the use of Attribute Value Templates here, the curly braces indicate an expression to be evaluated, not output literally)
<xsl:template match="figure">
<fo:block>
<fo:external-graphic src="../resources/{#name}" content-height="60%" scaling="uniform" padding-left="2cm"/>
</fo:block>
</xsl:template>
Try this XSLT as a starting point, and build on it
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="main">
<fo:block>
<xsl:apply-templates/>
</fo:block>
</xsl:template>
<xsl:template match="chapter">
<fo:block>
<xsl:apply-templates/>
</fo:block>
</xsl:template>
<xsl:template match="firstname">
<fo:inline font-weight="bold">
<xsl:value-of select="text()"/>
</fo:inline>
</xsl:template>
<xsl:template match="lastname"/>
<xsl:template match="figure">
<fo:block>
<fo:external-graphic src="../resources/{#name}" content-height="60%" scaling="uniform" padding-left="2cm"/>
</fo:block>
</xsl:template>
</xsl:stylesheet>