In XSLT, how do I use a for-each loop? - xslt

If I had a bunch of fields on a screen, say ten for first name and ten for last name, and they're named firstName1, firstName2, etc., and lastName1, lastName2, etc., how would I create a loop that goes through each last name field?
Right now, I have it set up to perform a task ten times, one for each last name field. How can I set up a for-each loop that goes through lastName1, lastName2, lastName3...lastName10 and does a specific task for each of them?
Input XML:
<Arguments>
<EnteredBy>SDSADFSADF</EnteredBy>
<IDNumber>WERWEW</IDNumber>
<Book1>Y</Book1>
<LastName1>ASDFASFASDFASFA</LastName1>
</Arguments>
XSLT:
<xsl:apply-templates select="/myQuery/Arguments/lastName1"/>
and there are lastName2 through lastName10, as well.
I want to loop through each of the ten and truncate the last names to five characters.

The simplest and most idiomatic form of iteration in XSLT is to use xsl:apply-templates on the set of elements you want to handle. If you don't see how to use that idiom to solve your problem, it's worth spending time working to learn it.
[Addendum:]
For example:
<xsl:template match="/myQuery/Arguments">
...
<xsl:element name="Arguments">
<xsl:apply-templates/>
</
...
</
<xsl:template match="lastName1 | lastName2 | lastName3
| ... | lastName9">
<xsl:element name="{name()">
<xsl:value-of select="substring(.,1,5)"/>
</
</
This assumes you have elements names lastName1, lastName2, etc., instead of the simpler idiom where all last names are called (wait for it!) lastName, and that what you want in this particular part of the document is a near-identity transform.
XSLT does also have an xsl:for-each, which can be regarded as syntactic sugar for xsl:apply-templates and is sometimes preferred by programmers with a procedural bent. If you think of it as analogous to a loop in an imperative language, however, your instincts will eventually fail you and you will be surprised because your attempts at variable mutation don't work the way you expected them to.
Systematic work through a good book or tutorial will pay off.

Here's what I got to work:
<xsl:template match="/">
<xsl:element name="Arguments">
<xsl:apply-templates />
</xsl:element>
</xsl:template>
<xsl:template match="Arguments">
<xsl:for-each select="./*[contains(name(), 'LastName')]">
<xsl:call-template name="test">
</xsl:call-template>
</xsl:for-each>
</xsl:template>
<xsl:template name="test">
<xsl:element name="{name()}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>
This way you don't need to make 9 calls, just one per loop.
Or if you want to avoid the for-each, you can do this instead:
<xsl:template match="Arguments/*[contains(name(), 'LastName')]">
<xsl:element name="{name()}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>

Related

How can I access an xsl:variable from outside the "scope" of an xsl:for-each block

