Multi layer conditional wrap HTML with XSLT - 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>

Related

optimization of XSLT code

While searching for some profiling tools for XSLT, I came across this post. Since a lot of people there suggested to just post the code and offered to give feedback on that, I was wondering if anyone could give me some feedback on mine. I tried this (http://www.saxonica.com/documentation/#!using-xsl/performanceanalysis), but the output html is not very detailed.
I'm new to XSLT and usually work with python/perl, where regex support is much better (however, I won't rule out the possibility that it's just my very basic understanding of XSLT). For the purpose of this project however, I had to work with XSLT. It could be that I'm forcing it to do things in a very unnatural way. Any comments -on performance in particular, but anything else is also welcome, as I'd like to learn- are welcome!
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions">
<xsl:template name="my_terms">
<xsl:variable name="excludes" select="not (codeblock or draft-comment or filepath or shortdesc or uicontrol or varname)"/>
<!-- leftover example of how to work with excludes var -->
<!--<xsl:if test=".//*[$excludes]/text()[contains(.,'access management console')]"><li class="prodterm"><b>PB QA:access management console should be "AppCenter"</b></li></xsl:if>-->
<!-- Loop through all sentences and check for deprecated stuff -->
<xsl:for-each select=".//*[$excludes]/text()">
<xsl:variable name="sentenceList" select="tokenize(., '[\.!\?:;]\s+')"/>
<xsl:variable name="segment" select="."/>
<!-- main sentence loop -->
<xsl:for-each select="$sentenceList">
<xsl:variable name="sentence" select="."/>
<!-- very rudimentary sentence length check -->
<xsl:if test="count(tokenize(., '\W+')) > 30"> <li class="prodterm"><b>Sentence too long:</b> <xsl:value-of select="."/></li></xsl:if>
<!-- efforts to flag the shady case of the gerund -->
<xsl:if test="matches(., '\w+ \w+ing (the|a)')">
<!-- some extra checks to weed out the false positives -->
<xsl:if test="not(matches(., '\b(on|about|for|before|while|when|after|by|a|the|an|some|all|every) \w+ing (the|a)', '!i')) and not(matches(., 'during'))">
<li class="prodterm"><b>Possible unclear usage of gerund. If so, consider rewriting:</b> <xsl:value-of select="."/></li>
</xsl:if>
</xsl:if>
<!-- comma's after certain starting phrases -->
<xsl:if test="matches(., '^\s*Therefore[^,]')"><li class="prodterm"><b>Use a comma after starting a sentence with 'Therefore':</b> <xsl:value-of select="."/></li></xsl:if>
<xsl:if test="matches(., '^\s*(If you|Before|When)[^,]+$')"><li class="prodterm"><b>Use a comma after starting a sentence with 'Before', 'If you' or 'When':</b> <xsl:value-of select="."/></li></xsl:if>
<!-- experimenting with phrasal verbs (if there are a lot of verbs in phrasalVerbs.xml, it will be better to have this as the main loop (and do it outside the sentence loop)) -->
<xsl:for-each select="document('phrasalVerbs.xml')/verbs/verb[matches($sentence, concat('.* ', ./#text, ' .*'))]">
<xsl:variable name="verbPart" select="."/>
<xsl:for-each select="$verbPart/particles/particle/#text[matches($sentence, .) and not(matches($sentence, concat($verbPart/#text, ' ', .)))]">
<xsl:variable name="particle" select="."/>
<li class="prodterm"><b>Separated phrasal verb found in:</b> <xsl:value-of select="$sentence"/></li>
</xsl:for-each>
</xsl:for-each>
<!-- checking if conditionals (should be followed by then) -->
<xsl:if test="matches($sentence, '^\s*If\b', '!i') and not(matches($sentence, '\bthen\b', '!i'))"><li class="prodterm"><b>Conditional If found, but no then:</b> <xsl:value-of select="."/></li></xsl:if>
<!-- very dodgy way of detecting passive voice -->
<!--<xsl:if test="matches($sentence, '\b(are|can be|must be) \w+ed\b', '!i')"><li class="prodterm"><b>PB QA:Possible passive voice. If so, consider using active voice for:</b> <xsl:value-of select="."/></li></xsl:if>-->
<xsl:for-each select='document("generalDeprecatedTermsAndPhrases.xml")/terms/dt'>
<xsl:variable name="pattern" select="./#pattern"/>
<xsl:variable name="message" select="./#message"/>
<xsl:variable name="regexFlag" select="./#regexFlag"/>
<!-- <xsl:if test="matches($sentence, $pattern, $regexFlag)"> -->
<xsl:if test="matches($sentence, concat('(^|\W)', $pattern, '($|\W)'), $regexFlag)"> <!-- This is the work around for not being able to use \b when variable is passed on inside matches() -->
<li class="prodterm"><b><xsl:value-of select="$message"/> in: </b> <xsl:value-of select="$sentence"/> </li>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
To get an idea, the stripped down version of my "generalDeprecatedTermsAndPhrases.xml" looks like this:
<dt pattern='to be able to' message="Use 'to' instead of 'to be able to'" regexFlag="i"></dt>
</terms>
The reason that Saxon's profile is not very detailed is that your code is so monolithic: it's all in one great big template rule.
However, being monolithic isn't by itself the cause of any performance problems.
First observation is a functionality problem: your variable
<xsl:variable name="excludes" select="not (codeblock or draft-comment or filepath or shortdesc or uicontrol or varname)"/>
doesn't do what you think. It's evaluated with the root document node as the context item, and its value is a boolean which is true if the outermost element has a name which is not one of those listed. So I think your xsl:for-each that uses [$excludes] as a predicate is applying to all elements, whereas I suspect you intended it to apply to selected elements. I don't know how much that affects the performance.
The main influence on performance will be the cost of evaluating the regular expressions. The best way to find out which ones are causing the problem is to measure the impact of removing them one-by-one. When you've isolated the problem, there may be a way of rewriting the regular expression to make it perform better (e.g. by making it avoid backtracking).

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

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>

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>

avoid mismatched tags outputting HTML with XSLT

I'm new to XSLT, and there's one specific thing I don't know how to do, despite hours of searching for the answer.
I'm outputting blocks of HTML (result sets), and sometimes the result is a hyperlink, sometimes it is not.
The simple flow looks like this:
<a...> if #url
some HTML code
</a> if #url
But if I do:
when #url
<a...>
/when
some HTML code
when #url
</a>
/when
... I'm told that I have mismatched tags.
I was using CDATA text for the anchor set, but a lot of messages say that this is a "hack" approach.
I'm trying to avoid having to repeat the entire HTML code block only to include the anchors on only one of them.
How do I do this?
-------edit / additional info-----------
Does this make more sense?
<xsl:template match="Row">
<xsl:choose>
<xsl:when test="#url!=''">
<a><xsl:attribute name="href"><xsl:value-of select="#url" /></xsl:attribute>
</xsl:when>
</xsl:choose>
<img />
<xsl:choose>
<xsl:when test="#url!=''">
</a>
</xsl:when>
</xsl:choose>
</xsl:template>
In XSLT, your output is a tree of nodes. Writing an element node is a single atomic operation; it can't be split into separate operations of writing a start tag and writing an end tag. You can't create half a node.
If you do try to treat <a> and </a> as separate and separable operations, you will get this error, because the stylesheet must be well-formed XML.
So, stand back and explain what you are trying to achieve, and then we can tell you how to achieve it properly in XSLT.
One way to refactor the XSLT to conditionally apply the hyperlink and not repeat the logic to produce the <img/> (or whatever more complex logic you are trying to avoid repeating) is to extract that logic out into a different template(s) as either a named template or a template with a #mode.
For instance:
<xsl:template match="Row">
<xsl:choose>
<xsl:when test="#url!=''">
<a>
<xsl:attribute name="href">
<xsl:value-of select="#url"/>
</xsl:attribute>
<xsl:apply-templates select="." mode="image"/>
</a>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="." mode="image"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!--The "common" logic to produce an image element, whether or not it will be surrounded by an anchor linking to the #url -->
<xsl:template match="Row" mode="image">
<img/>
</xsl:template>
An alternative way of accomplishing the same thing, but using templates instead of <xsl:choose>:
<xsl:template match="Row[#url]">
<a href="#url">
<xsl:apply-templates select="." mode="image"/>
</a>
</xsl:template>
<xsl:template match="Row">
<xsl:apply-templates select="." mode="image"/>
</xsl:template>
<xsl:template match="Row" mode="image">
<img/>
</xsl:template>

XSL two for-each loops for same node

Problem I have is I want to loop round the parents making them bold then get the children via the id:pid (parent id) and list them. My second loop doesn't work.
XML
XSL
<xsl:choose>
<xsl:when test="#PARENT_OBH_ID">
<b><xsl:value-of select="#TITLE"/></b>
<xsl:for-each select="FOOTER">
-<xsl:value-of select="#TITLE"/>
</xsl:for-each>
</xsl:when>
</xsl:choose>
</xsl:for-each>
Thanks
You're probably better off restructuring this to use templates, the system you're using at the moment means that the context data is becoming confused (you're xslt parser isn't sure which element it should read attributes from inside the second loop)
<xsl:choose>
<xsl:when test="#PARENT_OBH_ID">
<b><xsl:value-of select="#TITLE"/></b>
<xsl:apply-templates select="FOOTER" />
</xsl:when>
</xsl:choose>
<xsl:template match="FOOTER">
<xsl:value-of select="#TITLE"/>
</xsl:template>
apply-templates restarts the context with the footer element as the main focus (so #TITLE refers to the title attribute on footer, which is what you were aiming for I am guessing?)