How to get the last index of a char in a string? - xslt

I have the following task:
There is an xml element containing a long string. I need to use xsl to transform this element into a number of html <input> tags. It works like this: if the string is longer than an <input> field can hold without scrolling I call the same template recursively to create another input field with the remainig text.
The problem is that the string is often splitted right in the middle of the word which is not nice.
So I need to find the position of the last space character that is not bigger than the size of the substring that fits into the <input> tag and print only the substring before it on the line.
So I prepare a substring of max length, that can fit in the field, but I have no idea of how to get the index of the last space in it and pass it as a parameter into the next call of the function.
UPD: here is what I've got so far
<xsl:template name="multilineInput">
<xsl:param name="input" select="."/>
<xsl:param name="maxFirst" select="."/>
<xsl:param name="firstLineWidth" select="."/>
<input>
<xsl:attribute name="readonly">readonly</xsl:attribute>
<xsl:attribute name="class">input_multiline</xsl:attribute>
<xsl:attribute name="style">width = "<xsl:value-of select="$firstLineWidth"/>"</xsl:attribute>
<xsl:attribute name="type">text</xsl:attribute>
<xsl:attribute name="value"><xsl:value-of select="substring($input, 1, $maxFirst)"/></xsl:attribute>
</input>
<xsl:if test="$maxFirst < string-length($input)">
<xsl:call-template name="multilineInput">
<xsl:with-param name="input" select="substring($input, $maxFirst+1, string-length($input)-$maxFirst)"/>
<xsl:with-param name="maxFirst" select="110"/>
<xsl:with-param name="firstLineWidth" select="'980'"/>
</xsl:call-template>
</xsl:if>
</xsl:template>

The following recursive template can be used to return the last-index of a given delimiter:
<xsl:template name="last-index-of">
<xsl:param name="txt"/>
<xsl:param name="remainder" select="$txt"/>
<xsl:param name="delimiter" select="' '"/>
<xsl:choose>
<xsl:when test="contains($remainder, $delimiter)">
<xsl:call-template name="last-index-of">
<xsl:with-param name="txt" select="$txt"/>
<xsl:with-param name="remainder" select="substring-after($remainder, $delimiter)"/>
<xsl:with-param name="delimiter" select="$delimiter"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="lastIndex" select="string-length(substring($txt, 1, string-length($txt)-string-length($remainder)))+1"/>
<xsl:choose>
<xsl:when test="string-length($remainder)=0">
<xsl:value-of select="string-length($txt)"/>
</xsl:when>
<xsl:when test="$lastIndex>0">
<xsl:value-of select="($lastIndex - string-length($delimiter))"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="0"/>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
It can be invoked like this:
<xsl:call-template name="last-index-of">
<xsl:with-param name="txt" select="'The quick brown fox jumped over the lazy dog.'"/>
<xsl:with-param name="delimiter" select="' '"></xsl:with-param>
</xsl:call-template>
And returns: 41
You can assign the result of the template call to a variable like this:
<xsl:variable name="last-index">
<xsl:call-template name="last-index-of">
<xsl:with-param name="txt" select="'The quick brown fox jumped over the lazy dog.'"/>
<xsl:with-param name="delimiter" select="' '"></xsl:with-param>
</xsl:call-template>
</xsl:variable>

This transformation imlements a simple and efficient algorithm: the last space in a string is the first space in the reversed string:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:call-template name="lastCharIndex">
<xsl:with-param name="pText" select=
"'The quick brown fox jumped over the lazy dog.'"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="lastCharIndex">
<xsl:param name="pText"/>
<xsl:param name="pChar" select="' '"/>
<xsl:variable name="vRev">
<xsl:call-template name="reverse">
<xsl:with-param name="pStr" select="$pText"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select=
"string-length($pText) - string-length(substring-before($vRev, $pChar))"/>
</xsl:template>
<xsl:template name="reverse">
<xsl:param name="pStr"/>
<xsl:variable name="vLength" select="string-length($pStr)"/>
<xsl:choose>
<xsl:when test="$vLength = 1"><xsl:value-of select="$pStr"/></xsl:when>
<xsl:otherwise>
<xsl:variable name="vHalfLength" select="floor($vLength div 2)"/>
<xsl:variable name="vrevHalf1">
<xsl:call-template name="reverse">
<xsl:with-param name="pStr"
select="substring($pStr, 1, $vHalfLength)"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="vrevHalf2">
<xsl:call-template name="reverse">
<xsl:with-param name="pStr"
select="substring($pStr, $vHalfLength+1)"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="concat($vrevHalf2, $vrevHalf1)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on any XML document (not used), the wanted, correct result is produced:
41

