Avoid repetition in XSLT template rules using XSLT 1.0 - xslt

Given the following XML document:
<document>
<a>123</a>
<foo_1>true</foo_1>
<foo_2>false</foo_2>
<foo_3>true</foo_3>
<foo_4>true</foo_4>
<foo_5>false</foo_5>
<b/>
<bar_1>false</bar_1>
<bar_2>false</bar_2>
<bar_3>true</bar_3>
<bar_4>false</bar_4>
<bar_5>true</bar_5>
<c>some text</c>
</document>
I want to transform this document by eliminating all enumerated elements containing false and by converting all enumerated elements containing true into the form <prefix_n>value</prefix_n>, where value is the number after the underscore.
For the example given above the result should look like this:
<document>
<a>123</a>
<foo_n>1</foo_n>
<foo_n>3</foo_n>
<foo_n>4</foo_n>
<b/>
<bar_n>3</bar_n>
<bar_n>5</bar_n>
<c>some text</c>
</document>
For this I am using the following transformation, which works fine.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:output indent="yes"/>
<!-- standard copy template -->
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*" />
</xsl:copy>
</xsl:template>
<xsl:template match="*[substring(name(), 1, 4) = 'foo_']">
<xsl:if test=". = 1 or . = 'true'">
<xsl:element name="foo_n">
<xsl:value-of select="substring-after(name(), 'foo_')"/>
</xsl:element>
</xsl:if>
</xsl:template>
<xsl:template match="*[substring(name(), 1, 4) = 'bar_']">
<xsl:if test=". = 1 or . = 'true'">
<xsl:element name="bar_n">
<xsl:value-of select="substring-after(name(), 'bar_')"/>
</xsl:element>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Now I'm facing the problem that I have to deal with a multitude of different prefixes, not just foo_ and bar_.
Is there a way to turn the template rules into a named template where I can pass in the prefix as an argument, thereby avoiding to write a lot of repetitive template rules?

Here's one way you could look at it:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[starts-with(name(), 'foo_') or starts-with(name(), 'bar_')]">
<xsl:if test=". = 1 or . = 'true'">
<xsl:element name="{substring-before(name(), '_')}-n">
<xsl:value-of select="substring-after(name(), '_')"/>
</xsl:element>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Here's another:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[contains(translate(name(), '123456789', '000000000'), '_0')]">
<xsl:if test=". = 1 or . = 'true'">
<xsl:variable name="prefix" select="substring-before(translate(name(), '123456789', '000000000'), '_0')" />
<xsl:element name="{$prefix}-n">
<xsl:value-of select="substring(name(), string-length($prefix) + 2)"/>
</xsl:element>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
This last one should work with any prefix followed by _[1-9] - including prefixes that contain an additional underscore and including prefixes that contain other prefixes.

Is there a way to turn the template rules into a named template where I can pass in the prefix as an argument
Well, sure:
<xsl:template name="if-true">
<xsl:param name="prefix"/>
<xsl:if test=". = 1 or . = 'true'">
<xsl:element name="{concat($prefix, 'n')}">
<xsl:value-of select="substring-after(name(), $prefix)"/>
</xsl:element>
</xsl:if>
</xsl:template>
<!-- example using the above: -->
<xsl:template match="*[starts-with(name(), 'foo_')]">
<xsl:call-template name="if-true">
<xsl:with-param name="prefix" select="'foo_'"/>
</xsl:call-template>
</xsl:template>
But it's not clear whether that would really achieve your objective:
, thereby avoiding to write a lot of repetitive template rules?
because you would still need a bunch of templates of the second kind to match the specific elements you want to transform. A solution along the lines #michael.hor257k suggested, that avoids duplication by making the same template match all the elements you want to transform, is a better alternative.

Related

Conditionally modifying text in multiple identically named children

