SharePoint XSL Counter - sharepoint

So I've been banging my head against the wall for a while and am looking for some help. I'm trying to create a new itemstyle in sharepoint designer that basically checks each item in a task list and then tallies up the total number of Completed, In Progress, and Not Started statuses. The problem is to my knowledge, xsl doesn't have mutable variables. What I have so far is this:
<xsl:variable name="sChk">
<xsl:value-of select="#Status"/>
</xsl:variable>
<xsl:for-each select="#Status">
<xsl:if test="$sChk = 'Completed' ">
<!-- Add to Completed Counter -->
</xsl:if>
<xsl:if test="$sChk = 'In Progress' ">
<!-- Add to In Progress Counter -->
</xsl:if>
<xsl:if test="$sChk = 'Not Started' ">
<!-- Add to Not Started Counter -->
</xsl:if>
<br/>
</xsl:for-each>
Out Of Loop:
Total Completed: <!-- Completed Value -->
Total In Progress: <!-- In Progress Value -->
Total Not Started: <!-- Not Started Value -->
Any and all help would be greatly appreciated, thanks!
EDIT: So I've also tried this recursive method as well but this isn't working either...
<xsl:param name="cCount" select="0"/>
<xsl:param name="ipCount" select="0"/>
<xsl:param name="nsCount" select="0"/>
<xsl:choose>
<xsl:when test="$sChk = 'Completed'">
<xsl:call-template name="PSRView2.0">
<xsl:with-param name="cCount" select="$cCount +1"/>
<xsl:with-param name="ipCount" select="$ipCount"/>
<xsl:with-param name="nsCount" select="$nsCount"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="$sChk = 'In Progress'">
<xsl:call-template name="PSRView2.0">
<xsl:with-param name="cCount" select="$cCount"/>
<xsl:with-param name="ipCount" select="$ipCount +1"/>
<xsl:with-param name="nsCount" select="$nsCount"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="$sChk = 'Not Started'">
<xsl:call-template name="PSRView2.0">
<xsl:with-param name="cCount" select="$cCount"/>
<xsl:with-param name="ipCount" select="$ipCount"/>
<xsl:with-param name="nsCount" select="$nsCount +1"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$cCount"/>
<xsl:value-of select="$ipCount"/>
<xsl:value-of select="$nsCount"/>
</xsl:otherwise>
</xsl:choose>

You are correct in that in XSLT variables are immutable. What you need to use in your case those is the count function, which counts up all the items in a node-set. Something like this:
<xsl:variable name="completed" select="count(task[#Status='Completed'])" />
<xsl:variable name="inprogress" select="count(task[#Status='In Progress'])" />
<xsl:variable name="notstarted" select="count(task[#Status='Not Started'])" />
Total: <xsl:value-of select="$completed + $inprogress + $notstarted" />
Of course, you would need to replace 'task' with what ever element name you are using in your XSLT.
Without seeing your XML, it is hard to give a precise answer, but as an example, consider the following XML
<tasklist>
<task status="Completed" />
<task status="Completed" />
<task status="In Progress" />
</tasklist>
Then the XSLT (to get totals only), would look like this
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="tasklist">
<xsl:variable name="completed" select="count(task[#Status='Completed'])" />
<xsl:variable name="inprogress" select="count(task[#Status='In Progress'])" />
<xsl:variable name="notstarted" select="count(task[#Status='Not Started'])" />
Total: <xsl:value-of select="$completed + $inprogress + $notstarted" />
</xsl:template>
</xsl:stylesheet>
Notice how you need to be positioned on the parent element of all the individual 'task' elements here. As an alternative, you can do something like this...
<xsl:variable name="completed" select="count(//task[#Status='Completed'])" />
Which would count task elements wherever they are in the XML.
You could even do the following, if you really didn't know the element name, but were sure there were no other elements with a 'Status' attibute:
<xsl:variable name="completed" select="count(//*[#Status='Completed'])" />

Related

XSL-FO : How to make value expand vertically