You could use the EXSLT split template (description; implementation) or tokenize to split the string up by spaces. Then you could print all the text nodes except the last one, and/or use the length of the last text node to get the index of the last space.
See also this template which you could adapt to your needs.

Related

Splitting XSLT variable in to a foreach loop (XSLT 1.0) [duplicate]

how to split a node value in XSLT 1.0?
<mark>1,2</mark>
i need to perform some operations in the for loop with each value of the output of split.
<xsl:for-each select="">
</xsl:for-each>
How to do this?
I. XSLT 1.0 solution:
Here is one way to do this in XSLT 1.0 using only the xxx:node-set() extension function:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common" exclude-result-prefixes="ext">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="mark">
<xsl:variable name="vrtfSplit">
<xsl:apply-templates/>
</xsl:variable>
<xsl:for-each select="ext:node-set($vrtfSplit)/*">
<processedItem>
<xsl:value-of select="10 * ."/>
</processedItem>
</xsl:for-each>
</xsl:template>
<xsl:template match="text()" name="split">
<xsl:param name="pText" select="."/>
<xsl:if test="string-length($pText) >0">
<item>
<xsl:value-of select=
"substring-before(concat($pText, ','), ',')"/>
</item>
<xsl:call-template name="split">
<xsl:with-param name="pText" select=
"substring-after($pText, ',')"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied to the following XML document:
<mark>1,2,3,4,5</mark>
The wanted, correct output (each item multiplied by 10) is produced:
<processedItem>10</processedItem>
<processedItem>20</processedItem>
<processedItem>30</processedItem>
<processedItem>40</processedItem>
<processedItem>50</processedItem>
II. XSLT 2.0 solution:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="mark">
<xsl:for-each select="tokenize(., ',')">
<processedItem>
<xsl:sequence select="10*xs:integer(.)"/>
</processedItem>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
The explaination by Dimitre Novatchev is awesome, but we can also do it in much more simpler way without using node-set() function have a look:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="delimiter">
<xsl:text>,</xsl:text>
</xsl:variable>
<xsl:template match="mark">
<xsl:variable name="dataList">
<xsl:value-of select="."/>
</xsl:variable>
<xsl:call-template name="processingTemplate">
<xsl:with-param name="datalist" select="$dataList"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="processingTemplate">
<xsl:param name="datalist"/>
<xsl:choose>
<xsl:when test="contains($datalist,$delimiter) ">
<xsl:element name="processedItem">
<xsl:value-of select="substring-before($datalist,$delimiter) * 10"/>
</xsl:element>
<xsl:call-template name="processingTemplate">
<xsl:with-param name="datalist" select="substring-after($datalist,$delimiter)"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="string-length($datalist)=1">
<xsl:element name="processedItem">
<xsl:value-of select="$datalist * 10"/>
</xsl:element>
</xsl:when>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
In 1.0 you need to write a recursive template - except you don't, because it's already been written. Download the str:tokenize template from http://www.exslt.org.
If you can use exslt there's a tokenize() function that will do this nicely.
node-set str:tokenize(string, string?)
See http://www.exslt.org/str/functions/tokenize/
This code will split a delimited string in XSLT 1.0
(It will work for 2.0, but don't use the node-set.)
It will also optionally suppress empty elements in the string
or optionally upper case the elements.
<!-- Example delimited string. -->
<xsl:variable name="delimitedString" select="'a, b, c, , , d, e, f, g'"/>
<!-- Create a node set where each node contains one of the elements from the
delimited string. -->
<xsl:variable name="splitNodes">
<xsl:call-template name="getNodeListFromDelimitedList">
<xsl:with-param name="inStrList" select="$delimitedString"/>
<xsl:with-param name="delimiter" select="','"/>
<xsl:with-param name="suppressEmptyElements" select="false()"/>
<xsl:with-param name="upperCase" select="false()"/>
<xsl:with-param name="allTrim" select="false()"/>
</xsl:call-template>
</xsl:variable>
<!-- Use this for XSLT 1.0 only. -->
<xsl:variable name="splitNodesList" select="msxml:node-set($splitNodes)"/>
<!-- Use the split node list to do something. For example, create a string like
the delimited string, but without the delimiters. -->
<xsl:variable name="nonDelimitedString">
<xsl:for-each select="$splitNodesList/element">
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:variable>
<!-- Do something with the nonDelimitedString. -->
<!--
*****************************************************************************************
This template converts a delimited string list to a node list as follows:
Each value in the delimited input string is extracted from the string. Then, a node is
created to contain the value. The name of the node is 'element', and it is added to the
list. To use this template, create an variable and call this template from within the variable.
If you are using XSLT version 1.0, convert the node list to a node set using the node-set
function. You can access the element as follows: $SomeVariableNodeSet/element
*****************************************************************************************
-->
<xsl:template name="getNodeListFromDelimitedList">
<!-- Delimited string with one or more delimiters. -->
<xsl:param name="inStrList"/>
<!-- The delimiter. -->
<xsl:param name="delimiter" select="'|'"/>
<!-- Set to true to suppress empty elements from being added to node list. Otherwise, set to 'false'.-->
<xsl:param name="suppressEmptyElements" select="true()"/>
<!-- Set to true to upper case the strings added to the node list. -->
<xsl:param name="upperCase" select="false()"/>
<!-- Set to true to left trim and right trim the strings added to the nodes list. -->
<xsl:param name="allTrim" select="false()"/>
<xsl:variable name="element">
<xsl:choose>
<xsl:when test="contains($inStrList,$delimiter)">
<xsl:value-of select="substring-before($inStrList,$delimiter)"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$inStrList"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<!-- Write out the element based on parameters. -->
<xsl:if test="not($suppressEmptyElements) or normalize-space($element) != ''">
<!-- Put the element in the list. -->
<xsl:element name="element">
<xsl:choose>
<xsl:when test="$allTrim">
<xsl:call-template name="all-trim">
<xsl:with-param name="inStr" select="$element"/>
<xsl:with-param name="upperCase" select="$upperCase"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="$upperCase">
<xsl:value-of select="translate($element, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$element"/>
</xsl:otherwise>
</xsl:choose>
</xsl:element>
</xsl:if>
<xsl:if test="contains($inStrList,$delimiter)">
<!-- Call template recursively to process the next element. -->
<xsl:call-template name="getNodeListFromDelimitedList">
<xsl:with-param name="inStrList" select="substring-after($inStrList,$delimiter)"/>
<xsl:with-param name="delimiter" select="$delimiter"/>
<xsl:with-param name="suppressEmptyElements" select="$suppressEmptyElements"/>
<xsl:with-param name="upperCase" select="$upperCase"/>
<xsl:with-param name="allTrim" select="$allTrim"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<!--
*****************************************************************************************
This template trims the blanks from the left and right sides of a string.
*****************************************************************************************
-->
<xsl:template name="all-trim">
<!-- The string that you want to all trim. -->
<xsl:param name="inStr"/>
<xsl:param name="upperCase" select="false()"/>
<xsl:variable name="leftTrimmed">
<xsl:call-template name="left-trim">
<xsl:with-param name="inStr" select="$inStr"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="rightTrimmed">
<xsl:call-template name="right-trim">
<xsl:with-param name="inStr" select="$leftTrimmed"/>
</xsl:call-template>
</xsl:variable>
<xsl:choose>
<xsl:when test="$upperCase">
<xsl:value-of select="translate($rightTrimmed, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$rightTrimmed"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!--
*****************************************************************************************
This template trims the blanks from the left side of a string.
*****************************************************************************************
-->
<xsl:template name="left-trim">
<!-- The string you want to left trim. -->
<xsl:param name ="inStr"/>
<xsl:choose>
<xsl:when test="$inStr!=''">
<xsl:variable name="temp" select="substring($inStr, 1, 1)"/>
<xsl:choose>
<xsl:when test="$temp=' '">
<xsl:choose>
<xsl:when test="string-length($inStr) > 1">
<xsl:call-template name="left-trim">
<xsl:with-param name="inStr" select="substring($inStr, 2, string-length($inStr)-1)"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="''"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$inStr"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="''"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!--
*****************************************************************************************
This template trims the blanks from the right side of a string.
*****************************************************************************************
-->
<xsl:template name="right-trim">
<!-- The string you want to right trim. -->
<xsl:param name ="inStr"/>
<xsl:choose>
<xsl:when test="$inStr!=''">
<xsl:variable name="temp" select="substring($inStr, string-length($inStr), 1)"/>
<xsl:choose>
<xsl:when test="$temp=' '">
<xsl:choose>
<xsl:when test="string-length($inStr) > 1">
<xsl:call-template name="right-trim">
<xsl:with-param name="inStr" select="substring($inStr, 1, string-length($inStr)-1)"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="''"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$inStr"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="''"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Based on #Abhinav solution I just simplified recursive solution to work with general strings. My input string which I need to split is "GEN_EME2_G9_3311|A55;GEN_EME2_G9_3312|A55;foooo_3312|A42"
<xsl:variable name="delimiter">
<xsl:text>;</xsl:text>
</xsl:variable>
<xsl:template name="fooTemplate">
...
<xsl:choose>
<xsl:when test="$conditionlink != ''">
<xsl:call-template name="processconditionlinktemplate">
<xsl:with-param name="datalist" select="$conditionlink"/>
</xsl:call-template>
</xsl:when>
</xsl:choose>
...
</xsl:template>
<xsl:template name="processconditionlinktemplate">
<xsl:param name="datalist"/>
<xsl:choose>
<xsl:when test="contains($datalist,$delimiter)">
<xsl:element name="processedItem">
<xsl:value-of select="substring-before($datalist,$delimiter)"/>
</xsl:element>
<xsl:call-template name="processconditionlinktemplate">
<xsl:with-param name="datalist" select="substring-after($datalist,$delimiter)"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:element name="processedItem">
<xsl:value-of select="$datalist"/>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:template>

Count unique values in comma separated value in xslt 1.0

I have a node in an XML file:
<TEST_STRING>12,13,12,14</TEST_STRING>
I need to count how many unique numbers/values this string has. For example, in this case there are 2 unique values i.e. 13 and 14.
Honestly speaking i could not build anything yet. It seems it is difficult in XSLT 1.0 but my system only supports 1.0.
Is there any solution for it?
If your processor supports a node-set extension function (either exslt or the Microsoft msxsl one) then you can do it in two steps, first split the string and build an XML fragment with one element per value, then use normal XPath techniques to find the singletons.
Step one can be done with a tail-recursive template:
<xsl:template name="splitString">
<xsl:param name="str"/>
<xsl:choose>
<xsl:when test="contains($str, ',')">
<item><xsl:value-of select="substring-before($str, ',')"/></item>
<xsl:call-template name="splitString">
<xsl:with-param name="str" select="substring-after($str, ',')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<item><xsl:value-of select="$str"/></item>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
In the appropriate place you can call this as
<xsl:variable name="itemsRTF">
<xsl:call-template name="splitString">
<xsl:with-param name="str" select="TEST_STRING"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="items" select="exsl:node-set($itemsRTF)"/>
The items variable now contains a fragment of XML like
<item>12</item>
<item>13</item>
<item>12</item>
<item>14</item>
The next challenge is to find the singletons, which you can do with an expression like
$items/item[not(. = (preceding-sibling::item | following-sibling::item))]
(There are more efficient approaches using a key but for small numbers of items it's probably not worth the bother). So to count the singletons
<xsl:value-of select="count($items/item[not(. = (preceding-sibling::item | following-sibling::item))])"/>
It's possible without nodesets. It's not optimal, of course (hello, Shlemiel The Painter), but it's pretty easy - increment counter only if there is no given string ahead or behind.
Template
<xsl:template name="calcUnique">
<xsl:param name="str"/>
<xsl:param name="back"/>
<xsl:param name="count"/>
<xsl:if test="$str">
<xsl:choose>
<xsl:when test="contains($str, ',')">
<xsl:variable name="part" select="substring-before($str, ',')"/>
<xsl:call-template name="calcUnique">
<xsl:with-param name="str" select="substring-after($str, ',')"/>
<xsl:with-param name="back" select="concat($back, ',', $part)"/>
<xsl:with-param name="count">
<xsl:choose>
<xsl:when test="contains(concat($str, ','), concat(',', $part, ',')) or contains(concat($back, ','), concat(',', $part, ','))">
<xsl:value-of select="$count"/>
</xsl:when>
<xsl:otherwise><xsl:value-of select="$count + 1"/></xsl:otherwise>
</xsl:choose>
</xsl:with-param>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:choose>
<xsl:when test="contains(concat($back, ','), concat(',', $str, ','))">
<xsl:value-of select="$count"/>
</xsl:when>
<xsl:otherwise><xsl:value-of select="$count + 1"/></xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>
Sample usage
<xsl:variable name="result">
<xsl:call-template name="calc">
<xsl:with-param name="str" select="TEST_STRING"/>
<xsl:with-param name="back" select="''"/>
<xsl:with-param name="count" select="0"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="$result"/>

XSLT recursive substitution of string parameters

I am quite new to XSLT,
I have a source XML message, which in its simplified version looks something like this:
<?xml version='1.0' encoding='iso-8859-1'?>
<Message>
<Invalid>
<InvalidBody>
<SynchError>
<ErrorText>The value of %1 should not be %2.</ErrorText>
<ErrorParameter>
<!-- Error Parameter is %1 identifier -->
<ErrorParameterType>value</ErrorParameterType>
<ErrorParameterValue>someField</ErrorParameterValue>
</ErrorParameter>
<ErrorParameter>
<!-- Error Parameter is %2 identifier -->
<ErrorParameterType>value</ErrorParameterType>
<ErrorParameterValue>someValue</ErrorParameterValue>
</ErrorParameter>
</SynchError>
</InvalidBody>
</Invalid>
</Message>
Now, I would like to use XSLT 1.0 to extract the ErrorText string and substitute the parameters %1 and %2 with the corresponding ErrorParameter/ErrorParameterValue values. The number of parameters %1, %2, %3... cannot be known in advance, so the solution should be flexible enough to accommodate a variable number of parameters.
Is there any elegant way to do this?
So, after quite a lot of googling around and a healthy dose of headache, I came up with the following solution, which seems to work like a charm:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/">
<xsl:variable name="err-text" select="/Message/Invalid/InvalidBody/SynchError/ErrorText" />
<xsl:variable name="param-count" select="count(/Message/Invalid/InvalidBody/SynchError/ErrorParameter)" />
<xsl:call-template name="replace-params">
<xsl:with-param name="position" select="$param-count"/>
<xsl:with-param name="source-text" select="$err-text" />
</xsl:call-template>
</xsl:template>
<xsl:template name="replace-params">
<xsl:param name="position"/>
<xsl:param name="source-text"/>
<xsl:choose>
<xsl:when test="$position = 0">
<xsl:value-of select="$source-text"/>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="replace-params">
<xsl:with-param name="position" select="$position - 1"/>
<xsl:with-param name="source-text">
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text" select="$source-text" />
<xsl:with-param name="replace" select="concat('%', $position)" />
<xsl:with-param name="by" select="/Message/Invalid/InvalidBody/SynchError/ErrorParameter[$position]/ErrorParameterValue" />
</xsl:call-template>
</xsl:with-param>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="string-replace-all">
<xsl:param name="text" />
<xsl:param name="replace" />
<xsl:param name="by" />
<xsl:choose>
<xsl:when test="contains($text, $replace)">
<xsl:value-of select="substring-before($text,$replace)" />
<xsl:value-of select="$by" />
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text"
select="substring-after($text,$replace)" />
<xsl:with-param name="replace" select="$replace" />
<xsl:with-param name="by" select="$by" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$text" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
I use the "string-replace-all" template as a substitute for the XSLT 2.0 replace() function, since I cannot exclude multiple occurrences of a single parameter.
The "replace-params" template is applied recursively on the original text, iterating backwards on the index of the set of ErrorParameters.
The way I have tackled similar problems is to create a named template that recurses through the string (text element of) <ErrorText> with each cycle picking out the first n% item, then dereferences the <ErrorParameter> to access the contents of that and store in a result, then snip off the n% item just process and calls itself to grab the next one. When there are no more %n items left, return the result.
Here's an example of that, this template basically counts comma-separated items in a parameter passed in on the first cycle called List, and returns a $Count when it's finished.
<xsl:template name="CountList">
<xsl:param name="List"/>
<xsl:param name="Count" select="0"/>
<xsl:choose>
<xsl:when test="contains($List,',') = false()">
<xsl:value-of select="$Count"/>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="CountList">
<xsl:with-param name="List">
<xsl:value-of select="substring-after($List,',')"/>
</xsl:with-param>
<xsl:with-param name="Count">
<xsl:value-of select="$Count + 1"/>
</xsl:with-param>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
You can try something like this. You can use a replace instead of 'substring-before' and 'after' if this function is supported.
<xsl:template match="SynchError">
<xsl:apply-templates select="ErrorParameter[1]">
<xsl:with-param name="text"><xsl:value-of select="ErrorText"/></xsl:with-param>
<xsl:with-param name="position">1</xsl:with-param>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="ErrorParameter">
<xsl:param name="text"/>
<xsl:param name="position"/>
<xsl:apply-templates select="following::ErrorParameter">
<xsl:with-param name="position"><xsl:value-of select="number($position)+1"/></xsl:with-param>
<xsl:with-param name="text"><xsl:value-of select="concat(substring-before($text,concat('%',$position)),ErrorParameterValue,substring-after($text,concat('%',$position)))"/></xsl:with-param>
</xsl:apply-templates>
<xsl:if test="not(following::ErrorParameter)">
<xsl:value-of select="concat(substring-before($text,concat('%',$position)),ErrorParameterValue,substring-after($text,concat('%',$position)))"/>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Here's another way you could look at it:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<output>
<xsl:call-template name="merge">
<xsl:with-param name="string" select="Message/Invalid/InvalidBody/SynchError/ErrorText"/>
<xsl:with-param name="parameters" select="Message/Invalid/InvalidBody/SynchError/ErrorParameter"/>
</xsl:call-template>
</output>
</xsl:template>
<xsl:template name="merge">
<xsl:param name="string"/>
<xsl:param name="parameters"/>
<xsl:param name="flag" select="'%'"/>
<xsl:choose>
<xsl:when test="contains($string, $flag)">
<xsl:variable name="subsequent-char"
select="substring(translate(substring-after($string, $flag), '0123456789', ''), 1, 1)"/>
<xsl:variable name="i"
select="substring-before(substring-after($string, $flag), $subsequent-char)" />
<xsl:value-of select="substring-before($string, $flag)"/>
<xsl:value-of select="$parameters[number($i)]/ErrorParameterValue"/>
<!-- recursive call -->
<xsl:call-template name="merge">
<xsl:with-param name="string" select="substring-after($string, concat($flag, $i))"/>
<xsl:with-param name="parameters" select="$parameters"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$string"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>

Calculating string length of text node when parent has other child nodes too

I am stuck with XSLT 1.0 making a hyphenation script for verbatim output to PDF. (note it is PDF so I can't use clever CSS to hellp me out.
I would like to be able to access both text() nodes and child text() nodes to compare stringlength as part of the script.
For example:
<programlisting>This text will be displayed without word-wrap
I can measure length of string between end line chars '
'
and break long lines. That works great until
<emphasis>this gets added</emphasis> then my counting loop doesn't
count the text in the emphasis tags.
</programlisting>
I am looping through the text() using a template called screen. It detects the end of line character and decides if the line is too long. Long lines are broken and the remainder recursed through the template until the remainder is less than the max line length... then onto the next line of text...
... that works great, but now i have some children in the and I can't figure out how I can access the text() and any child node's text() at the same time to calculate string length.
Sorry for the long example code...
The template to match looks like this...
<xsl:template match="programlisting/text()">
<xsl:variable name="max_width">100</xsl:variable>
<xsl:variable name="min_width">80</xsl:variable>
<xsl:call-template name="screen">
<xsl:with-param name="text" select="."/>
<xsl:with-param name="max_width" select="$max_width"/>
<xsl:with-param name="min_width" select="$min_width"/>
</xsl:call-template>
</xsl:template>
The template to decide if string before linebreak is too long:
<xsl:template name="screen">
<xsl:param name="text" select="."/>
<xsl:param name="max_width"/>
<xsl:param name="min_width"/>
<xsl:variable name="fixed_text"> <!-- add end linebreak if missing -->
<xsl:choose>
<xsl:when test="substring($text,string-length($text),1) = '
'">
<xsl:value-of select="$text"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat($text,'
')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="first_line">
<xsl:value-of select="substring-before(concat($fixed_text,'
'),'
')"/>
</xsl:variable>
<xsl:variable name="other_lines">
<xsl:value-of select="substring-after($fixed_text,concat($first_line,'
'))"/>
</xsl:variable>
<xsl:choose>
<xsl:when test="string-length(normalize-space($first_line)) < 1"><!-- blank line (just trim and copy)-->
<xsl:value-of select="concat($verbatim_padding,substring($first_line,1,100),'
')"/>
</xsl:when>
<xsl:when test="string-length($first_line) < 100"> <!-- short line (just copy)-->
<xsl:value-of select="concat($verbatim_padding,$first_line,'
')"/>
</xsl:when>
<xsl:otherwise>
<!-- Line is too long!! -->
<xsl:variable name="wrapped_lines">
<xsl:call-template name="break_line">
<xsl:with-param name="long_string" select="$first_line"/>
<xsl:with-param name="max_chars" select="100"/>
<xsl:with-param name="min_chars" select="80"/>
<xsl:with-param name="break_chars" select="' ,/;'"/>
<xsl:with-param name="linebreak_string" select="'~'"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="concat($verbatim_padding,$wrapped_lines)"/>
</xsl:otherwise>
</xsl:choose>
<xsl:if test="string-length($other_lines) > 0">
<xsl:call-template name="screen">
<xsl:with-param name="text" select="$other_lines"/>
<xsl:with-param name="max_width" select="$max_width"/>
<xsl:with-param name="min_width" select="$min_width"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
The template to break up long lines:
<xsl:template name="break_line">
<xsl:param name="long_string"/>
<xsl:param name="max_chars"/> <!--max chars allowed on a line -->
<xsl:param name="min_chars"/> <!--max char position foa soft linebreak (else we hard break at the max chars!) -->
<xsl:param name="break_chars"/> <!-- chars used for soft breaking -->
<xsl:param name="linebreak_string"/> <!-- add this to end of a broken line -->
<xsl:choose>
<xsl:when test="(string-length($long_string) < $max_chars) or (string-length($long_string) < $min_chars) or (string-length($long_string) < 1)">
<xsl:value-of select="concat($long_string,'
')"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="trim_x_by">
<xsl:call-template name="CheckLastChar">
<xsl:with-param name="string" select="$long_string"/>
<xsl:with-param name="start" select="$max_chars"/>
<xsl:with-param name="stop" select="$min_chars"/>
<xsl:with-param name="break_chars" select="$break_chars"/>
<xsl:with-param name="max_chars" select="$max_chars"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="first_x_chars"><xsl:value-of select="substring($long_string,1,$trim_x_by)"/></xsl:variable>
<xsl:variable name="remaining_chars"><xsl:value-of select="substring-after($long_string,$first_x_chars)"/></xsl:variable>
<xsl:value-of select="concat($first_x_chars,' ',$linebreak_string)"/>
<xsl:call-template name="break_line">
<xsl:with-param name="long_string" select="$remaining_chars"/>
<xsl:with-param name="max_chars" select="$max_chars"/>
<xsl:with-param name="min_chars" select="$min_chars"/>
<xsl:with-param name="break_chars" select="$break_chars"/>
<xsl:with-param name="linebreak_string" select="$linebreak_string"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
And finally the recursive checking of string chars to see if its a safe place to line-break (soft break)
<xsl:template name="CheckLastChar">
<xsl:param name="string"/>
<xsl:param name="stop" select="1"/>
<xsl:param name="start" select="string-length($string)"/>
<xsl:param name="count" select="$start"/>
<xsl:param name="break_chars"/>
<xsl:param name="max_chars"/>
<xsl:choose>
<xsl:when test="($count < $stop) or (string-length($string) < $stop)">
<!-- gone so far into the line that a linebreak would look weird! So return the max-length and that will
force a 'hard' linebreak exactly at the max-length point regardless of the character -->
<xsl:value-of select="$max_chars"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="last_char"><xsl:value-of select="substring($string,$count,1)"/></xsl:variable>
<xsl:choose>
<xsl:when test="contains($break_chars,$last_char)">
<xsl:value-of select="$count"/>
</xsl:when>
<xsl:otherwise>
<!-- keep looking -->
<xsl:call-template name="CheckLastChar">
<xsl:with-param name="string" select="$string"/>
<xsl:with-param name="stop" select="$stop"/>
<xsl:with-param name="start" select="$start"/>
<xsl:with-param name="count" select="$count - 1"/>
<xsl:with-param name="break_chars" select="$break_chars"/>
<xsl:with-param name="max_chars" select="$max_chars"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
To get all of the text in an element (including its descendants' text), just use string(Element):
<xsl:template match="programlisting">
<xsl:variable name="max_width">100</xsl:variable>
<xsl:variable name="min_width">80</xsl:variable>
<xsl:call-template name="screen">
<xsl:with-param name="text" select="string(.)"/>
<xsl:with-param name="max_width" select="$max_width"/>
<xsl:with-param name="min_width" select="$min_width"/>
</xsl:call-template>
</xsl:template>
Note the removal of text() from the match attribute.
XPath Spec
The string-value of an element node is the concatenation of the string-values of all text node descendants of the element node in document order.
The string() function converts an object to a string as follows:
A node-set is converted to a string by returning the string-value of the node in the node-set that is first in document order. If the node-set is empty, an empty string is returned.

How to limit string word count in XSLT 1.0?

How can I limit a string's word count in XSLT 1.0?
How about something like:
<xsl:template match="data"> <!-- your data element or whatever -->
<xsl:call-template name="firstWords">
<xsl:with-param name="value" select="."/>
<xsl:with-param name="count" select="4"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="firstWords">
<xsl:param name="value"/>
<xsl:param name="count"/>
<xsl:if test="number($count) >= 1">
<xsl:value-of select="concat(substring-before($value,' '),' ')"/>
</xsl:if>
<xsl:if test="number($count) > 1">
<xsl:variable name="remaining" select="substring-after($value,' ')"/>
<xsl:if test="string-length($remaining) > 0">
<xsl:call-template name="firstWords">
<xsl:with-param name="value" select="$remaining"/>
<xsl:with-param name="count" select="number($count)-1"/>
</xsl:call-template>
</xsl:if>
</xsl:if>
</xsl:template>
This is an XSLT 1.0 solution:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common"
>
<xsl:import href="strSplit-to-Words.xsl"/>
<xsl:output indent="yes" omit-xml-declaration="yes"/>
<xsl:template match="/">
<xsl:variable name="vwordNodes">
<xsl:call-template name="str-split-to-words">
<xsl:with-param name="pStr" select="/"/>
<xsl:with-param name="pDelimiters"
select="',
()-'"/>
</xsl:call-template>
</xsl:variable>
<xsl:call-template name="strTakeWords">
<xsl:with-param name="pN" select="10"/>
<xsl:with-param name="pText" select="/*"/>
<xsl:with-param name="pWords"
select="ext:node-set($vwordNodes)/*"/>
</xsl:call-template>
</xsl:template>
<xsl:template match="word" priority="10">
<xsl:value-of select="concat(position(), ' ', ., '
')"/>
</xsl:template>
<xsl:template name="strTakeWords">
<xsl:param name="pN" select="10"/>
<xsl:param name="pText"/>
<xsl:param name="pWords"/>
<xsl:param name="pResult"/>
<xsl:choose>
<xsl:when test="not($pN > 0)">
<xsl:value-of select="$pResult"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="vWord" select="$pWords[1]"/>
<xsl:variable name="vprecDelims" select=
"substring-before($pText,$pWords[1])"/>
<xsl:variable name="vnewText" select=
"concat($vprecDelims, $vWord)"/>
<xsl:call-template name="strTakeWords">
<xsl:with-param name="pN" select="$pN -1"/>
<xsl:with-param name="pText" select=
"substring-after($pText, $vnewText)"/>
<xsl:with-param name="pWords" select=
"$pWords[position() > 1]"/>
<xsl:with-param name="pResult" select=
"concat($pResult, $vnewText)"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the following XML document:
<t>
(CNN) -- Behind closed doors in recent days,
senior White House aides have been saying that
measuring President Obama's first 100 days
is the journalistic equivalent of a Hallmark holiday.
</t>
the wanted result is returned:
(CNN) -- Behind closed doors in recent days,
senior White House
Do note:
The str-split-to-words template from FXSL is used for tokenization.
This template accepts a parameter pDelimiters which is a string consisting of all characters that should be treated as delimiters. Thus, in contrast with other solutions, it is possible to specify every delimiter (and not just a "space") -- in this case 8 of them.
The named template strTakeWords calls itself recursively to accumulate the text before and including every word from the wordlist produced by the tokenization, until the specified number of words has been processed.