save data to map while processing xslt dynamically - xslt

I want to create dynamic map in xslt and populate the same based on some conditions in xslt, how can i do that ?
i can see examples of hardcoded map like below and stored in variable, but i don't want like that
<xsl:variable name="map">
<map>
<entry key="key-1">value1</entry>
<entry key="key-2">value2</entry>
<entry key="key-3">value3</entry>
</map>
</xsl:variable>

Straight from the XSLT 3 spec in https://www.w3.org/TR/xslt-30/#element-map is the example
<xsl:variable name="index" as="map(xs:string, element(employee))">
<xsl:map>
<xsl:for-each select="//employee">
<xsl:map-entry key="#empNr" select="."/>
</xsl:for-each>
</xsl:map>
</xsl:variable>
It seems, however, that this gives a type error unless the input is schema validated so for untyped XML or not schema-aware XSLT you would need to redefine the map type or change the code a bit, the second option is shown below:
<xsl:variable name="index" as="map(xs:string, element(employee))">
<xsl:map>
<xsl:for-each select="//employee">
<xsl:map-entry key="string(#empNr)" select="."/>
</xsl:for-each>
</xsl:map>
</xsl:variable>
Example fiddle is at https://xsltfiddle.liberty-development.net/3MP42MS.

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.

How can I loop and generate keys for maps with XSLT 3.0?

I tried to construct a new map. In my source xml I've got many products (product data and IDs). How can I generate so many keys like products?
The goal is a transformation from XML to XML with XSLT. The idea was to create a map and in a next step call the keys for adressing the specifics product datas I need. So I need to know if this is possible with using maps or is there another solution?
Example for the source XML
<?xml version="1.0" encoding="UTF-8"?>
<root>
<row>
<id>102</id>
<product>Lenovo 1234</product>
<productfamily>laptop</productfamily>
</row>
<row>
.....
XSLT
<xsl:variable name="val" as="map(xs:integer, xs:integer)">
<xsl:map>
<xsl:for-each select="//id">
<xsl:map-entry key="" select="."/>
</xsl:map>
</xsl:variable>
<xsl:template match="/">
<xsl:value-of select="map:get($val , 102)"/>
</xsl:template>
To create a map based on a simple functional relationship in the data you can do
<xsl:variable name="index" as="map(*)">
<xsl:map>
<xsl:for-each select="//x">
<xsl:map-entry key=".//#id" select="."/>
</xsl:for-each>
</xsl:map>
</xsl:variable>
or if you prefer
<xsl:variable name="index" as="map(*)"
select="map:merge(//x ! map:entry(.//#id, .))"/>

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>

Convert string into XML structure XSLT

