XSLT contains two periods - xslt

I have an HTML document that used an unordered list to display the navigation. When it is output as XML it no longer uses the list, but instead assigns each link a <Description>. The way that the description works is that the main links are assigned numbers like 100, 200, 300, 400, and so on. Subnavigation links from those are assigned as follows: 200.100, 200.200, 200.300, and so on.
The XML looks like the following:
<Items>
<PgIndexElementItem>
<Description>100</Description>
<Title>This is the Title</Title>
</PgIndexElementItem>
<PgIndexElementItem>
<Description>200</Description>
<Title>This is the Title</Title>
</PgIndexElementItem>
<PgIndexElementItem>
<Description>200.100</Description>
<Title>This is the Title</Title>
</PgIndexElementItem>
<PgIndexElementItem>
<Description>200.100.100</Description>
<Title>This is the Title</Title>
</PgIndexElementItem>
</Items>
If you look at the last PgIndexElementItem, there are three sets of numbers.
I'm trying to recreate the unordered list using XSLT. This is what I have been doing so far:
<ul>
<xsl:for-each select="//PgIndexElementItem">
<xsl:if test="not(contains(Description, '.'))">
<li><xsl:value-of select="Title"/>
<ul>
<xsl:for-each select="//PgIndexElementItem">
<xsl:if test="contains(Description, '.')">
<li><xsl:value-of select="Title"/></li>
</xsl:if>
</xsl:for-each>
</ul>
</li>
</xsl:if>
</xsl:for-each>
</ul>
My question would be, is there a way to specify a contains(Description that targets descriptions with two periods, like the last PgIndexElementItem above?
I hope I explained this well enough. I don't know how to make it less confusing. Thanks in advance!
EDIT
o Graduate Program
* How to Apply
* Masters Program
o M.A. Handbook
o FAQs
o Alumni
o Masters Theses
o Sample Thesis Proposals
* M.A. Handbook
* FAQs
* Alumni
* Masters Theses
* Sample Thesis Proposals

You can use
<xsl:if test="contains(substring-after(Description, '.'), '.')">
...
to test whether an element has a Description child with two or more periods.
However, I would instead change your XSLT to:
<!-- this part replaces your <ul> code above -->
<ul>
<xsl:apply-templates
select="//PgIndexElementItem[not(contains(Description, '.'))]" />
</ul>
Updated this template:
<!-- add this new template -->
<xsl:template match="PgIndexElementItem">
<li>
<xsl:value-of select="Title"/>
<xsl:variable name="prefix" select="concat(Description, '.')"/>
<xsl:variable name="childOptions"
select="../PgIndexElementItem[starts-with(Description, $prefix)
and not(contains(substring-after(Description, $prefix), '.'))]"/>
<xsl:if test="$childOptions">
<ul>
<xsl:apply-templates select="$childOptions" />
</ul>
</xsl:if>
</li>
</xsl:template>
This will avoid unnecessary code duplication, and unwanted empty <ul> elements.
I've made some assumptions about how the Description children are to be interpreted... e.g. "200.100" should appear in a sublist under "200" but not under "100".

Use:
string-length(Description) - string-length(translate(Description, '.','')) = 2
This works the same for any number $n (while the "substring-after" technique is simply unusable in the general case):
string-length(Description) - string-length(translate(Description, '.','')) = $n

I looks like you're dealing with nested PgIndexElementItem elements - if they nest in multiple layers you should consider refactoring your XSL to use a recursive template (assuming the HTML output for each level is predictable).
As it is written, your for-each loop selects all the PgIndexElementItem descendants of the current element, then does that again when it hits the inner for-each loop. Possibly not what you intended.

Related

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>

Using out-of-sequence tags in XSLT (or grouping elements)

Is it possible to have out-of-sequence tags within XSLT using 1.0? My initial guess is not, as it breaks the rules of XML.
Consider XML data that has X elements, and I want to split those X entries into blocks of 3 within individual <div> blocks. What I would like to do is something this, but obviously it is completely invalid code...
<div>
<xsl:for-each select="mydata">
<xsl:value-of select="myvalue"/><br/>
<xsl:if test="(position() mod 3)=0">
</div> <!-- This is invalid -->
<div> <!-- This is invalid -->
</xsl:if>
</xsl:for-each>
</div>
So for 8 elements, the example result would be
<div>
value1<br/>
value2<br/>
value3<br/>
</div>
<div>
value4<br/>
value5<br/>
value6<br/>
</div>
<div>
value7<br/>
value8<br/>
</div>
If the above is simply not possible (as I suspect it is not), can somebody suggest an acceptable way to group them like this?
(Please note, this must be an XSLT 1.0 solution)
What you're trying to do is possible, but it isn't a good idea. This is a better approach:
<xsl:apply-templates select="mydata[position() mod 3 = 1]" mode="group" />
<!-- Separate templates -->
<xsl:template match="mydata" mode="group">
<div>
<xsl:apply-templates select=". | following-sibling::mydata[position() < 3]" />
</div>
</xsl:template>
<xsl:template match="mydata">
<xsl:value-of select="myvalue"/><br/>
</xsl:template>
JLRishe shows you the solution.
Your problem is that you are thinking of your stylesheet as writing start and end tags. That's not what XSLT does: it writes a tree. You can't write half a node to the result tree. Think nodes, not tags.
When you have problems like this in which the output structure doesn't exactly match the input structure, another useful rule of thumb is that the structure of the stylesheet should reflect the tree structure of the output, not that of the input. Don't think "what shall I do with the next ABC input node", but rather "I need to generate an XYZ node in the result tree, how shall I compute its content?".

