making compound xpath statement for xslt: match - xslt

Here's a generic XSLT 1.0 question which I need to know to write an XSLT statement for processing docbook xml files. In my docbook XML, I'm trying to write a compound xpath statement in XSLT 1.0 that says, hardcode a new attribute "class = "play" for p tags in html output.
I want this action to be done for every <para> tag which does NOT have these attributes
role="normal-play-paragraph" AND
role ="no-indent" AND
"role="line-verse"
Here is my XML source:
<chapter xmlns="http://docbook.org/ns/docbook"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="5.0" xml:id="play">
<title> Hamlet </title>
<para role="no-indent"> SPHINX. Do you think about it very much?</para>
<para role="normal-play-para"> INTERVIEWER. I do so say. </para>
<para>SPHINX. Hello </para>
<para> INTERVIEWER. dddddWhy I do so say. </para>
<para> SPHINX. Yes. </para>
<para role="line-verse"> Cosmologists have theorized or guessed</para>
</chapter>
I want the HTML output to look like this after Docbook XSLT processes it:
<html>
<body>
<p class="no-indent">SPHINX. Do you think about it very much? much. </p>
<p class="normal-play-para"> INTERVIEWER. I do so say. </p>
<p class="play">SPHINX. Hello </p>
<p class="play">INTERVIEWER. dddddWhy I do so say. </p>
<p class="play">SPHINX. Yes. </p>
<p class="line-verse"> Cosmologists have theorized or guessed</p>
</body>
<html>
The docbook xslt has 2 mechanisms at work which you don't really need to know about.
First, in <para role=""> elements, the value of role is changed into class of p. This is the default behavior.
Second, I'm using a special mode to hardcode a "class='play'" into p tags.
<xsl:template match="d:chapter[#xml:id = 'play']/d:para" mode="class.attribute" >
<xsl:param name="class" select="local-name(.)"/>
<xsl:attribute name="class">play</xsl:attribute>
</xsl:template>
However, I want class="play" to be hardcoded only when there are other attributes & values NOT present. I can modify the above statement to exclude all para tags with the attribute role="line-verse" :
<xsl:template match="d:chapter[#xml:id = 'play']/d:para[#role != 'line-verse']" mode="class.attribute" >
<xsl:param name="class" select="local-name(.)"/>
<xsl:attribute name="class">play</xsl:attribute>
</xsl:template>
But I need more than that. I want to exclude not only role= "line-verse," but also role="no-indent" and role="normal-play-para".
So I have to change the value of the xpath statement in the match attribute so that it excludes three attribute values. I haven't the foggiest idea how to do that. Does anybody know? Thanks.
Update about Answer:
First, I want to thank all of you for taking the time to understand my question and formulate an answer. I should mention that I am still a novice on this stuff, and also, my question was a little unfair because I am using some sophisticated/complicated Docbook XSL. Therefore I need an answer that doesn't cause collisions with the Docbook XSL stylesheets. Also, I realize that you wrote transformations that may be perfectly valid answers in generating html output if I were not also importing the docbook xsl.
The answer which I chose as "best" here may not be the most elegant, but simply the one that worked for me in the case when I am importing the epub3 docbook-ns stylesheets. So Mr. Rishe's one line answer actually does exactly I need it to do even if it isn't as elegant.
I really don't know what's going on in this customization which I started out with:
<xsl:template match="d:chapter[#xml:id = 'play']/d:para" mode="class.attribute" >
<xsl:param name="class" select="local-name(.)"/>
<xsl:attribute name="class">play</xsl:attribute>
</xsl:template>
What I do know is that it's invoking a <xsl:template name="generate.class.attribute"> which is found here. http://50.56.245.89/xsl-ns/xhtml-1_1/html.xsl
Another thing. Dimitre Novatchev's 2 answers looks as though they would work. By the way, you forgot to include the <xsl:param name="class" select="local-name(.)"/> statement -- which is easily fixed -- and that solution works.
However, Dimitre, I have another question. The second answer you gave used variables, which looks simple and functional. If I try it, my Saxon 6.5 parser gives a validation error. (E [Saxon6.5.5] The match pattern in xsl:template may not contain references to variables). Maybe it's something simple like a typo. But is it possible that variables are not allowed in XSLT 1.0 template matches?

