With the following XML
<para>Refer to Table 3 and Figure <grphcref refid="apm00-02-02-000018" shownow="0">6</grphcref>
for the door dimensions and clearances.</para>
and this XSL:
<xsl:template match="prcitem">
<xsl:for-each select="para">
<p>
<xsl:value-of select="." />
<xsl:apply-templates select="./grphcref" />
</p>
</xsl:for-each>
<xsl:apply-templates select="grphcref" />
<xsl:apply-templates select="table" />
<xsl:apply-templates select="unlist" />
</xsl:template>
<xsl:template match="grphcref">
<xsl:variable name="gotoimg" select="concat('#',#refid)"/>
<a href="{$gotoimg}" >
<xsl:value-of select="." /> - <xsl:value-of select="#refid" /> </a>
</xsl:template>
I get:
<p>Refer to Table 3 and Figure 6 for the door dimensions and clearances.
6 - apm00-02-02-000018
when I expected:
<p>Refer to Table 3 and Figure 6 - apm00-02-02-000018
for the door dimensions and clearances.
Can anyone offer guidance as to where I went wrong?
thx
If a para element is supposed to be transformed to a p element then in my view the "natural" way in XSLT is a template
<xsl:template match="para">
<p>
<xsl:apply-templates/>
</p>
</xsl:template>
If any other elements need special treatment add a template e.g.
<xsl:template match="grphcref">
<xsl:variable name="gotoimg" select="concat('#',#refid)"/>
<a href="{$gotoimg}" >
<xsl:value-of select="." /> - <xsl:value-of select="#refid" /> </a>
</xsl:template>
Text nodes are copied through to the result by the built-in templates.
The input sample doesn't explain why you mix template matching and for-each and why or whether you need xsl:apply-templates with any particularly selected nodes; as long as the input order should be preserved a simple processing of all child nodes with <xsl:apply-templates/> should suffice.
Related
I currently have something similar to the following XML:
<div class="newsFeed">
<div class="newsItem"><news position="3"/></div>
<categoryFilter dayFilter="4">
<div class="newsItem"><news position="2"/></div>
</categoryFilter>
</div>
I need to copy the XML, and output the nth news item on the node. Futhermore, I need to be able to filter that news. For this example, lets construct my news as follows:
<xsl:variable name="news">
<xsl:for-each select="1 to 30">
<item>
<day><xsl:value-of select=". mod 4" /></day>
<content>Content: <xsl:value-of select="." /></content>
</item>
</xsl:for-each>
</xsl:variable>
I actually use document() and use the for-each to sort it, but I'm trying to keep it succinct. This would mean that my output XML would be something like the following:
<div class="newsFeed">
<div class="newsItem">Content: 3</div>
<div class="newsItem">Content: 8</div>
</div>
The reason the second one is 8 is because the categoryFilter filters out every <item> where the day isn't 4 (which happens to be the 4th, 8th, 12th, and so on), and then we select the second one.
The XSLT to produce the above is as follows:
XSLT:
<xsl:template match="news">
<xsl:param name="items" select="$news" />
<xsl:variable name="position" select="#position" />
<xsl:copy-of select="$items/item[position()=$position]/content" />
</xsl:template>
<xsl:template match="categoryFilter">
<xsl:param name="items" select="$news" />
<xsl:variable name="day" select="#dayFilter" />
<xsl:variable name="filteredItems">
<xsl:for-each select="$items/item[day=$day]">
<xsl:copy-of select="." />
</xsl:for-each>
</xsl:variable>
<xsl:apply-templates>
<xsl:with-param name="items" select="$filteredItems">
</xsl:apply-templates>
</xsl:template>
My problem lies with the <for-each>. It seems silly that I have to use a for-each to filter out the <item> nodes, but I can't find a better way. Simply doing a <xsl:variable select="$items/item[day=$day]"> changes the structure, and makes it so that the <xsl:template match="news"> doesn't work.
Is there a way to filter out child nodes without using a for-each? I am using <xsl:stylesheet version="3.0">
Instead of doing this...
<xsl:variable name="filteredItems">
<xsl:for-each select="$items/item[day=$day]">
<xsl:copy-of select="." />
</xsl:for-each>
</xsl:variable>
... you could use a sequence. This will select the actual items, as opposed to create copies of them
<xsl:variable name="filteredItems">
<xsl:sequence select="$items/item[day=$day]" />
</xsl:variable>
This was, doing $filteredItems/item will still work.
Alternatively, you could take the opposite approach, and do away with the need to specify /item in all the expressions.
First, define your news variable like so:
<xsl:variable name="news" as="element()*">
This means you can write the expression that uses it like so:
<xsl:copy-of select="$news[position()=$position]/content" />
And similarly for filteredItems....
<xsl:variable name="filteredItems" select="$items[day=$day]" />
I've got the following template (excerpt):
<xsl:template match="section[#visible='1']">
<dl>
<dt><xsl:call-template name="content"/></dt>
<xsl:apply-templates select="(page|file)[#visible='1']" />
<xsl:apply-templates select="section[#visible=1]" mode="child" />
</dl>
</xsl:template>
<xsl:template match="section[#visible='1']" mode="child">
<dd><xsl:apply-templates select="." /></dd>
</xsl:template>
My problem is with the two apply-template elements at the end. The source XML elements (page, file, section,...) all have a pos attribute containing a number, which defines when they should be added to the output. But the way I have it currently, prevents a section with a lower pos value to be displayed before a page element with a higher position for example.
How can I achieve that? I've tried this:
<xsl:template match="section[#visible='1']">
<dl>
<dt><xsl:call-template name="content"/></dt>
<xsl:call-template name="kids"/>
</dl>
</xsl:template>
<xsl:template name="kids">
<xsl:for-each select="node()">
<xsl:sort select="#pos"/>
<!-- what would go here? -->
</xsl:for-each>
</xsl:template>
But I don't know what to put into the for-each loop. I could just duplicate the existing 2 templates, slap a name on them, and then call them with the current node as parameter, but that wouldn't be DRY. There must be a better way.
Have you tried this?
<xsl:template match="section[#visible='1']">
<dl>
<dt><xsl:call-template name="content"/></dt>
<xsl:apply-templates select="(page|file|section)[#visible='1']" mode="m">
<xsl:sort select="#pos"/>
</xsl:apply-templates>
</dl>
</xsl:template>
<xsl:template match="*" mode="m">
<xsl:apply-templates select="."/>
</xsl:template>
<xsl:template match="section" mode="m">
<xsl:apply-templates select="." mode="child"/>
</xsl:template>
I need to process attributes (of any node that has them) in a particular order. For example:
<test>
<event
era ="modern"
year ="1996"
quarter = "first"
day = "13"
month= "January"
bcad ="ad"
hour ="18"
minute = "23"
>The big game began.</event>
<happening
era ="modern"
day = "18"
bcad ="ad"
month= "February"
hour ="19"
minute = "24"
>The big game ended.</happening>
<other>Before time existed.</other>
</test>
This
<xsl:template match="test//*">
<div>
<xsl:apply-templates select="#*" />
<xsl:apply-templates />
</div>
</xsl:template>
<xsl:template match="#*">
<span class="{name()}">
<xsl:value-of select="."/>
</span>
</xsl:template>
would format things as I need them. That is, I'd get
<div><span class="era">modern</span>
<span class="year">1996</span>
<span class="quarter">first</span>
<span class="day">13</span>
<span class="month">January</span>
<span class="bcad">ad</span>
<span class="hour">18</span>
<span class="minute">23</span>The big game began.</div>
<div><span class="era">modern</span>
<span class="day">18</span>
<span class="bcad">ad</span>
<span class="month">February</span>
<span class="hour">19</span>
<span class="minute">24</span>The big game ended.</div>
<div>Before time existed.</div>
(though without the newlines I've added here for legibility).
But the order of the attributes wouldn't (necessarily) be right.
To fix that, I could change <xsl:apply-templates select="#*" /> to <xsl:call-template name="atts" /> and add a template that applies templates in the needed order, like this:
<xsl:template match="test//*">
<div>
<xsl:call-template name="atts" />
<xsl:apply-templates />
</div>
</xsl:template>
<xsl:template name="atts">
<xsl:apply-templates select="#era" />
<xsl:apply-templates select="#bcad" />
<xsl:apply-templates select="#year" />
<xsl:apply-templates select="#quarter" />
<xsl:apply-templates select="#month" />
<xsl:apply-templates select="#day" />
<xsl:apply-templates select="#hour" />
<xsl:apply-templates select="#minute" />
</xsl:template>
<xsl:template match="#*">
<span class="{name()}">
<xsl:value-of select="."/>
</span>
</xsl:template>
Is this the best-practices way to process attributes in a specified order? I keep wondering if there is a way that uses keys or a global variable.
I need to use XSLT 1.0 and in the real case, there are several dozen attributes, not just eight.
In contrast to elements for example, the order of attributes is not significant in XML, i.e. XPath and XSLT may process them in any order. Thus, the only way to force a given order, is to specify it somehow. One way is to call them explicitly as in your last code example. You can also extract all attribute names and store them in a separate XML file, e.g. something like
<attributes>
<attribute>era</attribute>
<attribute>year</attribute>
<attribute>month</attribute>
...
<attributes>
Now you can load these elements with the document() function and iterate over all attribute elements:
<xsl:variable name="attributes" select="document('attributes.xml')//attribute"/>
...
<xsl:template match="*">
<xsl:variable name="self" select="."/>
<xsl:for-each select="$attributes">
<xsl:apply-templates select="$self/#*[name()=current()]"/>
</xsl:for-each>
</xsl:template>
I've got the following XSLT code which lists out the folders and their file items from a specified node.
This all works fine but I'd like to parameterise the page and optionally filter its output by a tag value.
Being an XLST numpty I'm stumped with the syntax for the conditional I should be putting in under the <xsl:when test="$tag"> clause - can someone please help ?
<xsl:variable name="tag" select="umbraco.library:Request('tag')" />
<xsl:template match="/">
<!-- Root folder in Media that holds the folders to output -->
<xsl:variable name="mediaRootFolderId" select="5948" />
<!-- Pass in true() to get XML for all nodes below -->
<xsl:variable name="mediaRootNode" select="umbraco.library:GetMedia($mediaRootFolderId, true())" />
<xsl:choose>
<xsl:when test="$tag">
</xsl:when>
<xsl:otherwise>
<!-- If we didn't get an error, output Folder elements that contain Image elements -->
<xsl:apply-templates select="$mediaRootNode[not(error)]/Folder[File]" >
<xsl:sort select="#nodeName"/>
</xsl:apply-templates>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- Template for folders -->
<xsl:template match="Folder">
<div class="folder">
<h2>Folder: <xsl:value-of select="#nodeName" /></h2>
<div class="images">
<xsl:apply-templates select="File">
<xsl:sort select="#nodeName"/>
</xsl:apply-templates>
</div>
</div>
</xsl:template>
<!-- Template for files -->
<xsl:template match="File">
File: <a href="{umbracoFile}" alt="{#nodeName}" ><xsl:value-of select="#nodeName" /></a> <br/>
</xsl:template>
Instead of the long <xsl:choose> instruction, use:
<xsl:apply-templates select=
"$mediaRootNode[not($tag)][not(error)]
/Folder[File]" >
Explanation: For the XPath expression in the select attribute above to select a non-empty set of nodes it is necessary that boolean($tag) is true(). Thus the above single <xsl:apply-templates> instruction is equivalent to the long <xsl:choose> in the question.
you can test if $tag is set like this.
<xsl:param name="tag">
<xsl:message terminate="yes">
$tag has not been set
</xsl:message>
</xsl:param>
This isn't standard though, it'll work on most XSLT processors though.
If you wanted to be absolutely save, you could set the value to an illegal value (such as 1 div 0) and test for it in the body of the template:
<xsl:param name="tag" select="1 div 0" />
<xsl:if test="$tag = 1 div 0">
<xsl:message terminate="yes">
$tag has not been set, or has been set to Infinity, which is invalid.
</xsl:message>
</xsl:if>
Source: O'Reilly XSLT Cookbook
I cant really formulate that better, so I'll go with an example instead:
XML:
<root>
<foo>
<bar id="1">sdf</bar>
<bar id="2">sdooo</bar
</foo>
<feng>
<heppa id="4">hihi</heppa>
<heppa id="2">sseeapeea</heppa>
<heppa id="1">....</heppa>
</feng>
</root>
XSLT:
<xsl:for-each select="/root/foo/bar">
<p>
<xsl:value-of select="." />: <xsl:value-of select="/root/feng/heppa[#id = #id]" />
</p>
</xsl:for-each>
Desired output:
<p>sdf: ....</p>
<p>sdooo: sseeapeea</p>
Actual output:
<p>sdf: hihi</p>
<p>sdooo: hihi</p>
For selecting nodes with XPath 1.0 only, you need to do a node set comparison:
/root/feng/heppa[#id=/root/foo/bar/#id]
Of course, this has NxM complexity (as the others XSLT solutions)
With XSLT 1.0 you should use keys because there are cross references:
<xsl:key name="kBarById" select="bar" use="#id"/>
<xsl:template match="/root/feng/heppa[key('kBarById',#id)]">
<p>
<xsl:value-of select="concat(key('kBarById',#id),': ',.)"/>
</p>
</xsl:template>
I assume you mean /root/foo/bar since /root/foo elements don't have id.
You're comparing the #id with itself, so of course it's true for the first node examined. You can use current() to reference the current node in an expression:
<xsl:for-each select="/root/foo/bar">
<p>
<xsl:value-of select="." />: <xsl:value-of select="/root/feng/heppa[#id = current()/#id]" />
</p>
</xsl:for-each>
Another solution is to read the id attribute into a variable.
<xsl:for-each select="/root/foo/bar">
<xsl:variable name="id" select="#id"/>
<p>
<xsl:value-of select="." />: <xsl:value-of select="/root/feng/heppa[#id = $id]" />
</p>
</xsl:for-each>
This might be handier, if your real use case is more complicated and you need to use the value of the id multiple times in this for-each section.