I am getting an input structure as something like this
<ParameterSet>2|InterfaceMethod|EQ|I|GenericQuery|NIL</ParameterSet>
<ParameterSet>1|TargetFilename|EQ|I|VendorMaster|NIL</ParameterSet>
the output should look something like below
<Parameter>
<Expression>2</Expression>
<Parametername>InterfaceMethod</Parametername>
<Parameter_Opt>EQ</Parameter_Opt>
<Parameter_Sign>I</Parameter_Sign>
<Range_Low_Value>GenericQuery</Range_Low_Value>
<Range_High_Value>NIL</Range_High_Value>
</Parameter>
<Parameter>
<Expression>1</Expression>
<Parametername>TargetFilename</Parametername>
<Parameter_Opt>EQ</Parameter_Opt>
<Parameter_Sign>I</Parameter_Sign>
<Range_Low_Value>VendorMaster</Range_Low_Value>
<Range_High_Value>NIL</Range_High_Value>
</Parameter>
My Problem is the tag gets converted to <ParameterSet&gt and am not able to use foreach when I write into another repetitive structure.
Can anyone provide some sample code.
The operation you are looking for - turning lexical XML into a tree of nodes - is called parsing. Some XSLT processors have an extension function, e.g. saxon:parse(), that does this (in XSLT 3.0 it's available out-of-the-box as fn:parse-xml()). With other processors, you may be able to write your own extension function by calling out to Java or Javascript.
Your input data isn't XML, possibly it's encoded (escaped) XML. So, you need to turn it into well-formed XML, in other words perform pre-processing. Then apply XSL transform.
xslt is good at rearranging xml into different xml, not at changing non-xml into xml. While you could do this with xslt using lots of nested substring-before or the like, it will be much much easier if you can run it through sed or something first to create xml out of the |-delimited string.
Thanks everyone for your inputs, I wanted this solution in XSLT as that is only processor I had and also it is running on version 1.0. Got it working with the code below
<xsl:variable name="TempArea">
<ParameterSet>2|InterfaceMethod|EQ|I|GenericQuery|NIL</ParameterSet>
<ParameterSet>1|TargetFilename|EQ|I|VendorMaster|NIL</ParameterSet>
<ParameterSet>1|CompanyCode|EQ|I|4900|NIL</ParameterSet></xsl:when>
</xsl:variable>
<xsl:call-template name="for.loop.Parameters">
<xsl:with-param name="sourceNodes" select="substring-after($TempArea,'<ParameterSet>')"/>
</xsl:call-template>
<xsl:template name="for.loop.Parameters">
<xsl:param name="sourceNodes"/>
<xsl:variable name="temp">
<xsl:choose>
<xsl:when test="string-length($sourceNodes) > '0'">
<xsl:value-of select="substring-before($sourceNodes,'</ParameterSet>')"/>
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:variable name="Expression" select="substring-before($temp, '|')"/>
<xsl:variable name="remaining" select="substring-after($temp, '|')"/>
<xsl:variable name="Name" select="substring-before($remaining, '|')"/>
<xsl:variable name="remainingNext" select="substring-after($remaining, '|')"/>
<xsl:variable name="Option" select="substring-before($remainingNext, '|')"/>
<xsl:variable name="remainingNext1" select="substring-after($remainingNext, '|')"/>
<xsl:variable name="Sign" select="substring-before($remainingNext1, '|')"/>
<xsl:variable name="remainingNext2" select="substring-after($remainingNext1, '|')"/>
<xsl:variable name="LowValue" select="substring-before($remainingNext2, '|')"/>
<xsl:variable name="HighValue" select="substring-after($remainingNext2, '|')"/>
<Parameter>
<Expression>
<xsl:value-of select="$Expression"/>
</Expression>
<Parametername>
<xsl:value-of select="$Name"/>
</Parametername>
<Parameter_Opt>
<xsl:value-of select="$Option"/>
</Parameter_Opt>
<Parameter_Sign>
<xsl:value-of select="$Sign"/>
</Parameter_Sign>
<Range_Low_Value>
<xsl:value-of select="$LowValue"/>
</Range_Low_Value>
<Range_High_Value>
<xsl:value-of select="$HighValue"/>
</Range_High_Value>
</Parameter>
<xsl:variable name="test">
<xsl:value-of select="substring-after($sourceNodes,'</ParameterSet>')"/>
</xsl:variable>
<xsl:if test="string-length($test)> 1 ">
<xsl:call-template name="for.loop.Parameters">
<xsl:with-param name="sourceNodes">
<xsl:value-of select="substring-after($test,'<ParameterSet>')"/>
</xsl:with-param>
</xsl:call-template>
</xsl:if>
</xsl:template>

index in loop XSL

I have two nested loop in XSL like this, at this moment I use position() but it's not what I need.
<xsl:for-each select="abc">
<xsl:for-each select="def">
I wanna my variable in here increasing fluently 1,2,3,4,5.....n
not like 1,2,3,1,2,3
</xsl:for-each>
</xsl:for-each>
Can you give me some idea for this stub. Thank you very much!
With XSL, the problem is you cannot change a variable (it's more like a constant that you're setting). So incrementing a counter variable does not work.
A clumsy workaround to get a sequential count (1,2,3,4,...) would be to call position() to get the "abc" tag iteration, and another call to position() to get the nested "def" tag iteration. You would then need to multiply the "abc" iteration with the number of "def" tags it contains. That's why this is a "clumsy" workaround.
Assuming you have two nested "def" tags, the XSL would look as follows:
<xsl:for-each select="abc">
<xsl:variable name="level1Count" select="position() - 1"/>
<xsl:for-each select="def">
<xsl:variable name="level2Count" select="$level1Count * 2 + position()"/>
<xsl:value-of select="$level2Count" />
</xsl:for-each>
</xsl:for-each>
Just change the way to select the items to loop over:
<xsl:for-each select="abc/def">
<xsl:value-of select="position()"/>
</xsl:for-each>
Should you specifically need to keep the nested loops, consider adding yet another loop like this:
<xsl:variable name="items" select="//abc/def"/>
<xsl:for-each select="abc">
<xsl:for-each select="def">
<xsl:variable name="id" select="generate-id()"/>
<xsl:for-each select="$items">
<xsl:if test="generate-id()=$id">
<xsl:value-of select="position()"/>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
<xsl:for-each select="abc">
<xsl:variable name="i" select="position()"/>
<xsl:for-each select="def">
<xsl:value-of select="$i" />
</xsl:for-each>
</xsl:for-each>
This is an extension of pythonquick's answer that handles different numbers of sub-elements:
<xsl:for-each select="abc">
<xsl:variable name="level1Position" select="position()"/>
<xsl:variable name="priorCount" select="count(../abc[position() < $level1Position]/def)"/>
<xsl:for-each select="def">
<xsl:variable name="level2Count" select="$priorCount + position()"/>
<xsl:value-of select="$level2Count" />
</xsl:for-each>
</xsl:for-each>
Input:
<root>
<abc>
<def>A</def>
<def>B</def>
<def>C</def>
</abc>
<abc>
<def>D</def>
<def>E</def>
</abc>
<abc>
<def>F</def>
</abc>
<abc>
<def>G</def>
<def>H</def>
<def>I</def>
</abc>
</root>