I'm trying to access a variable from within a for-each. I've done a lot of reading today to try to figure this out, but nothing quite fits my scenario. Eventually, I will have multiple series like seen below and I will use the variables that I'm pulling out and make different condition. The for-each that I have below is bringing back data from 400 records. I'm sorry, I cannot provide an XML. I'm not able to expose GUIDs and such.
UPDATE
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output media-type="xml" indent="yes"/>
<xsl:template match="/">
<Records>
<xsl:for-each select="Records/Record/Record[#levelGuid = 'level1']">
<xsl:variable name="rocName1" select="Field[#guid = '123']"/>
<xsl:variable name ="rocName2" select="substring-before($rocName1, ' - ')"/>
</xsl:for-each>
<xsl:for-each select="Records/Record/Record[#levelGuid = 'levelA']">
<xsl:variable name ="findingName" select="Field[#guid = '123']"/>
<xsl:variable name="findingName1" select="substring-after($findingName, ': ')"/>
<xsl:variable name="findingName2" select="substring-after($findingName1, 'PCIDSSv3.1:')"/>
</xsl:for-each>
<xsl:if test="$findingName1 = $rocName1">
<Records>
<findingID>
<xsl:for-each select="Records/Record/Record[#levelGuid = '123']">
<xsl:value-of select ="Field[#guid = '123']"/>
</xsl:for-each>
</findingID>
</Records>
</xsl:if>
</Records>
</xsl:template>
</xsl:stylesheet>
The desired output is any findingID that has a $findingName1 that equals $rocName1. The GUIDS only appear once, but each level has hundreds of records.
I'm trying to access a variable from within a for-each.
The variable $rocRecord is in scope inside the for-each. You can simply reference it and use it.
But I think you are trying to do something else. I.e., defining a variable inside for-each and wanting to use it outside it.
Variables are scoped within their focus-setting containing block. So the short answer is: you cannot do it. The long answer however...
Use templates. The only reason to do what you seem to want to be doing is to need to access the data elsewhere:
<xsl:template match="/">
<!-- in fact, you don't need for-each at all, but I leave it in for clarity -->
<xsl:for-each select="records/record">
<!--
apply-templates means: call the declared xsl:template that
matches this node, it is somewhat similar to a function call in other
languages, except that it works the other way around, the processor will
magically find the "function" (i.e., template) for you
-->
<xsl:apply-templates select="Field[#guid='123']" />
</xsl:for-each>
</xsl:template>
<xsl:template match="Field">
<!-- the focus here is what is the contents of your variable $rocName1 -->
<rocName>
<xsl:value-of select="substring=-before(., ' - ')" />
</rocName>
</xsl:template>
XSLT is a declarative, template-oriented, functional language with concepts that are quite unique compared to most other languages. It can take a few hours to get used to it.
You said you did a lot of reading, but perhaps it is time to check a little XSLT course? There are a few online, search for "XSLT fundamentals course". It will save you hours / days of frustration.
This is a good, short read to catch up on variables in XSLT.
Update
On second read, I think it looks like you are troubled by the fact that the loop goes on for 400 items and that you only want to output the value of $rocName1. The example I showed above, does exactly that, because apply-templates does nothing if the selection is empty, which is what happens if the guid is not found.
If the guid appears once, the code above will output it once. If it appears multiple times and you only want the first, append [1] to the select statement.
Update #2 (after your update with an example)
You have two loops:
<xsl:for-each select="Records/Record/Record[#levelGuid = 'level1']">
and
<xsl:for-each select="Records/Record/Record[#levelGuid = 'levelA']">
You then want to do something (created a findingId) when a record in the first loop matches a record in the second loop.
While you can solve this using (nested) loops, it is not necessary to do so, in fact, it is discouraged as it will make your code hard to read. As I explained in my original answer, apply-templates is usually the easier way to do get this to work.
Since the Record elements are siblings of one another, I would tackle this as follows:
<xsl:template match="/">
<Records>
<xsl:apply-templates select="Records/Records/Record[#levelGuid = 'level1']" />
</Records>
</xsl:template>
<xsl:template match="Record">
<xsl:variable name="rocName1" select="Field[#guid = '123']"/>
<xsl:variable name ="rocName2" select="substring-before($rocName1, ' - ')"/>
<xsl:variable name="findingNameBase" select="../Record[#levelGuid = 'levelA']" />
<xsl:variable name ="findingName" select="$findingNameBase/Field[#guid = '123']"/>
<xsl:variable name="findingName1" select="substring-after($findingName, ': ')"/>
<xsl:variable name="findingName2" select="substring-after($findingName1, 'PCIDSSv3.1:')"/>
<findingId rocName="{$rocName1}">
<xsl:value-of select="$findingName" />
</findingId>
</xsl:template>
While this can be simplified further, it is a good start to learn about applying templates, which is at the core of anything you do with XSLT. Learn about applying templates, because without it, XSLT will be very hard to understand.

XSLT; Group 2 or more different elements BUT only when adjacent