I've got a name string in my XML whose length varies. In case of a long name, it expands horizontally which I cannot afford. How can I make it grow vertically? Below is what I have.
<fo:table-cell font-family="Courier" font-size="10px" display-align="before"height="0.01042in" border-style="solid" border-color="green">
<fo:block padding-top="1pt" padding-bottom="1pt" borderstyle="solid">
<fo:inline>
<xsl:text>USER INFORMATION</xsl:text>
</fo:inline>
<fo:block>
<xsl:text>
</xsl:text>
</fo:block>
<fo:inline font-family="Courier" font-size="12px" font-weight="bold">
<xsl:value-of select="USERNAME" />
</fo:inline>
</fo:block>
</fo:table-cell>
I used that link posted by Joel above in comments. When I applied template exactly, it displayed strings with odd length fine but missed the last character of even length strings. I then changed the comparison from less than< to less than or equal<= and it worked.
Hope it will help someone.Thanks.
<xsl:template name="zero_width_space_1">
<xsl:param name="data" />
<xsl:param name="counter" select="0" />
<xsl:choose>
<xsl:when test="$counter <= string-length($data)">
<xsl:value-of select='concat(substring($data,$counter,1),"​")' />
<xsl:call-template name="zero_width_space_2">
<xsl:with-param name="data" select="$data" />
<xsl:with-param name="counter" select="$counter+1" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="zero_width_space_2">
<xsl:param name="data" />
<xsl:param name="counter" />
<xsl:value-of select='concat(substring($data,$counter,1),"​")' />
<xsl:call-template name="zero_width_space_1">
<xsl:with-param name="data" select="$data" />
<xsl:with-param name="counter" select="$counter+1" />
</xsl:call-template>
</xsl:template>
<xsl:call-template name="zero_width_space_1">
<xsl:with-param name="data" select="mystring" />
</xsl:call-template>

how to get the second (third...) occurence of a substring in xslt?

I have a long String separated with the equal sign(=)as delimiter,something like this:
AAA=BBBBB=CCCCCCCCCCCCCCCC=D=FFFFF=GG=H
The substrings can have arbitrary length.If I want to get the first substring I can use the substring-before function like this:
<xsl:value-of select="substring-before($vari, '=')"/>
But is there a way to get only the second, (third, etc) substring?
I need BBBBB instead of AAA=BBBBB and CCCCCCCCCCCCCCCC instead of AAA=BBBBB=CCCCCCCCCCCCCCCC so substring-before-last won't work.
FYI, if you have EXSLT's String extension elements available, you can do something like this:
<xsl:value-of select="str:tokenize($vari, '=')[2]" />
Using your above string, this would return BBBBB.
In XPath 2.0, this function is built in:
<xsl:value-of select="tokenize($vari, '=')[2]" />
With xslt-1.0:
This could be done by a combination of substring-after and substring-before.
Try:
<xsl:value-of select="substring-before(
substring-after ($vari, , '=')
, '=')"/>
For BBBBB
Or:
<xsl:value-of select="substring-before(
substring-after ( substring-after ($vari, '='), '=')
, '=')"/>
For CCCCCCCCCCCCCCCC.
Attention: Not tested.
With xslt-2.0 there may be better ways.
Here is an xslt-1.0 solution that doesn't require the use of extensions and works with arbitrary separator lengths an separator occurrences:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:call-template name="substring-nth-occurrence-position">
<xsl:with-param name="string">aaa_#bbb_#ccc_#dddd_#ee</xsl:with-param>
<xsl:with-param name="substr">_#</xsl:with-param>
<xsl:with-param name="occurrence">3</xsl:with-param>
</xsl:call-template>
</xsl:template>
<xsl:template name="substring-nth-occurrence-position">
<xsl:param name="string"/>
<xsl:param name="substr"/>
<xsl:param name="occurrence"/>
<xsl:param name="current_iteration" select="1"/>
<xsl:param name="accumulated_length" select="0"/>
<xsl:choose>
<!-- in case occurrence is greater than the number of instances of substr return 0 -->
<xsl:when test="not(contains($string, $substr))">0</xsl:when>
<xsl:otherwise>
<!-- determine the position of the next instance of substr in the (remaining part of ) string and add it to accumulated_length-->
<xsl:variable name="v.accumulated_length_new">
<xsl:variable name="v.idx.of.substr">
<xsl:call-template name="instr">
<xsl:with-param name="string" select="$string"/>
<xsl:with-param name="substr" select="$substr"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="$accumulated_length + $v.idx.of.substr"></xsl:value-of>
</xsl:variable>
<!-- if current_iteration equals occurrence then return accumulated length... -->
<xsl:choose>
<xsl:when test="$current_iteration= $occurrence ">
<xsl:value-of select="$v.accumulated_length_new" />
</xsl:when>
<!-- ... else run another iteration-->
<xsl:otherwise>
<xsl:call-template name="substring-nth-occurrence-position">
<xsl:with-param name="string" select="substring-after($string, $substr)"/>
<xsl:with-param name="substr" select="$substr"/>
<xsl:with-param name="occurrence" select="$occurrence"/>
<xsl:with-param name="current_iteration" select="$current_iteration + 1"/>
<xsl:with-param name="accumulated_length" select="$v.accumulated_length_new + string-length($substr) - 1"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- returns the position of the first occurrence of a substring in a string -->
<xsl:template name="instr">
<xsl:param name="string"/>
<xsl:param name="substr"/>
<xsl:value-of select="contains($string,$substr)*(1 + string-length(substring-before($string,$substr)))" />
</xsl:template>
</xsl:stylesheet>
(Execution result of the above example is 14).

