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'])" />
In my xsl, I want to match on several nodes and then grab the value of the matched node into a variable. How can I do that?
I need to place a wildcard variable in place of Budget0 below:
<xsl:template match="FieldRef[#Name='Budget0' or #Name='Scope' or #Name='Risk' or #Name='Schedule']" mode="body">
<xsl:param name="thisNode" select="."/>
<xsl:variable name="currentValue" select="$thisNode/Budget0" />
<xsl:variable name="statusRating1">(1)</xsl:variable>
<xsl:variable name="statusRating2">(2)</xsl:variable>
<xsl:variable name="statusRating3">(3)</xsl:variable>
<xsl:choose>
<xsl:when test="contains($currentValue, $statusRating1)">
<span class="statusRatingX statusRating1"></span>
</xsl:when>
<xsl:when test="contains($currentValue, $statusRating2)">
<span class="statusRatingX statusRating2"></span>
</xsl:when>
<xsl:when test="contains($currentValue, $statusRating3)">
<span class="statusRatingX statusRating3"></span>
</xsl:when>
<xsl:otherwise>
<span class="statusRatingN"><xsl:value-of select="$currentValue" /></span>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
In this snippet, the xsl:template match... works just fine; it does seem to match those fields. I can see in Firebug that those fields receive the statusRating1 css class just like they should (since they are all set to receive the value of the Budget0 field.
[update]
I found that if I use this for the variable:
<xsl:variable name="currentValue" select="current()/#Name" />
or
<xsl:variable name="currentValue" select="FieldRef[#Name=current()/#Name"] />
It will get caught in the otherwise tag, and will print the name of the field. In other words, the html prints
<span class="statusRatingN">Budget0</span>
If I try any of Dimitre's solutions (below), it never matches in any of the when clauses, and the html is outputted like this (notice the span's text is blank):
<span class="statusRatingN"></span>
Therefore, I deduce that the $currentValue is only getting the name of the attribute, it isn't referring to value of the node. I need to refer to the value of that particular node.
Use:
<xsl:variable name="currentValue" select="$thisNode/*[name()=current()/#Name]"/>
Or, alternatively:
<xsl:variable name="currentValue" select="$thisNode/*[name()=$thisNode/#Name]"/>
Or, alternatively (best):
<xsl:variable name="currentValue" select="*[name()=current()/#Name]"/>
Ah, after hours and hours and hours and days and months, here is an example (which I fiddled myself with help from this thread):
These two lines are the keys (Dimitre's answer was close):
<xsl:param name="thisNode" select="."/>
<xsl:variable name="currentValue" select="$thisNode/#*[name()=current()/#Name]" />
Here is the entire function, which reads two different columns and applies the values to either one:
<xsl:template match="FieldRef[#Name='YesNo1']|FieldRef[#Name='YesNo2']" mode="body">
<xsl:param name="thisNode" select="."/>
<xsl:variable name="currentValue" select="$thisNode/#*[name()=current()/#Name]" />
<xsl:variable name="yesvalue">Yes</xsl:variable>
<xsl:variable name="novalue">No</xsl:variable>
<xsl:choose>
<xsl:when test="contains($currentValue, $yesvalue)">
<span class="yesno yes"><xsl:value-of select="$currentValue" /></span>
</xsl:when>
<xsl:when test="contains($currentValue, $novalue)">
<span class="yesno no"><xsl:value-of select="$currentValue" /></span>
</xsl:when>
<xsl:otherwise>
<span class="yesnoN"><xsl:value-of select="$currentValue" /></span>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
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.
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 can I create a link to the rest of the paragraph in an announcement in SharePoint, that displays the word: read more
Cheers
I've found that the cleanest, easiest way to do this is to create a template in ItemStyle.xsl which selects a substring of the body content of the announcement and displays a link below to the article itself.
After adding the following code to your ItemStyle.xsl file (in SharePoint Designer navigate to the 'Style Library/XSL Style Sheets' folder), you can modify the web part through the browser, and change the Item Style (Presentation/Styles) to 'ReadMoreAnnouncements'. This code keeps the amount of characters displayed to 190 characters (see the substring($bodyContent,1,190 function call).
<xsl:template name="removeMarkup">
<xsl:param name="string" />
<xsl:choose>
<xsl:when test="contains($string, '<')">
<xsl:variable name="nextString">
<xsl:call-template name="removeMarkup">
<xsl:with-param name="string" select="substring-after($string, '>')" />
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="concat(substring-before($string, '<'), $nextString)" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$string" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="ReadMoreAnnouncements" match="Row[#Style='ReadMoreAnnouncements']" mode="itemstyle">
<br />
<div class="RMAnnouncementsTitle">
<xsl:value-of select="#Title" />
</div>
<div class="RMAnnouncementsBody">
<xsl:variable name="bodyContent">
<xsl:call-template name="removeMarkup">
<xsl:with-param name="string" select="#Body"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="substring($bodyContent,1,190)" />
...
<br />
<a>
<xsl:attribute name="href">
/Lists/Announcements/DispForm.aspx?ID=
<xsl:value-of select="#ID">
</xsl:value-of>
</xsl:attribute>
<xsl:attribute name="class">
RMAnnouncementsMoreLink
</xsl:attribute>
read more
</a>
</div>
</xsl:template>
This should definitely work, and it's very very easy to implement.
If you have a page and can edit it in SharePoint Designer, try this.
If you want to have a web part that shows the announcement the way you want, try this.