I'm trying to conditionally modify the content of some XML. Am element has multiple identically named children, which I want to modify based on the text content. For example, I have the below XML:
<first>
<second>
<third>alice</third>
<third>bob</third>
<third>charlie</third>
</second>
</first>
Which I'd like to transform into:
<first>
<second>
<third>xavier</third>
<third>yvonne</third>
<third>charlie</third>
</second>
</first>
I had thought the below xsl would work, but it doesn't (I suspect for a couple of reasons). What am I doing wrong?
<?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" encoding="UTF-8" omit-xml-declaration="no"/>
<xsl:strip-space elements="*"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="/first/second/third/">
<xsl:choose>
<xsl:when test="contains(text(), 'alice')">
<xsl:text>xavier</xsl:text>
</xsl:when>
<xsl:when test="contains(text(), 'bob')">
<xsl:text>Yvonne</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="text()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Two things you're doing wrong:
You have a slash at the end of this XPath /first/second/third/. Syntactically, it's not legal to have a slash at the end of an XPath, and you don't need one here.
You have a template that's supposed to match third elements (basically providing a substitute for what they once were), but you're not copying the elements. You're just replacing them with text, which means you'd have a result like:
<first>
<second>xavieryvonnecharlie</second>
</first>
In order to get your attempt to work, modifying the template to this should be sufficient:
<xsl:template match="/first/second/third">
<xsl:copy>
<xsl:choose>
<xsl:when test="contains(text(), 'alice')">
<xsl:text>xavier</xsl:text>
</xsl:when>
<xsl:when test="contains(text(), 'bob')">
<xsl:text>Yvonne</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="text()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:copy>
</xsl:template>
However can do this much more cleanly by having templates to match the text nodes you want to replace:
<?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" encoding="UTF-8" omit-xml-declaration="no"/>
<xsl:strip-space elements="*"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="third/text()[. = 'alice']">xavier</xsl:template>
<xsl:template match="third/text()[. = 'bob']">yvonne</xsl:template>
</xsl:stylesheet>
When run on your sample input, the result is:
<first>
<second>
<third>xavier</third>
<third>yvonne</third>
<third>charlie</third>
</second>
</first>

Resolving variables in XSLT