making compound xpath statement for xslt: match

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>

XSLT: output list on condition of attribute of parent-node

I'm struggling to create a list to be displayed on the condition of the parent node ('folder') with attribute 'folded' set to either 'yes' or 'no'.
The result shall display the first two levels of the list only and not the third level as below.
1st. Level: display
2nd. Level: display
3rd. Level: NO display
The idea is to check the 'folder'-attribute <folder folded="yes"> with <xsl:if test="not(parent::yes)">.
That should qualify for the 3rd. Level to NOT being displayed, but somehow it doesn't do what I want it to do.
I probably use the wrong construct and/or syntax.
Assistance is highly appreciated, thanks.
The XML structure with some content:
<xbel>
<folder folded="yes">
<level>1</level>
<title>bookmarks</title>
<desc>my bookmarks</desc>
<folder folded="no">
<level>2</level>
<title>Android</title>
<desc>my Android</desc>
<bookmark href="http://www.phonesreview.co.uk/">
<title>HTC Sync 3.0.5422 Update: Aria, Desire, Hero, Legend</title>
<desc>The new HTC Sync 3.0.5422 update will be most welcome...</desc>
</bookmark>
<folder folded="no">
<level>3</level>
<title>Apps</title>
<desc>Android Apps</desc>
<bookmark href="http://www.androidzoom.com/">
<title>Android Communication Apps</title>
<desc>Download Communication Apps for Android.</desc>
</bookmark>
<bookmark href="http://www.htc.com/">
<title>HTC - Android</title>
<desc>Apps for HTC-Android.</desc>
</bookmark>
</folder>
</folder>
</folder>
</xbel>
The XSLT:
<!--creates a nested list of elements named 'folder'-->
<xsl:template match="folder" mode="linklist">
<li>
<xsl:if test="folder/level = 2">
Level:<xsl:value-of select="level"/> /
Title:<xsl:value-of select="title"/> /
Desc:<xsl:value-of select="desc"/>
<ul>
<xsl:apply-templates mode="linklist" />
</ul>
</xsl:if>
</li>
</xsl:template>
<xsl:template match="bookmark" mode="linklist">
<li> <!-- this bookmark is just another item in the list of bookmarks -->
<!-- the title -->
<a rel="nofollow" href="{#href}"><xsl:value-of select="title"/></a>
<!-- the description -->
<xsl:if test="desc">
<span class="bookmarkDesc">
<xsl:value-of select="desc"/>
</span>
</xsl:if>
</li>
</xsl:template>
The Stylesheet HTML
<body>
<ul>
<xsl:apply-templates mode="linklist" />
</ul>
</body>
The generated output (levels:1-3)
Level:1 / Title:bookmarks / Desc:my bookmarks
Level:2 / Title:Android / Desc:my Android
HTC Sync 3.0.5422 Update: Aria, Desire, Hero, Legend ...
Level:3 / Title:Apps / Desc:Android Apps
Android Communication AppsDownload Communication Apps for Android.
HTC - AndroidApps for HTC-Android.
The anticipated output: (levels: 1-2)
Level:1 / Title:bookmarks / Desc:my bookmarks
Level:2 / Title:Android / Desc:my Android
HTC Sync 3.0.5422 Update: Aria, Desire, Hero, Legend ...
I tried this template, but that outputs the last two nodes, I need the two first nodes.
<xsl:template match="folder[parent::folder/#folded = 'yes']" mode="linklist">
The simplest possible change you could make to prevent processing of unfolded folder elements is to add an empty template that swallows them (i.e. produces no output):
<xsl:template match="folder[#folded='no']" mode="linklist"/>
All folder elements not having a folded attribute equal to no will be processed using your existing template; those that do will be captured by this new one.
If instead you want to process each folder element having either its own folded attribute equal to yes or that of its parent (as in the updated XML example), then use the following template:
<xsl:template match="folder[#folded='yes' or ../#folded='yes']" mode="linklist">
<!-- body elided -->
</xsl:template>
You'll probably also want to include an empty template for hiding all other folder elements:
<xsl:template match="folder" mode="linklist" />

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