Is it possible to specify a parameter of a named template as the match pattern in another template?
Here, if I try to call the 'excerpt' template and pass in an XPath as the 'path' param, I get an error:
<xsl:template name="excerpt">
<xsl:param name="path" select="''" />
<xsl:apply-templates select="$path" />
</xsl:template>
<xsl:template match="$path">
<article class="newsarticle">
<h2><xsl:value-of select="title" /></h2>
<xsl:copy-of select="excerpt/node()" />
</article>
</xsl:template>
I can accomplish it with <xsl:for-each>, but I wondered if there was a good solution using something similar to the approach above.
Edit: here's what I'm trying to accomplish, working with a <xsl:for-each>:
<xsl:template name="excerpt">
<xsl:param name="path" select="''" />
<xsl:for-each select="$path">
<article class="newsarticle">
<h2><xsl:value-of select="title" /></h2>
<xsl:copy-of select="excerpt/node()" />
</article>
</xsl:for-each>
</xsl:template>
Edit: an example of calling the template:
<xsl:call-template name="excerpt">
<xsl:with-param name="path" select="path/to/nodeset" />
</xsl:call-template>
Thanks for the additional information. One clarification to make here is that in that call-template there, you are passing a node-set, not a path. String values of paths are pretty much worthless in XSLT 1.0 without convoluted parsing logic or extension functions.
There is a way to do what you're trying to do, just in a slightly different way from what you envisioned. You just need to use a template with a generic match value and a mode value, like this.
<xsl:template name="excerpt">
<xsl:param name="items" select="''" />
<xsl:apply-templates select="$items" mode="excerptItem" />
</xsl:template>
<xsl:template match="node() | #*" mode="excerptItem">
<article class="newsarticle">
<h2>
<a href="{$root}/news/view/{title/#handle}">
<xsl:value-of select="title" />
</a>
</h2>
<xsl:copy-of select="excerpt/node()" />
</article>
</xsl:template>
But if the named template is only serving to invoke the match template, then you don't need the named template at all. You can just use the match template directly:
<xsl:apply-templates select="path/to/nodeset" mode="excerptItem" />
The purpose of the mode attribute is that when you specify mode in an apply-templates, the XSLT will only consider templates that also have that same mode value. So you could define two different templates that handled the same element in different ways:
<xsl:template match="Item" mode="header">
Item in header: <xsl:value-of select="." />
</xsl:template>
<xsl:template match="Item" mode="body">
Item in body: <xsl:value-of select="." />
</xsl:template>
Then you can specify which one you want to use at different times:
<div id="header">
<xsl:apply-templates match="/root/Items/Item" mode="header" />
</div>
<div id="body">
<xsl:apply-templates match="/root/Items/Item" mode="body" />
</div>
and the appropriate one would be used in each case. You can read more about modes here.
node() | #* is a generic XPath that matches any node or attribute, so if you use it in the match attribute of a template, you can make a template that will match almost anything you use in an apply-templates (as long as there isn't another template with higher precedence). Using that in combination with a mode allows you to make a template that you can invoke on any node and only at specific times when you want to. In your example, it looks like the element you will use with this template will always be the same, so it would probably be better practice to explicitly specify it:
<xsl:template match="ExportItem" mode="excerptItem">
Is it possible to get a parameter of a named template to act as the
match path in another template?
No, in XSLT 2.0 the match pattern of a template can only contain a variable reference as an argument to an id() function.
See the XSLT 2.0 W3C Specificification for the complete grammar of a Pattern
http://www.w3.org/TR/xslt20/#pattern-syntax
In XSLT 1.0 it is an error to have a variable reference anywhere within a match pattern.
Related
I started on XSL a few weeks ago, and I have developed a pattern for templates.
My templates will handle some of the content of the matched element, then pass off its children to other templates that might match them.
But I exclude elements that it has already dealt with, so as not to process the same elements again.
For example:
<xsl:template name="DEFINITION" match="DEFINITION">
<xsl:element name="body">
<xsl:attribute name="break">before</xsl:attribute>
<xsl:element name="defn">
<xsl:attribute name="id" />
<xsl:attribute name="scope" />
<xsl:value-of select="DEFINEDTERM" />
</xsl:element>
<xsl:apply-templates select="TEXT" />
</xsl:element>
<xsl:apply-templates select="*[not(self::DEFINEDTERM|self::TEXT)]" />
</xsl:template>
Is this the correct approach? Is there a better way?
It seems you don't need any xsl:element or xsl:attribute but could just use literal result elements and attribute value templates e.g. <body break="before"><xsl:apply-templates select="DEFINEDTERM, TEXT"/></body> (that is XSLT 2 or 3, for XSLT 1 use <body break="before"><xsl:apply-templates select="DEFINEDTERM"/> <xsl:apply-templates select="TEXT"/></body>) plus obviously a template
<xsl:template matched="DEFINEDTERM">
<defn id="" scope="">
<xsl:value-of select="."/>
</defn>
</xsl:template>
In XSLT 2/3 you can write <xsl:apply-templates select="*[not(self::DEFINEDTERM|self::TEXT)]" /> as e.g. <xsl:apply-templates select="* except (DEFINEDTERM, TEXT)"/>.
I'm fairly new to XSL, XPATH etc. Some of this code is mine, some are someone else's.
Problem: When the template below gets called with the templates I've outlined further below, all the xsl:text nodes after the if test is output as a string instead of an HTML node, and thus the icon is not rendered.
This question has to do with understanding the why? of what's going on. My exact question is at the bottom of this post.
So, I have a template that I call that generates SVG elements with a <use> element for use with an SVG sprite.
<xsl:template name="svg-link">
<xsl:param name="svg-id"/>
<xsl:param name="svg-class"/>
<xsl:param name="svg-title"/>
<span class="{$svg-class} svgstore svgstore--{$svg-loc}">
<svg>
<xsl:if test="$svg-title != ''">
<title><xsl:value-of select="$svg-title"/></title>
</xsl:if>
<xsl:text disable-output-escaping="yes"><use xlink:href="</xsl:text>
<xsl:value-of select="concat('#', $svg-loc)" />
<xsl:text disable-output-escaping="yes">"></use></xsl:text>
</svg>
</span>
</xsl:template>
All sorts of templates call/apply this template. There is one in particular that I'm having an issue with. We have two snippets implemented by the CMS that output the same markup, but the configurations for the snippets are implemented differently, i.e. Page Template A vs Page Template B. The snippet in question is made of multiple XSL templates. The templates are organized like so:
Snippet Template: entry point for snippet for all callers. Accepts a couple of params related to CSS classes. Creates a few wrapper elements for the snippet. Calls the following template.
"Model" Template: is a template that needs to be defined by each page template. As mentioned above, each page template uses a different approach to implementing configuration options for the snippet. The idea is to make the following template agnostic about how the snippet was configured in the first place because this template is responsible for knowing those details and passing it on to the following template.
Snippet Item Template: renders most of the markup for the snippet based on the info passed to it by the "Model" Template.
Here's some simplified pseudo-code demonstrating above:
<xsl:template name="snippet">
<xsl:param name="outer-classes"/>
<xsl:param name="inner-classes"/>
<xsl:variable name="items">
<xsl:call-template name="snippet-model"/>
</xsl:variable>
<!-- Render Snippet if it has content. -->
<xsl:if test="count( $items )">
<div class="{ $outer-classes }">
<div class="{ $inner-classes }">
<xsl:copy-of select="$items">
</div>
</div>
</xsl:if>
</xsl:template>
<!-- Placeholder. Defined by each page template. -->
<xsl:template name="snippet-model"/>
<xsl:template name="snippet-item">
<xsl:param name="a"/>
<xsl:param name="b"/>
<xsl:param name="b"/>
<div class="snippet-item {$a}">
<xsl:apply-templates select="$b"/>
<xsl:call-template name="svg-link">
<xsl:with-param name="svg-id">alpha</xsl:with-param>
<xsl:with-param name="svg-class">alpha</xsl:with-param>
<xsl:with-param name="svg-title">The Title</xsl:with-param>
</xsl:call-template>
</div>
</xsl:template>
And an example of how a page template uses the above:
<xsl:template match="table[#class = 'snippet-alpha']">
<xsl:call-template="snippet">
<xsl:with-param name="outer-classes">page-template-a other</xsl:with-param>
<xsl:with-param name="inner-classes">some-template-modifier</xsl:with-param>
</xsl:call-template>
</xsl:template>
<!-- Template definition of `snippet-model` template. -->
<xsl:template name="snippet-model">
<!-- Another page template might not use `tbody/tr` to loop over. -->
<xsl:for-each select="tbody/tr">
<xsl:call-template="snippet-item">
<xsl:with-param name="a" select="td[1]"/>
<xsl:with-param name="b" select="td[2]"/>
<xsl:with-param name="c" select="td[3]"/>
</xsl:call-template>
</xsl:for-each>
</xsl:template>
I've narrowed down my issue to likely be the xsl:variable capturing the results of xsl:call-template in the snippet template. And/Or the referencing of that variable later with xsl:copy-of.
What Have I Tried?
Below I have working and non-working solutions, all of which I do not fully grokk why they may or may not work.
Works: Adding xmlns:xlink="http://www.w3.org/1999/xlink" to xsl:stylesheet for the file that contains the svg-link template and then modding svg-link template, see code below this list.
Works: Instead of outputting the value of the xsl:variable that captures the results of xsl:call-template with xsl:copy-of. I replace xsl:copy-of with a second xsl:call-template that is identical to that of the call that was done inside the variable.
Does Not Work: Used xsl:sequence instead of xsl:copy-of.
Does Not Work: Tried data typing(?) the xsl:variable that captures the results of xsl:call-template with the as attribute. I.e. as="node()*".
<xsl:template name="svg-link">
<xsl:param name="svg-id"/>
<xsl:param name="svg-class"/>
<xsl:param name="svg-title"/>
<span class="{$svg-class} svgstore svgstore--{$svg-loc}">
<svg>
<xsl:if test="$svg-title != ''">
<title><xsl:value-of select="$svg-title"/></title>
</xsl:if>
<use xlink:href="{concat( '#', $svg-loc )}"></use>
</svg>
</span>
</xsl:template>
Question: Why are some of the contents of the svg-link template being output as a string (instead of HTML) based on how the result of a call to xsl:call-template is captured/called? As you can see, I have working and non-working solutions - I would like to know why. Thanks!
First of all, disable-output-escaping is an optional serialization feature. Additionally, the XSLT 2 or 3 specs spell out when it doesn't work at all, see https://www.w3.org/TR/xslt-30/#disable-output-escaping
If output escaping is disabled for an xsl:value-of or xsl:text
instruction evaluated when temporary output state is in effect, the
request to disable output escaping is ignored.
and https://www.w3.org/TR/xslt-30/#dt-temporary-output-state
xsl:variable, xsl:param, xsl:with-param, xsl:function, xsl:key,
xsl:sort, xsl:accumulator-rule, and xsl:merge-key always evaluate the
instructions in their contained sequence constructor in temporary
output state
So inside your xsl:variable any disable-output-escaping can't work.
The whole attempt to use it to try to construct an SVG use element is completely unnecessary, you can create any result elements as literal result elements e.g. <use xlink:href="{concat( '#', $svg-loc )}"></use> (with an appropriate XLink namespace declaration in scope for the attribute from that namespace), or, if you need to compute part of the name or namespace, using xsl:element.
The Problem
My parameters are coming up empty when passing through from a wildcarded templte match.
My xml Source :
<c:control name="table" flags="all-txt-align-top all-txt-unbold">
<div xmlns="http://www.w3.org/1999/xhtml">
<thead>
<tr>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td> </td>
</tr>
</tbody>
</c:control>
My XSL :
The initial c:control[#name='table'] match is to do with a wider piece of XSL architecture, and splitting out calls from a main template
<xsl:template match="c:control[#name='table']">
<xsl:call-template name="table" />
</xsl:template>
It then calls a named template in another file, which shouldn't change my starting reference - I should still be able to reference c:control[#name='table'] as if I was in the matched template.
<xsl:template name="table">
<xsl:variable name="all-txt-top">
<xsl:if test="contains(#flags,'all-txt-align-top')">true</xsl:if>
</xsl:variable>
<xsl:variable name="all-txt-unbold" select="contains(#flags,'all-txt-unbold')" />
<div xmlns="http://www.w3.org/1999/xhtml">
<table>
<xsl:apply-templates select="xhtml:*" mode="table">
<xsl:with-param name="all-txt-top" select="$all-txt-top" />
<xsl:with-param name="all-txt-unbold" select="$all-txt-unbold" />
</xsl:apply-templates>
</table>
</div>
</xsl:template>
If I get the value of all-txt-top in the above template, it works as expected.
However, trying to passing it through to the template below is failing - I'm not getting anything.
<xsl:template match="xhtml:thead|xhtml:tbody" mode="table">
<xsl:param name="all-txt-top" />
<xsl:param name="all-txt-unbold" />
<xsl:element name="{local-name()}">
<xsl:apply-templates select="*" mode="table" />
</xsl:element>
</xsl:template>
Even if i try passing a simple string through as a parameter - it doesn't make it to the xhtml:thead template.
Not sure where I'm going wrong... Any help would be much appreciated.
In the sample code you have shown us, you call the named table template after the c:control element is matched
<xsl:template match="c:control[#name='table']">
<xsl:call-template name="table" />
</xsl:template>
This means within the table template, the current context element is c:control. However in your sample XML, the only child of the c:control is a div element. Therefore, when you do your apply-templates....
<xsl:apply-templates select="xhtml:*" mode="table">
... it will will look for a template that matches xhtml:div at this point. If you have no such template, the default template match will kick in, which will just ignore the element and process its children. This will not pass on any parameters though, and so you template that matches xhtml:thead will not have any parameters values.
One solution would be to have a template to specifically match the xhtml:div element, and pass on the attributes
<xsl:template match="xhtml:div" mode="table">
<xsl:param name="all-txt-top"/>
<xsl:param name="all-txt-unbold"/>
<xsl:apply-templates select="xhtml:*" mode="table">
<xsl:with-param name="all-txt-top" select="$all-txt-top"/>
<xsl:with-param name="all-txt-unbold" select="$all-txt-unbold"/>
</xsl:apply-templates>
</xsl:template>
In fact, you could change the template match here to just "xhtml:*" to make it more generic if you want to cope with more elements.
As I have understood the match atribute of a template tag, it defines what part of the xml tree that will be enclosed in the template.
However ther seem to be some exceptions, I have a working peace of code, lite this:
<xsl:template match="/root/content">
<xsl:for-each select="/root/meta/errors/error">
<p>
<strong>Error:</strong> <xsl:value-of select="message" /> (<xsl:value-of select="data/param" />)<br />
<xsl:for-each select="data/option">
<xsl:value-of select="." /><br />
</xsl:for-each>
</p>
<br /><br />
</xsl:for-each>
</xsl:template>
But when I try to add a conditional like this:
<xsl:template match="/root/content">
<xsl:if test="not(/root/meta/error/errors/data/param)"-->
<xsl:for-each select="/root/meta/errors/error">
<p>
<strong>Error:</strong> <xsl:value-of select="message" /> (<xsl:value-of select="data/param" />)<br />
<xsl:for-each select="data/option">
<xsl:value-of select="." /><br />
</xsl:for-each>
</p>
<br /><br />
</xsl:for-each>
<xsl:call-template name="trip_form">
<xsl:with-param name="type" select="'driver'" />
<xsl:with-param name="size" select="'savetrip'" />
</xsl:call-template>
</xsl:if>
</xsl:template>
It doesn't work any more, why, and how can I make it work again?
Attribute matches are applied when you ask for it (you are pulling with complex and unneeded for-each resulting in no attribute matching at all), otherwise they are ignored. That's why the copy idiom is used with specific attribute apply-templates:
<xsl:template match="node() | #*">
<xsl:copy>
<xsl:apply-templates select="* | #*" />
</xsl:copy>
</xsl:template>
When it comes to the order in which they are applied, the order is the document order, which means: after the element is applied, its attributes will be applied (in undetermined order) and then the element's children are applied. Attributes never have children and their parent is the containing element.
"it defines what part of the xml tree that will be enclosed in the template."
No. It is called when the processor encounters input that matches the specification, or when you specifically apply this input by using xsl:apply-templates. Your code should not use xsl:for-each, that's rarely needed. Instead, use xsl:apply-templates. This will also give you the possibility to match the attributes when you like.
Normally, you don't (need to) specify the parent in the match-attribute of apply-templates. And you surely don't write down the whole path inside the templates each time, that will wreak havoc on usability of your stylesheet... Try something like this instead and have a look at some XSL tutorials on the net (w3schools provides some basic information and Tennison's book is next to invaluable to learn about this variant of functional programming):
<xsl:template match="/">
<xsl:apply-templates select="/root/content" />
</xsl:template>
<xsl:template match="content">
<xsl:apply-templates select="errors/error" />
</xsl:template>
<xsl:template match="error">
<p>
<strong>Error:</strong>
<xsl:value-of select="message" />
(<xsl:value-of select="data/param" />)
<br />
<xsl:apply-templates select="data/option" />
</p>
<br /><br />
</xsl:template>
<xsl:template match="option">
<xsl:value-of select="." /><br />
</xsl:template>
"It doesn't work any more, why, and how can I make it work again?"
Because your if-statement is probably always true (or always false). Reason: if anywhere in your document the XPath is correct, it will always be false, if it is never correct, it will always be true. Using xsl:if with an XPath that starts in the root will, for the live of the transformation, always yield the same result. Not sure what you are after, so I cannot really help you further here. Normally, instead of xsl:if, we tend to use a matching template (again, yes, I know it gets boring ;).
Note: you ask something about attributes in your question, this I tried to answer in the opening paragraph (before this edit). However, there's nothing about attributes inside your code, so I don't know how to really help you.
Note on the note: LarsH suggests that you perhaps mean to ask about the match-attribute inside xsl:template. If so, the answer lies in the text above anywhere, where I talk about apply-templates and the sort. In short: the input document is processed, node by node, possibly directed by xsl:apply-templates, and it tries to find a matching template for each node it's currently at. That's all there is to it.
I have the following template
<h2>one</h2>
<xsl:apply-templates select="one"/>
<h2>two</h2>
<xsl:apply-templates select="two"/>
<h2>three</h2>
<xsl:apply-templates select="three"/>
I would like to only display the headers (one,two,three) if there is at least one member of the corresponding template. How do I check for this?
<xsl:if test="one">
<h2>one</h2>
<xsl:apply-templates select="one"/>
</xsl:if>
<!-- etc -->
Alternatively, you could create a named template,
<xsl:template name="WriteWithHeader">
<xsl:param name="header"/>
<xsl:param name="data"/>
<xsl:if test="$data">
<h2><xsl:value-of select="$header"/></h2>
<xsl:apply-templates select="$data"/>
</xsl:if>
</xsl:template>
and then call as:
<xsl:call-template name="WriteWithHeader">
<xsl:with-param name="header" select="'one'"/>
<xsl:with-param name="data" select="one"/>
</xsl:call-template>
But to be honest, that looks like more work to me... only useful if drawing a header is complex... for a simple <h2>...</h2> I'd be tempted to leave it inline.
If the header title is always the node name, you could simplifiy the template by removing the "$header" arg, and use instead:
<xsl:value-of select="name($header[1])"/>
I like to exercise the functional aspects of XSL which lead me to the following implementation:
<?xml version="1.0" encoding="UTF-8"?>
<!-- test data inlined -->
<test>
<one>Content 1</one>
<two>Content 2</two>
<three>Content 3</three>
<four/>
<special>I'm special!</special>
</test>
<!-- any root since take test content from stylesheet -->
<xsl:template match="/">
<html>
<head>
<title>Header/Content Widget</title>
</head>
<body>
<xsl:apply-templates select="document('')//test/*" mode="header-content-widget"/>
</body>
</html>
</xsl:template>
<!-- default action for header-content -widget is apply header then content views -->
<xsl:template match="*" mode="header-content-widget">
<xsl:apply-templates select="." mode="header-view"/>
<xsl:apply-templates select="." mode="content-view"/>
</xsl:template>
<!-- default header-view places element name in <h2> tag -->
<xsl:template match="*" mode="header-view">
<h2><xsl:value-of select="name()"/></h2>
</xsl:template>
<!-- default header-view when no text content is no-op -->
<xsl:template match="*[not(text())]" mode="header-view"/>
<!-- default content-view is to apply-templates -->
<xsl:template match="*" mode="content-view">
<xsl:apply-templates/>
</xsl:template>
<!-- special content handling -->
<xsl:template match="special" mode="content-view">
<strong><xsl:apply-templates/></strong>
</xsl:template>
Once in the body all elements contained in the test element have header-content-widget applied (in document order).
The default header-content-widget template (matching "*") first applies a header-view then applies a content-view to the current element.
The default header-view template places the current element's name in the h2 tag. The default content-view applies generic processing rules.
When there is no content as judged by the [not(text())] predicate no output for the element occurs.
One off special cases are easily handled.