Initially this seemed a trivial problem, but it seems to be harder than I thought.
In the following xml, I want to group adjacent 'note' and p elements only. A note should always start a notegroup and any following p should be included. No other elements are allowed in the group.
From this:
<doc>
<note />
<p/>
<p/>
<other/>
<p/>
<p/>
</doc>
To this:
<doc>
<notegroup>
<note />
<p/>
<p/>
</notegroup>
<other/>
<p/>
<p/>
</doc>
Seems ridiculously easy, but the rule is: 'note' and any following 'p'. Any p on their own are to be ignored (as in the last 2 p above)
With xslt 2.0, if I try something like:
<xsl:for-each-group select="*" group-adjacent="boolean(self::note) or boolean(self::p)">
fails because it also groups the two later p elements.
Or the 'starts-with' approach on the 'note' which seems to indiscriminately group any element after (instead of just the p elements).
The other approach I'm considering is to simply add an attribute to each note and the p that immediately follow the note, and using that to group later, but how can I do that?
Thanks for any answers
I think you can do this by being a bit creative with group-starting-with, essentially you want to start a new group whenever you see an element that is not one that belongs in a notegroup. In your example this would generate two groups - note+p+p and other+p+p, the trick is to only wrap a notegroup around groups where the initial item is a note, and to simply output groups that are not headed by a note unchanged
<xsl:for-each-group select="*" group-starting-with="*[not(self::p)]">
<xsl:choose>
<xsl:when test="self::note">
<notegroup>
<xsl:copy-of select="current-group()"/>
</notegroup>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
This will wrap all note elements in a notegroup even if they don't actually have any following p elements. If you don't want to wrap a "bare" note then make it <xsl:when test="self::note and current-group()[2]"> to trigger the wrapping only when the current group has more than one member.
If you have more than one element name that can be part of a notegroup then you could either list them all in the predicate
<xsl:for-each-group select="*" group-starting-with="*[not(self::p|self::ul)]">
or declare a variable holding the node names that can be part of a notegroup:
<xsl:variable name="notegroupMembers" select="(xs:QName('p'), xs:QName('ul'))" />
and then say
<xsl:for-each-group select="*"
group-starting-with="*[not(node-name(.) = $notegroupMembers)]">
taking advantage of the fact that an = comparison where one side is a sequence succeeds if any of the items in the sequence match.
Issue
You're group on either <note> or <p>. Hence the failing.
Hint
You can try by using group-starting-with="note",as describe in Grouping With XSLT 2.0 # XML.com:
<xsl:template match="doc">
<doc>
<xsl:for-each-group select="*" group-starting-with="note">
<notegroup>
<xsl:for-each select="current-group()">
<xsl:copy>
<xsl:apply-templates select="self::note or self::p" />
</xsl:copy>
</xsl:for-each>
</notegroup>
</xsl:for-each-group>
</doc>
</xsl:template>
You may need another <xsl:apply-templates /> around the end.

Multi layer conditional wrap HTML with XSLT

In my Umbraco CMS site, I'm making a list of node "widgets" for content editors to use, with a list of many options they can toggle to change the display. This often involves wrapping an element with an anchor, div, or something else.
Using XSLT to display these from the XML output, I've put together what a kludge approach as I'm a very new XSLT beginner.
What I've come to as a solution is multiple nested apply-templates. This creates a large list of conditions, often asking repeat checks, which trees out pretty huge. It's a bit of a nightmare to manage.
It looks as such (but with more than two options in each choose):
<xsl:template match="/">
<xsl:choose>
<xsl:when test="type='1'">
<xsl:apply-templates select="widgetContent" mode="type_1" />
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="widgetContent" mode="type_default" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="wigetContent" mode="type_1">
<xsl:choose>
<xsl:when test="./wrap_with_hyperlink != 0">
<xsl:element name="a">
<xsl:apply-templates select="." mode="hyperlink_wrapped" />
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="widgetContent" mode="not_hyperlink_wrapped" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
What can I do to reduce this tangled mess? I've structured the conditions to be as top down as much as possible, but there are definitely repeated checks where type_2 has to ask the same questions as type_1 all over again.
(edit: clarity) Because the design is basically an onion, type_1 is wrapped by certain tags, type_2 is wrapped by different tags. Next layer in, both could be wrapped by the same tags, and so forth. What would be perfect is:
<xsl:if test="this_wrap_style = 1"><xsl:element name="abc"></xsl:if>
<xsl:if test="this_wrap_style = 2"><xsl:element name="xyz"></xsl:if>
(everything else)
</abc> //if it exist.
</xyz> //etc
Which definitely doesn't work.
Some of this has been reduced by using Umbraco Doc Types for different widget controls, but part of the nature is that to the ideal structure for content editors is selecting a box widget will give them 5 different types of widget boxes (or more) to choose from, and a coherent back end isn't as important.
Thank you all for your time.
<!--Store data in processing instruction nodes in a separate XML file-->
<?xml version='1.0' encoding="utf-8"?>
<root>
<?_1 div?>
<?_2 p?>
</root>
type_1 is wrapped by certain tags, type_2 is wrapped by different tags.
<xsl:variable name="divider" select="document('condition.xml')//processing-instruction(concat('_', $type) )" />
<xsl:variable name="equalizer" select="'span'"/>
<xsl:element name="{$divider}">
...
</xsl:element>
Next layer in, both could be wrapped by the same tags
<xsl:if test="contains('1,2',$type)">
<xsl:element name="{$equalizer}">
...
</xsl:element>
</xsl:if>

