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

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" />

Related

Use dynamically created xsl:variable in select (Sharepoint 2013)

I'm using xslt 1.0 to transform a Sharepoint 2013 Data View.
The data source consists of 3 joined lists:
ISOList
Threat_Catalogue (has a lookup (multiple) to ISOList)
Risks (has a lookup (single) to Threat_Catalogue)
I'm looping thru items in ISOList, and want to show all Risks that are relevant for this specific item. The logic being: First find Threats related to the specific item in ISOList, and then find Risks related to one of those Threats.
So I'm calling a template with the item ID (ISORef) from ISOList as a parameter - like so:
<xsl:template name="Risks">
<xsl:param name="ISORef" />
<!-- add extra info to string, so it only selects relevant items -->
<xsl:variable name="ISORefString" select="concat('&ID=',$ISORef,'&RootFolder')"/>
<!-- get relevant threats -->
<xsl:variable name="SelectedThreats" select="/dsQueryResponse/Threat_Catalogue/Rows/Row[contains(#ISO_x0020_Reference, $ISORefString)]"/>
<!-- Create variable, that contains all ID's in Threat_Catalogue that are relevant -->
<xsl:variable name="ListThreats"><xsl:for-each select="$SelectedThreats">=<xsl:value-of select="#ID"/>&</xsl:for-each></xsl:variable>
<!-- Variable - just to illustrate problem -->
<xsl:variable name="ListThreatsAlternative" select="$ListThreats" />
<!-- this is where I'm trying to use variable to get relevant risks, but fails -->
<!-- Old: Didn't Work, has been replaced -->
<!--
<xsl:variable name="SelectedRisks" select="/dsQueryResponse/Risks/Rows/Row[contains($ListThreats, substring-before(substring-after(#Threat, 'ID'), 'RootFolder'))]"/>
-->
<!-- New: Works as intended -->
<xsl:variable name="SelectedRisks" select="/dsQueryResponse/Risks/Rows/Row[contains($ListThreats, substring-before(substring-after(#Threat, 'ID'), 'RootFolder')) and $ListThreats != '' and #Threat != '']"/>
<!-- Example output: -->
Original: <xsl:value-of select="$ListThreats" /> <!-- contains: '=118&' --> <br/>
Alternative: <xsl:value-of select="$ListThreatsAlternative" /> <!-- empty -->
</xsl:template>
The problem is that variable ListThreats appears as an empty string when used as a criteria in the select for either SelectedRisks or ListThreatsAlternative.
*Edited:
The source XML is generated by Sharepoint. I don't know how to get the XML of the specific data source, but below I've added the XML from the Risks list. I've shortened down the XML source substantially, but hopefully it is sufficient.
Also - there are some significant differences in source XML and what Sharepoint presents in a list. Especially when it comes to the lookup-fields.
Example:
When Sharepoint presents the content:
https://xxx.xxx.xxx.com/sites/xxx_xxx/ISMS/_layouts/15/listform.aspx?PageType=4&ListId={4ab4bbff-588b-4c06-9685-c814cdefd59f}&ID=56&RootFolder=*', RefreshPage); return false;" href="https://xxx.xxx.xxx.com/sites/xxx_xxx/ISMS/_layouts/15/listform.aspx?PageType=4&ListId={4ab4bbff-588b-4c06-9685-c814cdefd59f}&ID=56&RootFolder=*">Hackers: 14. Password Cracking
When similar content is shown in source XML:
55;#Hackers: 13. Malware
This is the XML source (shortened):
<xml xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema">
<s:Schema id="RowsetSchema">
<s:ElementType name="row" content="eltOnly" rs:CommandTimeout="30">
<s:AttributeType name="ows_ID" rs:name="ID" rs:number="1">
<s:datatype dt:type="i4" dt:maxLength="4"/>
</s:AttributeType>
<s:AttributeType name="ows_LinkTitle" rs:name="Risk Title" rs:number="2">
<s:datatype dt:type="string" dt:maxLength="512"/>
</s:AttributeType>
<s:AttributeType name="ows_Threat" rs:name="Threat" rs:number="15">
<s:datatype dt:type="variant" dt:lookup="true" dt:maxLength="8009"/>
</s:AttributeType>
</s:ElementType>
</s:Schema>
<rs:data>
<z:row ows_ID="72" ows_LinkTitle="Test risk" ows_Threat="55;#Hackers: 13. Malware" />
</rs:data>
</xml>
Found the answer, stupid mistake in the select. Edited in OP.

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?".

XSLT contains two periods

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.

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