Could you give this a try:
<!-- Special handling for paras with one of the three roles -->
<xsl:template
match="d:chapter[#xml:id = 'play']/d:para[#role = 'line-verse' or #role = 'normal-play-para' or #role - 'line-indent']"
mode="class.attribute" >
<xsl:attribute name="class">
<xsl:value-of select="#role" />
</xsl:attribute>
</xsl:template>
<!-- Other paras get the default class "play" -->
<xsl:template match="d:chapter[#xml:id = 'play']/d:para" mode="class.attribute">
<xsl:attribute name="class">play</xsl:attribute>
</xsl:template>
One step further would be to have the <xsl:attribute> in the template that's calling these templates, and just have the needed value in the class.attribute templates themselves. Something like this:
<xsl:template match="d:chapter[#xml:id = 'play']/d:para">
<p>
<xsl:attribute name="class">
<xsl:apply-templates select="." mode="class.attribute" />
</xsl:attribute>
...
</p>
</xsl:template>
<!-- Special handling for paras with one of the three roles -->
<xsl:template
match="d:chapter[#xml:id = 'play']/d:para[#role = 'line-verse' or #role = 'normal-play-para' or #role - 'line-indent']"
mode="class.attribute" >
<xsl:value-of select="#role" />
</xsl:template>
<!-- Other paras get the default class "play" -->
<xsl:template match="d:chapter[#xml:id = 'play']/d:para" mode="class.attribute">
<xsl:text>play</xsl:text>
</xsl:template>
To specifically answer your original question, if you really needed a template that specifically matches paras that don't have one of those #role values, you could match on this XPath:
d:chapter[#xml:id = 'play']/d:para[not(#role = 'line-verse' or #role = 'normal-play-para' or #role - 'line-indent')]
But I think the approach I've presented above (treat paras those roles as the special case, and treat everything else as the default) is the better way to go.

One possible solution is:
<xsl:template mode="class.attribute" match=
"d:chapter[#xml:id = 'play']
/d:para[not(#role = 'line-verse'
or #role = 'no-indent'
or #role = 'normal-play-para'
)]" >
<xsl:attribute name="class">play</xsl:attribute>
</xsl:template>
However, I would use a more flexible and extensible solution, that allows easy modification of the "non-play" values:
<xsl:param name="pNonPlayVals">
<val>line-verse</val>
<val>no-indent</val>
<val>normal-play-para</val>
</xsl:param>
<xsl:template mode="class.attribute" match=
"d:chapter[#xml:id = 'play']/d:para
[not(#role = document('')/*/xsl:param[#name='pNonPlayVals']/val)]" >
<xsl:attribute name="class">play</xsl:attribute>
</xsl:template>

Related

Attempts to use following-sibling to convert

I try to convert my old html by xslt-script to my new xml stucture.
I have a Problem to converting the folowing source to my needed xml structure.
Source
<p>
<a class="DropDown">Example Text</a>
</p>
<div class="collapsed">
<table>..</table>
<p>..</p>
</div>
xml structure
<lq>
<p>Example Text</p>
<table>..</table>
<p>..</p>
</lp>
I tried the following xls, but the div class="collapsed" is not adopted into the lp tag.
<xsl:template match="p/a[#class='DropDown']">
<lp>
<p><xsl:apply-templates select="text()"/></p>
<xsl:if test="/p/a/following-sibling::*[1][self::div]">
<xsl:apply-templates select="*|text()"/>
</xsl:if>
</lp>
</xsl:template>
Can anyone tell me what I did wrong ore where the mistake is?
Thanks much
IMHO, you want to do:
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="p[a/#class='DropDown']">
<lp>
<p>
<xsl:value-of select="a"/>
</p>
<xsl:copy-of select="following-sibling::*[1][self::div]/node()"/>
</lp>
</xsl:template>
<xsl:template match="div[preceding-sibling::*[1][self::p/a/#class='DropDown']]"/>
As for your mistake:
You are testing the existence of some p that is the root element
and contains an a whose following sibling is div. None of these are true in the given example;
xsl:if does not change the context: your <xsl:apply-templates
select="*|text()"/> applies templates to the child nodes of the
current a;
Presumably you don't want the div to appear again in the original place -
so if you have another template to suppress it, you cannot use
<xsl:apply-templates> to insert it at the place you do want it -
at least not without using another mode.

Why is .[expression] invalid XPath syntax [duplicate]

(I'm posting this self-answered question because the typically offered solution to this issue is needlessly verbose and I'd like to set the record straight. I couldn't find an existing SO question for this, but if there is one, please close this as a duplicate.)
I am looking for a way to perform an XPath selection to select the current node only if it matches a certain condition. This would be useful, for example, when I want to conditionally apply an XSLT template to the current node:
<xsl:template match="Device">
<div>
<h2><xsl:value-of select="Name" /></h2>
<xsl:apply-templates select="???[Featured = 'true']" mode="featured" />
<p><xsl:value-of select="Description" /></p>
</div>
</xsl:template>
<xsl:template match="Book">
<div>
<h2><xsl:value-of select="Title" /></h2>
<xsl:apply-templates select="???[FeaturedBook = 'true']" mode="featured" />
<h3><xsl:value-of select="Author" /></h3>
<p><xsl:value-of select="Summary" /></p>
</div>
</xsl:template>
<xsl:template match="node()" mode="featured">
<p class='featured-notice'>This is a featured item!
Buy now to get a 15% discount.
</p>
</xsl:template>
I have tried using .[Featured = 'true'], but I get a syntax error. How can I do this?
I'm not going to add an input and output here since they are tangential to the question and would make it exceedingly long, but if you want to see what I have in mind, I have placed them here: input, output.
The syntax .[predicate] is not allowed in XPath 1.0 on account of the syntax rules (see the end of this post for the gritty details).
100% of the advice I have found says that the only option is to use self::node() for this:
self::node()[Featured = 'true']
This XPath tester is even specifically designed to tell users to use self::node()[predicate] if they try to use .[predicate], but this is not the only option.
A valid and more concise option is to just wrap the abbreviated step in parentheses:
(.)[Featured = 'true']
This is perfectly valid by XPath 1.0 syntax rules (and in my opinion, a lot clearer).
You can also use this approach with the .. abbreviated step, even going up multiple levels:
Select grandfather node if it is featured
../..[Featured = 'true'] - Not valid
../../../*[Featured = 'true'] - Valid, but not accurate
../../self::node()[Featured = 'true'] - Valid, but verbose
(../..)[Featured = 'true'] - Valid
Addendum: Why it's not possible to use .[predicate] in XPath 1.0
The following is the definition of a "step" in XPath 1.0 (basically, the pieces of an XPath node selection expression separated by slashes are called "steps"):
[4] Step ::= AxisSpecifier NodeTest Predicate* | AbbreviatedStep
This means that one step consists of one of two possible options:
An axis specifier (which can be an empty string), followed by a node test, followed by 0 or more predicates
An abbreviated step: . or ..
There is no option to have an abbreviated step followed by predicates.
<xsl:template match="Device">
<div>
<h2><xsl:value-of select="Name" /></h2>
<xsl:apply-templates select="Featured[. = 'true']" />
<p><xsl:value-of select="Description" /></p>
</div>
</xsl:template>
<xsl:template match="Book">
<div>
<h2><xsl:value-of select="Title" /></h2>
<xsl:apply-templates select="FeaturedBook[. = 'true']" />
<h3><xsl:value-of select="Author" /></h3>
<p><xsl:value-of select="Summary" /></p>
</div>
</xsl:template>
<xsl:template match="FeaturedBook|Featured">
<p class='featured-notice'>This is a featured item!
Buy now to get a 15% discount.
</p>
</xsl:template>

Apply a predicate to the current node

(I'm posting this self-answered question because the typically offered solution to this issue is needlessly verbose and I'd like to set the record straight. I couldn't find an existing SO question for this, but if there is one, please close this as a duplicate.)
I am looking for a way to perform an XPath selection to select the current node only if it matches a certain condition. This would be useful, for example, when I want to conditionally apply an XSLT template to the current node:
<xsl:template match="Device">
<div>
<h2><xsl:value-of select="Name" /></h2>
<xsl:apply-templates select="???[Featured = 'true']" mode="featured" />
<p><xsl:value-of select="Description" /></p>
</div>
</xsl:template>
<xsl:template match="Book">
<div>
<h2><xsl:value-of select="Title" /></h2>
<xsl:apply-templates select="???[FeaturedBook = 'true']" mode="featured" />
<h3><xsl:value-of select="Author" /></h3>
<p><xsl:value-of select="Summary" /></p>
</div>
</xsl:template>
<xsl:template match="node()" mode="featured">
<p class='featured-notice'>This is a featured item!
Buy now to get a 15% discount.
</p>
</xsl:template>
I have tried using .[Featured = 'true'], but I get a syntax error. How can I do this?
I'm not going to add an input and output here since they are tangential to the question and would make it exceedingly long, but if you want to see what I have in mind, I have placed them here: input, output.
The syntax .[predicate] is not allowed in XPath 1.0 on account of the syntax rules (see the end of this post for the gritty details).
100% of the advice I have found says that the only option is to use self::node() for this:
self::node()[Featured = 'true']
This XPath tester is even specifically designed to tell users to use self::node()[predicate] if they try to use .[predicate], but this is not the only option.
A valid and more concise option is to just wrap the abbreviated step in parentheses:
(.)[Featured = 'true']
This is perfectly valid by XPath 1.0 syntax rules (and in my opinion, a lot clearer).
You can also use this approach with the .. abbreviated step, even going up multiple levels:
Select grandfather node if it is featured
../..[Featured = 'true'] - Not valid
../../../*[Featured = 'true'] - Valid, but not accurate
../../self::node()[Featured = 'true'] - Valid, but verbose
(../..)[Featured = 'true'] - Valid
Addendum: Why it's not possible to use .[predicate] in XPath 1.0
The following is the definition of a "step" in XPath 1.0 (basically, the pieces of an XPath node selection expression separated by slashes are called "steps"):
[4] Step ::= AxisSpecifier NodeTest Predicate* | AbbreviatedStep
This means that one step consists of one of two possible options:
An axis specifier (which can be an empty string), followed by a node test, followed by 0 or more predicates
An abbreviated step: . or ..
There is no option to have an abbreviated step followed by predicates.
<xsl:template match="Device">
<div>
<h2><xsl:value-of select="Name" /></h2>
<xsl:apply-templates select="Featured[. = 'true']" />
<p><xsl:value-of select="Description" /></p>
</div>
</xsl:template>
<xsl:template match="Book">
<div>
<h2><xsl:value-of select="Title" /></h2>
<xsl:apply-templates select="FeaturedBook[. = 'true']" />
<h3><xsl:value-of select="Author" /></h3>
<p><xsl:value-of select="Summary" /></p>
</div>
</xsl:template>
<xsl:template match="FeaturedBook|Featured">
<p class='featured-notice'>This is a featured item!
Buy now to get a 15% discount.
</p>
</xsl:template>

How to make XSLT xsl:if work

I am trying to make this (xml 1.0) code work . I am new to this and already exhausted myself in
trying different ways. Does someone know my mistake?
<xsl:for-each select="News/Sport">
<xsl:if test="local-name()='Basketball'">
<p>
<xsl:text>Basketball Sport</xsl:text>
</p>
<xsl:value-of select="News/Sport/Basketball/Phrases/Phrase"/>
</xsl:if>
</xsl:for-each>
When I transform it into an HTML file the content doesn't show up. When I remove the xsl:for each and the xsl:if statements the content is successfully presented. I only wish that the content is first checked (if it is available in the XML file) and if yes, that it is taken from the XML content.
Thank you in advance for your help!
EDIT:
This is my XML code
<News>
<Sport>
<Basketball>
<Phrases>
<Phrase>Zach Randolph recovered the opening tipoff in Game 1 of the Western Conference Finals, and he didn’t touch the ball again until the possession following the Grizzlies’ first timeout.
</Phrase>
<Phrases>
</Basketball>
</Sport>
</News>
EDIT2:
Could you tell me why I cannot apply a template inside this below function? Only the text works now:(
<xsl:for-each select="News/Sport[Basketball]">
<xsl:apply-templates select="News/Sport/*" />
</xsl:for-each>
<xsl:template match="Basketball">
<p>
<xsl:text>Basketball Sport</xsl:text>
</p>
<xsl:apply-templates select="Phrases/Phrase"/>
</xsl:template>
<xsl:for-each select="News/Sport">
<xsl:if test="local-name()='Basketball'">
In this if test, the context node is a Sport element, so local-name() will always be Sport and will never equal Basketball.
I only wish that the content is first checked (if it is available in the XML file) and if yes, that it is taken from the XML content.
The usual way to handle this sort of thing in XSLT is to define templates matching the various nodes that might be present and then applying templates to all the nodes that are actually found. If there are no nodes of a particular type then the corresponding template will not fire
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html" />
<xsl:template match="/">
<html>
<body>
<!-- apply templates that match all elements inside Sport, which may
be Basketball, Football, etc. -->
<xsl:apply-templates select="News/Sport/*" />
</body>
</html>
</xsl:template>
<!-- when we find a Basketball element ... -->
<xsl:template match="Basketball">
<p>
<xsl:text>Basketball Sport</xsl:text>
</p>
<xsl:apply-templates select="Phrases/Phrase"/>
</xsl:template>
<!-- when we find a Football element ... -->
<xsl:template match="Football">
<p>
<xsl:text>Football Sport</xsl:text>
</p>
<!-- whatever you need to do for Football elements -->
</xsl:template>
<xsl:template match="Phrase">
<p><xsl:value-of select="." /></p>
</xsl:template>
</xsl:stylesheet>
That way you don't need any explicit for-each or if, the template matching logic handles it all for you.
You are missing the idea of a context node. Within a xsl:for-each, everything you refer to is about or relative to the selected nodes. So, for instance, within <xsl:for-each select="News/Sport">, the context node is the Sport elements, and a test like <xsl:if test="local-name()='Basketball'"> is always going to be false because the local name is always Sport.
It looks like you want just <xsl:if test="Basketball"> which tests whether there are any Basketball child nodes of the current Sport node.
The same thing applies to <xsl:value-of select="News/Sport/Basketball/Phrases/Phrase"/>. Because everything is relative to the Sport node, XSLT is looking for News/Sport/Basketball/Phrases/Phrase within the Sport element, which never exists.
In addition, you can just put text in literally: there is no need for an xsl:text element here, so your code should look like
<xsl:for-each select="News/Sport">
<xsl:if test="Basketball">
<p>Basketball Sport</p>
<xsl:value-of select="Basketball/Phrases/Phrase"/>
</xsl:if>
</xsl:for-each>
You can refine this further by adding a predicate to the for-each selection, so that it matches only Sport elements with a Basketball child. Like this
<xsl:for-each select="News/Sport[Basketball]">
<p>Basketball Sport</p>
<xsl:value-of select="Basketball/Phrases/Phrase"/>
</xsl:for-each>

Hyperlinks within XSLT Templates

I am trying to create hyperlinks using XML information and XSLT templates. Here is the XML source.
<smartText>
Among individual stocks, the top percentage gainers in the S. and P. 500 are
<smartTextLink smartTextRic="http://investing.domain.com/research/stocks/snapshot
/snapshot.asp?ric=HBAN.O">Huntington Bancshares Inc</smartTextLink>
and
<smartTextLink smartTextRic="http://investing.domain.com/research/stocks/snapshot
/snapshot.asp?ric=EK">Eastman Kodak Co</smartTextLink>
.
</smartText>
I want the output to look like this, with the company names being hyperlinks based on the "smartTextLink" tags in the Xml.
Among individual stocks, the top percentage gainers in the S.&P. 500 are Eastman Kodak Co and Huntington Bancshares Inc.
Here are the templates that I am using right now. I can get the text to display, but not the hyperlinks.
<xsl:template match="smartText">
<p class="smartText">
<xsl:apply-templates select="child::node()" />
</p>
</xsl:template>
<xsl:template match="smartTextLink">
<a>
<xsl:apply-templates select="child::node()" />
<xsl:attribute name="href">
<xsl:value-of select="#smartTextRic"/>
</xsl:attribute>
</a>
</xsl:template>
I have tried multiple variations to try to get the hyperlinks to work correctly. I am thinking that the template match="smartTextLink" is not being instantiated for some reason. Does anyone have any ideas on how I can make this work?
EDIT: After reviewing some of the answers, it is still not working in my overall application.
I am calling the smartText template from within my main template
using the following statement...
<xsl:value-of select="marketSummaryModuleData/smartText"/>
Could this also be a part of the problem?
Thank you
Shane
Either move the xsl:attribute before any children, or use an attribute value template.
<xsl:template match="smartTextLink">
<a href="{#smartTextRic}">
<xsl:apply-templates/>
</a>
</xsl:template>
From the creating attributes section of the XSLT 1 spec:
The following are all errors:
Adding an attribute to an element after children have been added to it; implementations may either signal the error or ignore the attribute.
Try this - worked for me:
<xsl:template match="smartText">
<p class="smartText">
<xsl:apply-templates/>
</p>
</xsl:template>
<xsl:template match="smartTextLink">
<a>
<xsl:attribute name="href">
<xsl:value-of select="#smartTextRic"/>
</xsl:attribute>
<xsl:value-of select="text()"/>
</a>
</xsl:template>
Trick is - <xsl:attribute> first, before you do any other processing.
Marc