XSLT for-each counter - how to access data

for performance testing purposes I want to take a small XML file and create a bigger one from it - using XSLT. Here I plan to take each entity (Campaign node in the example below) in the original XML and copy it n times, just changing its ID.
The only way I can think of to realize this, is a xsl:for-each select "1 to n". But when I do this I do not seem to be able to access the entity node anymore (xsl:for-each select="campaigns/campaign" does not work in my case). I am getting a processor error: "cannot be used here: the context item is an atomic value".
It seems that by using the "1 to n" loop, I am loosing the access to my actual entity. Is there any XPath expression that gets me access back or does anyone have a completely different idea how to realize this?
Here is what I do:
Original XML
<campaigns>
<campaign id="1" name="test">
<campaign id="2" name="another name">
</cmpaigns>
XSLT I try to use
<xsl:template match="/">
<xsl:element name="campaigns">
<xsl:for-each select="1 to 10">
<xsl:for-each select="campaigns/campaign">
<xsl:element name="campaign">
<xsl:copy-of select="#*[local-name() != 'id']" />
<xsl:attribute name="id"><xsl:value-of select="#id" /></xsl:attribute>
</xsl:element>
</xsl:for-each>
</xsl:for-each>
</xsl:element>
</xsl:template>
Define a variable as the first thing in the match, like so:
<xsl:variable name="foo" select="."/>
This defines a variable $foo of type nodeset. Then access it like this
<xsl:for-each select="$foo/campaigns/campaign">
...
</xsl:for-each>

How do I render a comma delimited list using xsl:for-each

I am rendering a list of tickers to html via xslt and I would like for the list to be comma deliimited. Assuming I was going to use xsl:for-each...
<xsl:for-each select="/Tickers/Ticker">
<xsl:value-of select="TickerSymbol"/>,
</xsl:for-each>
What is the best way to get rid of the trailing comma? Is there something better than xsl:for-each?
<xsl:for-each select="/Tickers/Ticker">
<xsl:if test="position() > 1">, </xsl:if>
<xsl:value-of select="TickerSymbol"/>
</xsl:for-each>
In XSLT 2.0 you could do it (without a for-each) using the string-join function:
<xsl:value-of select="string-join(/Tickers/Ticker, ',')"/>
In XSLT 1.0, another alternative to using xsl:for-each would be to use xsl:apply-templates
<xsl:template match="/">
<!-- Output first element without a preceding comma -->
<xsl:apply-templates select="/Tickers/Ticker[position()=1]" />
<!-- Output subsequent elements with a preceding comma -->
<xsl:apply-templates select="/Tickers/Ticker[position()>1]">
<xsl:with-param name="separator">,</xsl:with-param>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Ticker">
<xsl:param name="separator" />
<xsl:value-of select="$separator" /><xsl:value-of select="TickerSymbol" />
</xsl:template>
I know you said xsl 2.0 is not an option and it has been a long time since the question was asked, but for all those searching for a posibility to do what you wanted to achieve:
There is an easier way in xsl 2.0 or higher
<xsl:value-of separator=", " select="/Tickers/Ticker/TickerSymbol" />
This will read your /Tickers/Ticker elements and insert ', ' as separator where needed
If there is an easier way to do this I am looking forward for advice
Regards Kevin