How can I speed up my 'divide and conquer' XSLT template which replaces certain characters in a string?

UPDATE: I added an answer to this question which incorporates almost all the suggestions which have been given. The original template given in the code below needed 45605ms to finish a real world input document (english text about script programming). The revised template in the community wiki answer brought the runtime down to 605ms!
I'm using the following XSLT template for replacing a few special characters in a string with their escaped variants; it calls itself recursively using a divide-and-conquer strategy, eventually looking at every single character in a given string. It then decides whether the character should be printed as it is, or whether any form of escaping is necessary:
<xsl:template name="escape-text">
<xsl:param name="s" select="."/>
<xsl:param name="len" select="string-length($s)"/>
<xsl:choose>
<xsl:when test="$len >= 2">
<xsl:variable name="halflen" select="round($len div 2)"/>
<xsl:variable name="left">
<xsl:call-template name="escape-text">
<xsl:with-param name="s" select="substring($s, 1, $halflen)"/>
<xsl:with-param name="len" select="$halflen"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="right">
<xsl:call-template name="escape-text">
<xsl:with-param name="s" select="substring($s, $halflen + 1)"/>
<xsl:with-param name="len" select="$halflen"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="concat($left, $right)"/>
</xsl:when>
<xsl:otherwise>
<xsl:choose>
<xsl:when test="$s = '"'">
<xsl:text>"\""</xsl:text>
</xsl:when>
<xsl:when test="$s = '#'">
<xsl:text>"#"</xsl:text>
</xsl:when>
<xsl:when test="$s = '|'">
<xsl:text>"|"</xsl:text>
</xsl:when>
<xsl:when test="$s = '#'">
<xsl:text>"#"</xsl:text>
</xsl:when>
<xsl:when test="$s = '\'">
<xsl:text>"\\"</xsl:text>
</xsl:when>
<xsl:when test="$s = '}'">
<xsl:text>"}"</xsl:text>
</xsl:when>
<xsl:when test="$s = '&'">
<xsl:text>"&"</xsl:text>
</xsl:when>
<xsl:when test="$s = '^'">
<xsl:text>"^"</xsl:text>
</xsl:when>
<xsl:when test="$s = '~'">
<xsl:text>"~"</xsl:text>
</xsl:when>
<xsl:when test="$s = '/'">
<xsl:text>"/"</xsl:text>
</xsl:when>
<xsl:when test="$s = '{'">
<xsl:text>"{"</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$s"/>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
This template accounts for the majority of runtime which my XSLT script needs. Replacing the above escape-text template with just
<xsl:template name="escape-text">
<xsl:param name="s" select="."/>
<xsl:value-of select="$s"/>
</xsl:template>
makes the runtime of my XSLT script go from 45 seconds to less than one seconds on one of my documents.
Hence my question: how can I speed up my escape-text template? I'm using xsltproc and I'd prefer a pure XSLT 1.0 solution. XSLT 2.0 solutions would be welcome too. However, external libraries might not be useful for this project - I'd still be interested in any solutions using them though.
Another (complementary) strategy would be to terminate the recursion early, before the string length is down to 1, if the condition translate($s, $vChars, '') = $s is true. This should give much faster processing of strings that contain no special characters at all, which is probably the majority of them. Of course the results will depend on how efficient xsltproc's implementation of translate() is.
A very small correction improved the speed in my tests about 17 times.
There are additional improvements, but I guess this will suffice for now ... :)
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="my:my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="vChars">"#|#\}&^~/{</xsl:variable>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="text()" name="escape-text">
<xsl:param name="s" select="."/>
<xsl:param name="len" select="string-length($s)"/>
<xsl:choose>
<xsl:when test="$len >= 2">
<xsl:variable name="halflen" select="round($len div 2)"/>
<xsl:variable name="left">
<xsl:call-template name="escape-text">
<xsl:with-param name="s" select="substring($s, 1, $halflen)"/>
<xsl:with-param name="len" select="$halflen"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="right">
<xsl:call-template name="escape-text">
<xsl:with-param name="s" select="substring($s, $halflen + 1)"/>
<xsl:with-param name="len" select="$halflen"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="concat($left, $right)"/>
</xsl:when>
<xsl:otherwise>
<xsl:choose>
<xsl:when test="not(contains($vChars, $s))">
<xsl:value-of select="$s"/>
</xsl:when>
<xsl:when test="$s = '"'">
<xsl:text>"\""</xsl:text>
</xsl:when>
<xsl:when test="$s = '#'">
<xsl:text>"#"</xsl:text>
</xsl:when>
<xsl:when test="$s = '|'">
<xsl:text>"|"</xsl:text>
</xsl:when>
<xsl:when test="$s = '#'">
<xsl:text>"#"</xsl:text>
</xsl:when>
<xsl:when test="$s = '\'">
<xsl:text>"\\"</xsl:text>
</xsl:when>
<xsl:when test="$s = '}'">
<xsl:text>"}"</xsl:text>
</xsl:when>
<xsl:when test="$s = '&'">
<xsl:text>"&"</xsl:text>
</xsl:when>
<xsl:when test="$s = '^'">
<xsl:text>"^"</xsl:text>
</xsl:when>
<xsl:when test="$s = '~'">
<xsl:text>"~"</xsl:text>
</xsl:when>
<xsl:when test="$s = '/'">
<xsl:text>"/"</xsl:text>
</xsl:when>
<xsl:when test="$s = '{'">
<xsl:text>"{"</xsl:text>
</xsl:when>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Here is a more improved version, based on #Dimitre's answer:
<xsl:template match="text()" name="escape-text">
<xsl:param name="s" select="."/>
<xsl:param name="len" select="string-length($s)"/>
<xsl:choose>
<xsl:when test="$len > 1">
<xsl:variable name="halflen" select="round($len div 2)"/>
<!-- no "left" and "right" variables necessary! -->
<xsl:call-template name="escape-text">
<xsl:with-param name="s" select="substring($s, 1, $halflen)"/>
</xsl:call-template>
<xsl:call-template name="escape-text">
<xsl:with-param name="s" select="substring($s, $halflen + 1)"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:choose>
<xsl:when test="not(contains($vChars, $s))">
<xsl:value-of select="$s"/>
</xsl:when>
<xsl:when test="contains('\"', $s)">
<xsl:value-of select="concat('"\', $s, '"')" />
</xsl:when>
<!-- all other cases can be collapsed, this saves some time -->
<xsl:otherwise>
<xsl:value-of select="concat('"', $s, '"')" />
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Should be another tiny bit faster, but I have not benchmarked it. In any case, it's shorter. ;-)
For what it's worth, here's my current version of the escape-text template which incorporates most of the (excellent!) suggestions which people have given in response to my question. For the record, my original version took about 45605ms on average on my sample DocBook document. After that, the runtime was decreased in multiple steps:
Removing the left and right variable together with the concat() call brought the runtime down to 13052ms; this optimization was taken from Tomalak's answer.
Moving the common case (which is: the given character doesn't need any special escaping) first in the inner <xsl:choose> element brought the runtime further down to 5812ms. This optimization was first suggested by Dimitre.
Aborting the recursion early by first testing whether the given string contains any of the special characters at all brought the runtime down to 612ms. This optimization was suggested by Michael.
Finally, I couldn't resist doing a micro optimization after reading a comment by Dimitre in Tomalak's answer: I replaced the <xsl:value-of select="concat('x', $s, 'y')"/> calls with <xsl:text>x</xsl:text><xsl:value-of select="$s"/><xsl:text>y</xsl:text>. This brought the runtime to about 606ms (so about 1% improvement).
In the end, the function took 606ms instead of 45605ms. Impressive!
<xsl:variable name="specialLoutChars">"#|#\}&^~/{</xsl:variable>
<xsl:template name="escape-text">
<xsl:param name="s" select="."/>
<xsl:param name="len" select="string-length($s)"/>
<xsl:choose>
<!-- Common case optimization:
no need to recurse if there are no special characters -->
<xsl:when test="translate($s, $specialLoutChars, '') = $s">
<xsl:value-of select="$s"/>
</xsl:when>
<!-- String length greater than 1, use DVC pattern -->
<xsl:when test="$len > 1">
<xsl:variable name="halflen" select="round($len div 2)"/>
<xsl:call-template name="escape-text">
<xsl:with-param name="s" select="substring($s, 1, $halflen)"/>
<xsl:with-param name="len" select="$halflen"/>
</xsl:call-template>
<xsl:call-template name="escape-text">
<xsl:with-param name="s" select="substring($s, $halflen + 1)"/>
<xsl:with-param name="len" select="$len - $halflen"/>
</xsl:call-template>
</xsl:when>
<!-- Special character -->
<xsl:otherwise>
<xsl:text>"</xsl:text>
<!-- Backslash and quot need backslash escape -->
<xsl:if test="$s = '"' or $s = '\'">
<xsl:text>\</xsl:text>
</xsl:if>
<xsl:value-of select="$s"/>
<xsl:text>"</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
How about using EXSLT? The String functions in EXSLT have a function called replace. I think it is something that is supported by quite a few XSLT implementations.
Update: I fixed this to actually work; now, it is not a speedup!
Building off #Wilfred's answer...
After fiddling with the EXSLT replace() function, I decided it was interesting enough to post another answer, even if it's not useful to the OP. It may well be useful to others.
It's interesting because of the algorithm: instead of the main algorithm worked on here (doing a binary recursive search, dividing in half at each recursion, pruned whenever a 2^nth substring has no special characters in it, and iterating over a choice of special characters when a length=1 string does contain a special character), Jeni Tennison's EXSLT algorithm puts the iteration over a set of search strings on the outside loop. Therefore on the inside of the loop, it is only searching for one string at a time, and can use substring-before()/substring-after() to divide the string, instead of blindly dividing in half.
[Deprecated: I guess that's enough to speed it up significantly. My tests show a speedup of 2.94x over #Dimitre's most recent one (avg. 230ms vs. 676ms).] I was testing using Saxon 6.5.5 in the Oxygen XML profiler. As input I used a 7MB XML document that was mostly a single text node, created from web pages about javascript, repeated. It sounds to me like that is representative of the task that the OP was trying to optimize. I'd be interested to see hear what results others get, with their test data and environments.
Dependencies
This uses an XSLT implementation of replace which relies on exsl:node-set(). It looks like xsltproc supports this extension function (possibly an early version of it). So this may work out-of-the-box for you, #Frerich; and for other processors, as it did with Saxon.
However if we want 100% pure XSLT 1.0, I think it would not be too hard to modify this replace template to work without exsl:node-set(), as long as the 2nd and 3rd params are passed in as nodesets, not RTFs.
Here is the code I used, which calls the replace template. Most of the length is taken up with the verbose way I created search/replace nodesets... that could probably be shortened. (But you can't make the search or replace nodes attributes, as the replace template is currently written. You'll get an error about trying to put attributes under the document element.)
<xsl:stylesheet version="1.0" xmlns:str="http://exslt.org/strings"
xmlns:foo="http://www.foo.net/something" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:import href="lars.replace.template.xsl"/>
<foo:replacements>
<replacement>
<search>"</search>
<replace>"\""</replace>
</replacement>
<replacement>
<search>\</search>
<replace>"\\"</replace>
</replacement>
<replacement>
<search>#</search>
<replace>"["</replace>
</replacement>
<replacement>
<search>|</search>
<replace>"["</replace>
</replacement>
<replacement>
<search>#</search>
<replace>"["</replace>
</replacement>
<replacement>
<search>}</search>
<replace>"}"</replace>
</replacement>
<replacement>
<search>&</search>
<replace>"&"</replace>
</replacement>
<replacement>
<search>^</search>
<replace>"^"</replace>
</replacement>
<replacement>
<search>~</search>
<replace>"~"</replace>
</replacement>
<replacement>
<search>/</search>
<replace>"/"</replace>
</replacement>
<replacement>
<search>{</search>
<replace>"{"</replace>
</replacement>
</foo:replacements>
<xsl:template name="escape-text" match="text()" priority="2">
<xsl:call-template name="str:replace">
<xsl:with-param name="string" select="."/>
<xsl:with-param name="search"
select="document('')/*/foo:replacements/replacement/search/text()"/>
<xsl:with-param name="replace"
select="document('')/*/foo:replacements/replacement/replace/text()"/>
</xsl:call-template>
</xsl:template>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
The imported stylesheet was originally this one.
However, as #Frerich pointed out, that never gave the correct output!
That ought to teach me not to post performance figures without checking for correctness!
I can see in a debugger where it's going wrong, but I don't know whether the EXSLT template never worked, or if it just doesn't work in Saxon 6.5.5... either option would be surprising.
In any case, EXSLT's str:replace() is specified to do more than we need, so I modified it so as to
require that the input parameters are already nodesets
as a consequence, not require exsl:node-set()
not sort the search strings by length (they're all one character, in this application)
not insert a replacement string between every pair of characters when the corresponding search string is empty
Here is the modified replace template:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:str="http://exslt.org/strings">
<!-- By Lars Huttar
based on implementation of EXSL str:replace() by Jenni Tennison.
http://www.exslt.org/str/functions/replace/str.replace.template.xsl
Modified by Lars not to need exsl:node-set(), not to bother sorting
search strings by length (in our application, all the search strings are of
length 1), and not to put replacements between every other character
when a search string is length zero.
Search and replace parameters must both be nodesets.
-->
<xsl:template name="str:replace">
<xsl:param name="string" select="''" />
<xsl:param name="search" select="/.." />
<xsl:param name="replace" select="/.." />
<xsl:choose>
<xsl:when test="not($string)" />
<xsl:when test="not($search)">
<xsl:value-of select="$string" />
</xsl:when>
<xsl:otherwise>
<xsl:variable name="search1" select="$search[1]" />
<xsl:variable name="replace1" select="$replace[1]" />
<xsl:choose>
<xsl:when test="contains($string, $search1)">
<xsl:call-template name="str:replace">
<xsl:with-param name="string"
select="substring-before($string, $search1)" />
<xsl:with-param name="search"
select="$search[position() > 1]" />
<xsl:with-param name="replace"
select="$replace[position() > 1]" />
</xsl:call-template>
<xsl:value-of select="$replace1" />
<xsl:call-template name="str:replace">
<xsl:with-param name="string"
select="substring-after($string, $search)" />
<xsl:with-param name="search" select="$search" />
<xsl:with-param name="replace" select="$replace" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="str:replace">
<xsl:with-param name="string" select="$string" />
<xsl:with-param name="search"
select="$search[position() > 1]" />
<xsl:with-param name="replace"
select="$replace[position() > 1]" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
One of the side benefits of this simpler template is that you could now use attributes for the nodes of your search and replace parameters. This would make the <foo:replacements> data more compact and easier to read IMO.
Performance: With this revised template, the job gets done in about 2.5s, vs. my 0.68s for my recent tests of the leading competitor, #Dimitre's XSLT 1.0 stylesheet. So it's not a speedup. But again, others have had very different test results than I have, so I'd like to hear what others get with this stylesheet.
After #Frerich-Raabe published a community wiki answer which combines the suggestions so far and achieves (on his data) a speedup of 76 times -- big congratulations to everybody!!!
I couldn't resist not to go further:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:variable name="specialLoutChars">"#|#\}&^~/{</xsl:variable>
<xsl:key name="kTextBySpecChars" match="text()"
use="string-length(translate(., '"#|#\}&^~/', '') = string-length(.))"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="text()[key('kTextBySpecChars', 'true')]" name="escape-text">
<xsl:param name="s" select="."/>
<xsl:param name="len" select="string-length($s)"/>
<xsl:choose>
<xsl:when test="$len >= 2">
<xsl:variable name="halflen" select="round($len div 2)"/>
<xsl:call-template name="escape-text">
<xsl:with-param name="s" select="substring($s, 1, $halflen)"/>
<xsl:with-param name="len" select="$halflen"/>
</xsl:call-template>
<xsl:call-template name="escape-text">
<xsl:with-param name="s" select="substring($s, $halflen + 1)"/>
<xsl:with-param name="len" select="$len - $halflen"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="$len = 1">
<xsl:choose>
<!-- Common case: the character at hand needs no escaping at all -->
<xsl:when test="not(contains($specialLoutChars, $s))">
<xsl:value-of select="$s"/>
</xsl:when>
<xsl:when test="$s = '"' or $s = '\'">
<xsl:text>"\</xsl:text>
<xsl:value-of select="$s"/>
<xsl:text>"</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>"</xsl:text>
<xsl:value-of select="$s"/>
<xsl:text>"</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
This transformation achieves (on my data) a further speedup of 1.5 times. So the total speedup should be more than 100 times.
OK, I'll chip in. Though not as interesting as optimizing the XSLT 1.0 version, you did say that XSLT 2.0 solutions are welcome, so here's mine.
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template name="escape-text" match="text()" priority="2">
<xsl:variable name="regex1">[#|#}&^~/{]</xsl:variable>
<xsl:variable name="replace1">"$0"</xsl:variable>
<xsl:variable name="regex2">["\\]</xsl:variable>
<xsl:variable name="replace2">"\\$0"</xsl:variable>
<xsl:value-of select='replace(replace(., $regex2, $replace2),
$regex1, $replace1)'/>
</xsl:template>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
This just uses a regexp replace() to replace \ or " with "\" or "\"" respectively; composed with another regexp replace() to surround any of the other escapable characters with quotes.
In my tests, this performs worse than Dimitre's most recent XSLT 1.0 offering, by a factor of more than 2. (But I made up my own test data, and other conditions may be idiosyncratic, so I'd like to know what results others get.)
Why the slower performance? I can only guess it's because searching for regular expressions is slower than searching for fixed strings.
Update: using analyze-string
As per #Alejandro's suggestion, here it is using analyze-string:
<xsl:template name="escape-text" match="text()" priority="2">
<xsl:analyze-string select="." regex='([#|#}}&^~/{{])|(["\\])'>
<xsl:matching-substring>
<xsl:choose>
<xsl:when test="regex-group(1)">"<xsl:value-of select="."/>"</xsl:when>
<xsl:otherwise>"\<xsl:value-of select="."/>"</xsl:otherwise>
</xsl:choose>
</xsl:matching-substring>
<xsl:non-matching-substring><xsl:value-of select="."/></xsl:non-matching-substring>
</xsl:analyze-string>
</xsl:template>
While this seems like a good idea, unfortunately it does not give us a performance win: In my setup, it consistently takes about 14 seconds to complete, versus 1 - 1.4 sec for the replace() template above. Call that a 10-14x slowdown. :-( This suggests to me that breaking and concatenating lots of big strings at the XSLT level is a lot more expensive than traversing a big string twice in a built-in function.

MOSS Content Query Web part itemstyle.xsl

I have a Content Query Webpart (CQWP) pulling the URL and title from a News links list. The CQWP uses the XSLT style Orange.News.Links defined in ItemStyle.xsl.
I need to sort the title #Title0 field as commented out below because it causes an error.
Does anyone know whats causing this error? - Many Thanks. The XSLT code is below:
<xsl:template name="Orange.News.Links" match="Row[#Style='Orange.News.Links']" mode="itemstyle">
<xsl:param name="CurPos" />
<xsl:param name="Last" />
<xsl:variable name="SafeLinkUrl">
<xsl:call-template name="OuterTemplate.GetSafeLink">
<xsl:with-param name="UrlColumnName" select="'LinkUrl'"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="DisplayTitle">
<xsl:call-template name="OuterTemplate.GetTitle">
<xsl:with-param name="Title" select="#URL"/>
<xsl:with-param name="UrlColumnName" select="'URL'"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="LinkTarget">
<xsl:if test="#OpenInNewWindow = 'True'" >_blank</xsl:if>
</xsl:variable>
<xsl:variable name="SafeImageUrl">
<xsl:call-template name="OuterTemplate.GetSafeStaticUrl">
<xsl:with-param name="UrlColumnName" select="'ImageUrl'"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="Header">
<xsl:if test="$CurPos = 1">
<![CDATA[<ul class="list_Links">]]>
</xsl:if>
</xsl:variable>
<xsl:variable name="Footer">
<xsl:if test="$Last = $CurPos">
<![CDATA[</ul>]]>
</xsl:if>
</xsl:variable>
<xsl:value-of select="$Header" disable-output-escaping="yes" />
<li>
<a>
<xsl:attribute name="href">
<xsl:value-of select="substring-before($DisplayTitle,', ')"></xsl:value-of>
</xsl:attribute>
<xsl:attribute name="title">
<xsl:value-of select="#Description"/>
</xsl:attribute>
<!-- <xsl:sort select="#Title0"/> -->
<xsl:value-of select="#Title0">
</xsl:value-of>
</a>
</li>
<xsl:value-of select="$Footer" disable-output-escaping="yes" />
</xsl:template>
I need to sort the title #Title0 field
as commented out below because it
causes an error.
Does anyone know whats causing this
error?
Yes, <xsl:sort/> can only be a child node of either <xsl:apply-templates> or <xsl:for-each> (and of <xsl:perform-sort> in XSLT 2.0).
Recommendation: Take at least a mini-course in XSLT and XPath so that you at least grok the fundamental concepts.

How to format a string to Pascal case in XSLT?

I'm trying to format strings in XSLT that needs to be in pascal case to be used appropriately for the application I'm working with.
For example:
this_text would become ThisText
this_long_text would become ThisLongText
Is it possible to also set this up where I can send an input to the format so I do not have to recreate the format multiple times?
This transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="vLower" select=
"'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="vUpper" select=
"'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="text()">
<xsl:call-template name="Pascalize">
<xsl:with-param name="pText" select="concat(., '_')"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="Pascalize">
<xsl:param name="pText"/>
<xsl:if test="$pText">
<xsl:value-of select=
"translate(substring($pText,1,1), $vLower, $vUpper)"/>
<xsl:value-of select="substring-before(substring($pText,2), '_')"/>
<xsl:call-template name="Pascalize">
<xsl:with-param name="pText"
select="substring-after(substring($pText,2), '_')"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
when applied on this XML document:
<t>
<a>this_text</a>
<b>this_long_text</b>
</t>
produces the desired result:
<t>
<a>ThisText</a>
<b>ThisLongText</b>
</t>
BTW, this is camelCase and this is PascalCase
Here, two years after the fact, is an XSLT 2.0 solution:
<xsl:function name="fn:pascal-case">
<xsl:param name="string"/>
<xsl:value-of select="string-join(for $s in tokenize($string,'\W+') return concat(upper-case(substring($s,1,1)),substring($s,2)),'')"/>
</xsl:function>
It will pascalize either 'this_long_text' or 'this-long-text' to 'ThisLongText' because it breaks on any non-word characters.
In the regex flavors I am most familiar with (perl, pcre, etc.), an underscore is considered part of the '\w' character class (therefore not part of \W), but for XSLT 2.0 the XSD datatypes are used (http://www.w3.org/TR/xmlschema-2/) and '\w' is defined as:
[#x0000-#x10FFFF]-[\p{P}\p{Z}\p{C}] (all characters except the set of "punctuation", "separator" and "other" characters)
so '\W' includes an underscore.
This version worked for me. I added a choose that outputs "the rest" of the string when no more underbars are present.
<xsl:variable name="vLower" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="vUpper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:template name="Pascalize">
<xsl:param name="pText" />
<xsl:if test="$pText">
<xsl:value-of select="translate(substring($pText,1,1), $vLower, $vUpper)" />
<xsl:choose>
<xsl:when test="contains($pText, '_')">
<xsl:value-of select="substring-before(substring($pText,2), '_')" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring($pText,2)" />
</xsl:otherwise>
</xsl:choose>
<xsl:call-template name="Pascalize">
<xsl:with-param name="pText" select="substring-after(substring($pText,2), '_')" />
</xsl:call-template>
</xsl:if>
</xsl:template>
Also, in case anyone comes here looking for the reverse process (which I happened to also require today and could find not a single example of anywhere)...
<xsl:variable name="vLower" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="vUpper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:template name="TitleCase">
<xsl:param name="pText" />
<xsl:call-template name="TitleCase_recurse">
<xsl:with-param name="pText" select="concat(translate(substring($pText,1,1), $vLower, $vUpper), substring($pText,2))" />
</xsl:call-template>
</xsl:template>
<xsl:template name="TitleCase_recurse">
<xsl:param name="pText" />
<xsl:if test="string-length($pText) > 1">
<xsl:if test="not(substring($pText,1,1) = ' ' and substring($pText,1,1) = ' ')">
<xsl:value-of select="substring($pText,1,1)" />
</xsl:if>
<xsl:if test="translate(substring($pText,1,1), $vLower, $vUpper) != substring($pText,1,1)">
<xsl:if test="translate(substring($pText,2,1), $vLower, $vUpper) = substring($pText,2,1)">
<xsl:text> </xsl:text>
</xsl:if>
</xsl:if>
<xsl:call-template name="TitleCase_recurse">
<xsl:with-param name="pText" select="substring($pText,2)" />
</xsl:call-template>
</xsl:if>
<xsl:if test="string-length($pText) = 1">
<xsl:value-of select="$pText" />
</xsl:if>
</xsl:template>
I love it when my subconscious brain pops up an answer a few hours after I've completely given up consciously. ;-)
I was trying to achieve the "pascalizing" with the following XLST function call:
<xsl:value-of select="fn:replace(#name,'_(\w{1})','\U$1')"/>
Unfortunately the processor throws the error message "Invalid replacement string in replace():
\ character must be followed by \ or $"
the problem is the \U modifier which is supposed to do the uppercase conversion of the matched pattern. If I change it to
<xsl:value-of select="fn:replace(#name,'_(\w{1})','\\U$1')"/>
the output string contains the sequence '\U' because it is now esacped - but I don't want to escape it, I want it do be effective ;-) . I did test
<xsl:value-of select="fn:replace(#name,'_(\w{1})','$1')"/>
(without converting the match to uppercase) and that works fine. But of course it does no uppercasing, just removes underscores and replaces the letter after the underscore by itself instead of capitalizing it. Am I doing something wrong here or is the \U modifier simply not supported in the regex implementation of my XSLT processor?
Thanks to Dimitre, I was able to get most of the way there. When running my strings through the Pascalize template, the bit after the last '_' was cut off. There's probably a cleaner way of doing it, but here's the code I used:
<xsl:template name="Pascalize">
<xsl:param name="pText"/>
<xsl:if test="$pText">
<xsl:value-of select="translate(substring($pText,1,1), $vLower, $vUpper)"/>
<xsl:value-of select="substring-before(substring($pText,2), '_')"/>
<xsl:call-template name="Pascalize">
<xsl:with-param name="pText" select="substring-after(substring($pText,2), '_')"/>
</xsl:call-template>
<xsl:call-template name="GrabLastPart">
<xsl:with-param name="pText" select="$pText"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="GrabLastPart">
<xsl:param name="pText"/>
<xsl:choose>
<xsl:when test="contains($pText, '_')">
<xsl:call-template name="GrabLastPart">
<xsl:with-param name="pText" expr="substring-after($pText, '_')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring($pText, 2)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>

Resources