I'm having problems with resolving variables in XSLT. I have a working XSL file with fixed values that I now want to make dynamic (the variale declarations will be move outside the XSL-file once it works). My current problem is to use variable $beginning in the starts-with function. This is the way all the googling has lead me to believe it should look, but it will not compile. It works how I use it in the substring-after function. How should this be done?
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="oldRoot" select="'top'" />
<xsl:variable name="beginning" select="concat('$.',$oldRoot)" />
<xsl:variable name="newRoot" select="'newRoot'" />
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="bind/#ref[starts-with(., $beginning)]">
<xsl:attribute name="ref">
<xsl:text>$.newRoot.</xsl:text><xsl:value-of select="$oldRoot"></xsl:value-of>
<xsl:value-of select="substring-after(., $beginning)" />
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
In XSLT 1.0 it is considered an error for a template match expression to contain a variable (See https://www.w3.org/TR/xslt#section-Defining-Template-Rules), so this line is failing
<xsl:template match="bind/#ref[starts-with(., $beginning)]">
(I believe some processors may allow it, but if they were following the spec, they shouldn't. It is allowed in XSLT 2.0 though).
What you can do is move the condition inside the template, and handle it with an xsl:choose
Try this XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="oldRoot" select="'top'" />
<xsl:variable name="beginning" select="concat('$.',$oldRoot)" />
<xsl:variable name="newRoot" select="'newRoot'" />
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="bind/#ref">
<xsl:choose>
<xsl:when test="starts-with(., $beginning)">
<xsl:attribute name="ref">
<xsl:text>$.newRoot.</xsl:text><xsl:value-of select="$oldRoot"></xsl:value-of>
<xsl:value-of select="substring-after(., $beginning)" />
</xsl:attribute>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="." />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>

XSLT regular expression to remove sequences text

I have an XML, something like this:
<?xml version="1.0" encoding="UTF-8"?>
<earth>
<computer>
<parts>;;remove;;This should stay;;remove too;;This stay;;yeah also remove;;this stay </parts>
</computer>
</earth>
I want to create an XSLT 2.0 transform to remove all text which starts and ends with ;;
<?xml version="1.0" encoding="utf-8"?>
<earth>
<computer>
<parts>This should stay This stay this stay </parts>
</computer>
</earth>
Try to do something like this but no luck:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fn="http://www.w3.org/2005/xpath-functions"
exclude-result-prefixes="fn">
<xsl:output encoding="utf-8" method="xml" indent="yes" />
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="parts">
<xsl:element name="parts" >
<xsl:value-of select="replace(., ';;.*;;','')" />
</xsl:element>
</xsl:template>
</xsl:stylesheet>
Wow, what a dumb way to markup text. You have XML at your disposal, why not use it? And even if marking this way, why not use different symbols for opening and closing the marked parts?
Anyway, I believe this returns the expected result:
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="parts">
<xsl:copy>
<xsl:value-of select="replace(., ';;.+?;;', '')" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Another approach would be tokenize on ";;" as separator, then remove all even-numbered tokens:
<xsl:template match="parts">
<parts>
<xsl:value-of select="tokenize(.,';;')[position() mod 2 = 1]"
separator=""/>
</parts>
</xsl:template>
XSLT 1.0
For this kind of thing I'd use recursion. Just using string replace you can get what is before and after a certain character (or set of characters). All you need to do is continually loop over the string until there are no more occurrences of the replace character, like follows:
<xsl:template name="string-remove-between">
<xsl:param name="text" />
<xsl:param name="remove" />
<xsl:choose>
<xsl:when test="contains($text, $remove)">
<xsl:value-of select="substring-before($text,$remove)" />
<xsl:call-template name="string-remove-between">
<xsl:with-param name="text" select="substring-after(substring-after($text,$remove), $remove)" />
<xsl:with-param name="remove" select="$remove" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$text"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Then you'd just call the template with your text and the section you want to remove:
<xsl:call-template name="string-remove-between">
<xsl:with-param name="text" select="parts"/>
<xsl:with-param name="remove">;;</xsl:with-param>
</xsl:call-template>
Note that there are two substring-after calls, this makes sure we get the second instance of the replace characters ';;' so we aren't pulling in the text between.

Applying consecutive transformations in xslt

I am a newbie to xslt and I have a variable "name" which stores a result of a transformation how can we transform the variable "name" using some other template in same xslt file.
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes" />
<xsl:strip-space elements="*" />
<xsl:template match="#* | node()" >
<xsl:variable name="name">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:variable>
</xsl:template>
<xsl:template match="ns1:BP7Locations" >
<xsl:copy>
<xsl:apply-templates select="ns1:Entry">
<xsl:sort select="ns4:Location/ns4:LocationNum" />
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
In XSLT 2.0 the typical pattern for a multi-phase transformation is
<xsl:variable name="temp1">
<xsl:apply-templates mode="phase1"/>
</xsl:variable>
<xsl:variable name="temp2">
<xsl:apply-templates select="$temp1" mode="phase2"/>
</xsl:variable>
<xsl:apply-templates select="$temp2" mode="phase3"/>
In XSLT 1.0 this isn't allowed, because the variable holds a "result tree fragment" which can only be processed in very limited ways. Nearly every XSLT 1.0 processor implements the exslt:node-set() extension function so you can get around this restriction. The code then becomes:
<xsl:variable name="temp1">
<xsl:apply-templates mode="phase1"/>
</xsl:variable>
<xsl:variable name="temp2">
<xsl:apply-templates select="exslt:node-set($temp1)" mode="phase2"/>
</xsl:variable>
<xsl:apply-templates select="exslt:node-set($temp2)" mode="phase3"/>
You will need to add the namespace xmlns:exslt="http://exslt.org/common" to your stylesheet.
You don't have to use different modes for the different phases of processing, but it helps to avoid hard-to-spot bugs: the template rules for each processing phase should have corresponding mode attributes, and it can also be a good idea to put the rules for each mode in a separate stylesheet module.
For an example, you can consider this XML:
<root>
<a>value</a>
</root>
And this XSLT:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exslt="http://exslt.org/common" version="1.0">
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="root">
<xsl:variable name="a">
<xsl:apply-templates select="a" mode="mode1"/>
</xsl:variable>
<xsl:apply-templates select="exslt:node-set($a)/a" mode="mode2"/>
</xsl:template>
<xsl:template match="a" mode="mode1">
<a><xsl:value-of select="'mode1 called'"/></a>
</xsl:template>
<xsl:template match="a" mode="mode2">
<a><xsl:value-of select="concat(., ' mode2 called')"/></a>
</xsl:template>
</xsl:stylesheet>
This produces the following as output:
<?xml version="1.0" encoding="utf-8"?>
<a xmlns:exslt="http://exslt.org/common">mode1 called mode2 called</a>
The XSLT's first template has a variable a which stores the data after processing element <a> and then the xsl:apply-templates processes the data in the variable a again. Here the #mode on xsl:template differentiates the second and the third templates.

XSLT select text without children

XML:
<data><ph>Foo</ph>Bar</data>
XSL:
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<xsl:apply-templates select="data/ph"/>
<xsl:apply-templates select="data"/>
</xsl:template>
<xsl:template match="data/ph">
<xsl:value-of select="."/>
</xsl:template>
<xsl:template match="data">
<xsl:value-of select="."/>
</xsl:template>
When the XSL selects the text in the /data/ with <xsl:template match="data"><xsl:value-of select="."/> it is also selecting the text in the child entity data/ph. How do I point to only the text of /data/, without including the text of /data/ph/ ?
My output should be: FooBar, and not FooFooBar.
When the XSL selects the text in the /data/ with <xsl:template
match="data"><xsl:value-of select="."/> it is also selecting the text
in the child entity data/ph. How do I point to only the text of
/data/, without including the text of /data/ph/ ?
Use:
<xsl:copy-of select="text()"/>
This copies all text node-children of the current node.
With this correction, the whole transformation becomes:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<xsl:apply-templates select="data/ph"/>
<xsl:apply-templates select="data"/>
</xsl:template>
<xsl:template match="data/ph">
<xsl:value-of select="."/>
</xsl:template>
<xsl:template match="data">
<xsl:copy-of select="text()"/>
</xsl:template>
</xsl:stylesheet>
and when applied on the provided XML document:
<data><ph>Foo</ph>Bar</data>
the wanted, correct result is produced:
FooBar