Reputation: 700
I'm quite new to XSLT and therefore I want to know what is the best practice for checking the existance of an attribute. My XML looks something like this:
<root>
<languages>
<lang id="EN">English<lang>
<lang id="FR">French<lang>
<lang id="DE">German</lang>
</languages>
<items>
<item lang="EN">test 1</item>
<item>test 2</item>
<item lang="FR">item 3</item>
</items>
</root>
Note that the 'lang'-attribute for the 'item'-element is optional.
Now I want to loop through the items using a -loop, while checking if it has a "lang"-attribute. If it does, I want to fetch the entire string using the ID (eg. EN -> 'English'). If the attribute isn't set I want it to write "No language set" or something alike.
Now I use the following code but I'm questioning myself if it can't be done in a more efficient way.
<xsl:for-each select="//root/items/item">
<xsl:variable name="cur_lang" select="@lang" /> <!-- first I store the attr lang in a variable -->
<xsl:choose>
<xsl:when test="@lang"> <!-- then i test if the attr exists -->
<xsl:value-of select="//root/languages/lang[@id=$cur_lang]" /> <!-- if so, parse the element value -->
</xsl:when>
<xsl:otherwise>
No language set <!-- else -->
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
Any suggestions / tips?
Upvotes: 7
Views: 21026
Reputation: 52888
Another alternative, if you're able to use XSLT 3.0, is a map (another helpful link: map).
XML Input (fixed to be well-formed)
<root>
<languages>
<lang id="EN">English</lang>
<lang id="FR">French</lang>
<lang id="DE">German</lang>
</languages>
<items>
<item lang="EN">test 1</item>
<item>test 2</item>
<item lang="FR">item 3</item>
</items>
</root>
XSLT 3.0
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
xmlns:xs="http://www.w3.org/2001/XMLSchema" extension-element-prefixes="xs map">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="lang-map" as="map(xs:string, xs:string)"
select="map:new(
for $lang in /*/languages/lang
return
map{$lang/@id := $lang/string()}
)"/>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="languages"/>
<xsl:template match="item[@lang and map:contains($lang-map,@lang)]">
<item><xsl:value-of select="$lang-map(current()/@lang)"/></item>
</xsl:template>
<xsl:template match="item">
<item>No language found.</item>
</xsl:template>
</xsl:stylesheet>
Output
<root>
<items>
<item>English</item>
<item>No language found.</item>
<item>French</item>
</items>
</root>
Upvotes: 0
Reputation: 122414
It might be more efficient to use a key. You define a key using a top-level element outside your templates
<xsl:key name="langByCode" match="lang" use="@id" />
Then in the loop you can simply say
<xsl:when test="@lang"> <!-- then i test if the attr exists -->
<xsl:value-of select="key('langByCode', @lang)" />
</xsl:when>
But generally speaking a more natural XSLT approach to the whole thing would be to use template matching instead of for-each
and if
:
<xsl:template match="item[@lang]">
<xsl:value-of select="key('langByCode', @lang)" />
</xsl:template>
<xsl:template match="item">
<xsl:text>No language set</xsl:text>
</xsl:template>
With these templates in place you can then do <xsl:apply-templates select="/root/items/item" />
and it will pick the appropriate template for each item automatically. The rule is that it will use the most specific template, so the item[@lang]
one for those items that have a lang
attribute and the plain item
one for those that don't.
A third possibility is a little trick I learned on SO to put the whole if/else check into a single XPath expression
<xsl:value-of select="
substring(
concat('No language set', key('langByCode', @lang)),
1 + (15 * boolean(@lang))
)" />
The trick here is that boolean(@lang)
when treated as a number is 1
if the lang attribute exists and 0
if it doesn't. If there is a lang="EN"
, say, then we construct a string "No language setEnglish"
and then take the substring starting at the 16th character, which is "English"
. If there is no lang attribute we construct the string "No language set"
(because the string value of an empty node set is the empty string) and take the substring starting at the first character (i.e. the whole string).
You could use the same trick with other attributes, e.g. suppose we had an optional color attribute and wanted to say "No color specified"
if it is absent, you can do that with
<xsl:value-of select="substring(
concat('No color specified', @color),
1 + (18 * boolean(@color))
)" />
Upvotes: 7