My question is How to un-escape xml that has already been escaped.
I tried the code provided by Tomalak in response to How to unescape XML characters with help of XSLT?, but I can't get that to do what I want.
I have SoapMsg Xml. The body contains a few elements one of which is a String. This string
contains Escaped XML. This is often done in RPC SoapMsg because they don't allow complex
types. To Get around this they embed Escaped-Xml inside a String Element, see sXmlParameters in the input below.
Example Input:
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:pan="http://wsdl.somebody.com/Stuff/">
<soap:Header />
<soap:Body>
<pan:SomeCommand>
<first>eefbb52a0fee443cbda838caffbc2654</first>
<second>f26eb2f5dabc457ca045e64585f7b185</second>
<sXmlParameters><PARAMETERS><TIMEOUTDATETIME>2011-03-15
2:09:48.997</TIMEOUTDATETIME></PARAMETERS></sXmlParameters>
</pan:SomeCommand>
</soap:Body>
</soap:Envelope>
I also see this data escaped with <![CDATA[>]]>, I need to un-escape it also.
Converted Output:
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:pan="http://wsdl.somebody.com/Stuff/">
<soap:Header />
<soap:Body>
<pan:SomeCommand>
<first>eefbb52a0fee443cbda838caffbc2654</first>
<second>f26eb2f5dabc457ca045e64585f7b185</second>
<sXmlParameters>
<PARAMETERS>
<TIMEOUTDATETIME>2011-03-15 2:09:48.997</TIMEOUTDATETIME>
</PARAMETERS>
</sXmlParameters>
</pan:SomeCommand>
</soap:Body>
</soap:Envelope>
This will already take care of half of your problem – not the CDATA part:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="//sXmlParameters">
<xsl:copy>
<xsl:call-template name="unescape">
<xsl:with-param name="escaped" select="string(.)"/>
</xsl:call-template>
</xsl:copy>
</xsl:template>
<xsl:template name="unescape">
<xsl:param name="escaped"/>
<xsl:choose>
<xsl:when test="contains($escaped,'<')">
<xsl:variable name="beforeelem" select="substring-before($escaped,'<')"/>
<xsl:variable name="elemname1" select="substring-before(substring-after($escaped,'<'),' ')"/>
<xsl:variable name="elemname2" select="substring-before(substring-after($escaped,'<'),'>')"/>
<xsl:variable name="elemname3" select="substring-before(substring-after($escaped,'<'),'/>')"/>
<xsl:variable name="hasattributes" select="string-length($elemname1) > 0 and ((string-length($elemname2)=0 or string-length($elemname1) < string-length($elemname2)) and (string-length($elemname3)=0 or string-length($elemname1) < string-length($elemname3)))"/>
<xsl:variable name="elemclosed" select="string-length($elemname3) > 0 and (string-length($elemname2)=0 or string-length($elemname3) < string-length($elemname2))"/>
<xsl:variable name="elemname">
<xsl:choose>
<xsl:when test="$hasattributes">
<xsl:value-of select="$elemname1"/>
</xsl:when>
<xsl:when test="not($elemclosed)">
<xsl:value-of select="$elemname2"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$elemname3"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="elemclosetag" select="concat('</',$elemname,'>')"/>
<xsl:variable name="innercontent">
<xsl:if test="not($elemclosed)">
<xsl:call-template name="skipper-before">
<xsl:with-param name="source" select="substring-after(substring-after($escaped,'<'),'>')"/>
<xsl:with-param name="delimiter" select="$elemclosetag"/>
</xsl:call-template>
</xsl:if>
</xsl:variable>
<xsl:variable name="afterelem">
<xsl:choose>
<xsl:when test="not($elemclosed)">
<xsl:call-template name="skipper-after">
<xsl:with-param name="source" select="substring-after(substring-after($escaped,'<'),'>')"/>
<xsl:with-param name="delimiter" select="$elemclosetag"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring-after(substring-after($escaped,'<'),'/>')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:element name="{$elemname}">
<xsl:if test="$hasattributes">
<xsl:call-template name="unescapeattributes">
<xsl:with-param name="escapedattributes">
<xsl:choose>
<xsl:when test="not($elemclosed)">
<xsl:value-of select="normalize-space(substring-after($elemname2,' '))"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="normalize-space(substring-after($elemname3,' '))"/>
</xsl:otherwise>
</xsl:choose>
</xsl:with-param>
</xsl:call-template>
</xsl:if>
<xsl:call-template name="unescape">
<xsl:with-param name="escaped" select="$innercontent"/>
</xsl:call-template>
</xsl:element>
<xsl:call-template name="unescape">
<xsl:with-param name="escaped" select="$afterelem"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="unescapetext">
<xsl:with-param name="escapedtext" select="$escaped"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="unescapeattributes">
<xsl:param name="escapedattributes"/>
<xsl:variable name="attrname" select="substring-before($escapedattributes,'=')"/>
<xsl:variable name="attrquote" select="substring($escapedattributes,string-length($attrname)+2,1)"/>
<xsl:variable name="attrvalue" select="substring-before(substring-after($escapedattributes,$attrquote),$attrquote)"/>
<xsl:variable name="afterattr" select="substring-after(substring-after($escapedattributes,$attrquote),$attrquote)"/>
<xsl:attribute name="{$attrname}">
<xsl:call-template name="unescapetext">
<xsl:with-param name="escapedtext" select="$attrvalue"/>
</xsl:call-template>
</xsl:attribute>
<xsl:if test="contains($afterattr,'=')">
<xsl:call-template name="unescapeattributes">
<xsl:with-param name="escapedattributes" select="normalize-space($afterattr)"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="unescapetext">
<xsl:param name="escapedtext"/>
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text">
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text">
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text" select="$escapedtext"/>
<xsl:with-param name="replace">></xsl:with-param>
<xsl:with-param name="by">></xsl:with-param>
</xsl:call-template>
</xsl:with-param>
<xsl:with-param name="replace"><</xsl:with-param>
<xsl:with-param name="by"><</xsl:with-param>
</xsl:call-template>
</xsl:with-param>
<xsl:with-param name="replace">&</xsl:with-param>
<xsl:with-param name="by">&</xsl:with-param>
</xsl:call-template>
</xsl:template>
<!-- replaces substrings in strings -->
<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>
<!-- returns the substring after the last delimiter -->
<xsl:template name="skipper-after">
<xsl:param name="source"/>
<xsl:param name="delimiter"/>
<xsl:choose>
<xsl:when test="contains($source,$delimiter)">
<xsl:call-template name="skipper-after">
<xsl:with-param name="source" select="substring-after($source,$delimiter)"/>
<xsl:with-param name="delimiter" select="$delimiter"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$source"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- returns the substring before the last delimiter -->
<xsl:template name="skipper-before">
<xsl:param name="source"/>
<xsl:param name="delimiter"/>
<xsl:param name="result"/>
<xsl:choose>
<xsl:when test="contains($source,$delimiter)">
<xsl:call-template name="skipper-before">
<xsl:with-param name="source" select="substring-after($source,$delimiter)"/>
<xsl:with-param name="delimiter" select="$delimiter"/>
<xsl:with-param name="result">
<xsl:if test="result!=''">
<xsl:value-of select="concat($result,$delimiter)"/>
</xsl:if>
<xsl:value-of select="substring-before($source,$delimiter)"/>
</xsl:with-param>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$result"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
I found that I can use saxon to do this in a much simpler way using the following:
<xsl:template match="SomeCommand">
<sXmlParameters>
<xsl:apply-templates select="saxon:parse(.)" />
</sXmlParameters>
</xsl:template>
there is also saxon:seriralize() that can be used to escape the xml
thanks to all for you input
Wrote a SAX parser for xml-escaped strings in pure xsl 1.0+EXSLT
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:pxml="https://github.com/ilyakharlamov/pure-xsl/parseStringAsXML"
version="1.0">
<xsl:import href="https://raw.githubusercontent.com/ilyakharlamov/pure-xsl/master/parseStringAsXML.xsl"/>
<xsl:template match="/">
<xsl:call-template name="pxml:parseStringAsXML">
<xsl:with-param name="string"><PARAMETERS><TIMEOUTDATETIME>2011-03-152:09:48.997</TIMEOUTDATETIME></PARAMETERS></xsl:with-param>
</xsl:call-template>
</xsl:template>
</xsl:stylesheet>
Output:
<PARAMETERS>
<TIMEOUTDATETIME>2011-03-152:09:48.997</TIMEOUTDATETIME>
</PARAMETERS>
Related
Given the following XML:
<application>
<documentation>Before text.
[This|http://google.com] is a link.
Click [here|http://google.com] for another link.
After text.
</documentation>
</application>
I want to replace new lines with <br/> tags and create links. The outcome should produce the following HTML:
Before text.<br/>
This is a link.<br/>
Click here for another link.<br/>
After text.
I have the XSLT below with the appropriate templates. My problem is that the <br/> elements added by the first function are stripped out by the second function. I've tried using an identity transform, but I can't wrap my head around it.
Any help would be appreciated.
My current XSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" version="1.0"/>
<xsl:template match="application">
<p>
<xsl:variable name="with_new_lines">
<xsl:call-template name="replace-newlines">
<xsl:with-param name="text" select="documentation"/>
<xsl:with-param name="replace" select="'
'"/>
<xsl:with-param name="by" select="'new_line'"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="with_links">
<xsl:call-template name="create-links">
<xsl:with-param name="text">
<xsl:copy-of select="$with_new_lines"/>
</xsl:with-param>
</xsl:call-template>
</xsl:variable>
<xsl:copy-of select="$with_links"/>
</p>
</xsl:template>
<xsl:template name="replace-newlines">
<xsl:param name="text"/>
<xsl:choose>
<xsl:when test="contains($text, '
')">
<xsl:copy-of select="substring-before($text,'
')"/>
<br/>
<xsl:call-template name="replace-newlines">
<xsl:with-param name="text" select="substring-after($text,'
')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="$text"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- Changing [here|http://a.com] to hereand
[http://a.com] to http://a.com-->
<xsl:template match="*" name="create-links">
<xsl:param name="text" select="."/>
<xsl:choose>
<xsl:when test="contains($text, 'http')">
<xsl:copy-of select="substring-before($text,'[')"/>
<xsl:variable name="the_link_and_after">
<xsl:copy-of select="substring-after($text,'[')"/>
</xsl:variable>
<xsl:variable name="the_link">
<xsl:copy-of select="substring-before($the_link_and_after,']')"/>
</xsl:variable>
<xsl:choose>
<xsl:when test="contains($the_link,'|')">
<xsl:variable name="the_url">
<xsl:copy-of select="substring-after($the_link,'|')"/>
</xsl:variable>
<a href="{$the_url}">
<xsl:copy-of select="substring-before($the_link,'|')"/>
</a>
</xsl:when>
<xsl:otherwise>
<a href="{$the_link}">
<xsl:copy-of select="$the_link"/>
</a>
</xsl:otherwise>
</xsl:choose>
<!--<xsl:copy-of select="substring-after($text,']')" />-->
<xsl:call-template name="create-links">
<xsl:with-param name="text" select="substring-after($text,']')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="$text"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
The problem with your approach is that each of your named templates creates a result-tree-fragment, containing a mixture of text nodes and elements (either <br> or <a>). When you send the result to be processed as a string by the other template, only the text nodes survive the treatment.
I would suggest you start by tokenizing the input into lines as elements. Then send each line in turn to be processed by the other template:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="html" encoding="utf-8"/>
<xsl:template match="application">
<xsl:variable name="lines">
<xsl:call-template name="tokenize-lines">
<xsl:with-param name="text" select="documentation"/>
</xsl:call-template>
</xsl:variable>
<p>
<xsl:for-each select="exsl:node-set($lines)/line">
<xsl:call-template name="create-links">
<xsl:with-param name="text" select="."/>
</xsl:call-template>
<br/>
</xsl:for-each>
</p>
</xsl:template>
<xsl:template name="tokenize-lines">
<xsl:param name="text"/>
<xsl:choose>
<xsl:when test="contains($text, '
')">
<line><xsl:copy-of select="substring-before($text,'
')"/></line>
<xsl:call-template name="tokenize-lines">
<xsl:with-param name="text" select="substring-after($text,'
')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<line><xsl:copy-of select="$text"/></line>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="create-links">
<xsl:param name="text"/>
<xsl:choose>
<xsl:when test="contains($text, 'http')">
<xsl:copy-of select="substring-before($text,'[')"/>
<xsl:variable name="the_link_and_after">
<xsl:copy-of select="substring-after($text,'[')"/>
</xsl:variable>
<xsl:variable name="the_link">
<xsl:copy-of select="substring-before($the_link_and_after,']')"/>
</xsl:variable>
<xsl:choose>
<xsl:when test="contains($the_link,'|')">
<xsl:variable name="the_url">
<xsl:copy-of select="substring-after($the_link,'|')"/>
</xsl:variable>
<a href="{$the_url}">
<xsl:copy-of select="substring-before($the_link,'|')"/>
</a>
</xsl:when>
<xsl:otherwise>
<a href="{$the_link}">
<xsl:copy-of select="$the_link"/>
</a>
</xsl:otherwise>
</xsl:choose>
<!--<xsl:copy-of select="substring-after($text,']')" />-->
<xsl:call-template name="create-links">
<xsl:with-param name="text" select="substring-after($text,']')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="$text"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Alternatively, you could call the 2nd template from within the 1st one, for each line separately before returning it:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" encoding="utf-8"/>
<xsl:template match="application">
<p>
<xsl:call-template name="create-lines">
<xsl:with-param name="text" select="documentation"/>
</xsl:call-template>
</p>
</xsl:template>
<xsl:template name="create-lines">
<xsl:param name="text"/>
<xsl:choose>
<xsl:when test="contains($text, '
')">
<xsl:call-template name="create-links">
<xsl:with-param name="text" select="substring-before($text,'
')"/>
</xsl:call-template>
<br/>
<xsl:call-template name="create-lines">
<xsl:with-param name="text" select="substring-after($text,'
')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="create-links">
<xsl:with-param name="text" select="$text"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="create-links">
<xsl:param name="text"/>
<xsl:choose>
<xsl:when test="contains($text, 'http')">
<xsl:copy-of select="substring-before($text,'[')"/>
<xsl:variable name="the_link_and_after">
<xsl:copy-of select="substring-after($text,'[')"/>
</xsl:variable>
<xsl:variable name="the_link">
<xsl:copy-of select="substring-before($the_link_and_after,']')"/>
</xsl:variable>
<xsl:choose>
<xsl:when test="contains($the_link,'|')">
<xsl:variable name="the_url">
<xsl:copy-of select="substring-after($the_link,'|')"/>
</xsl:variable>
<a href="{$the_url}">
<xsl:copy-of select="substring-before($the_link,'|')"/>
</a>
</xsl:when>
<xsl:otherwise>
<a href="{$the_link}">
<xsl:copy-of select="$the_link"/>
</a>
</xsl:otherwise>
</xsl:choose>
<!--<xsl:copy-of select="substring-after($text,']')" />-->
<xsl:call-template name="create-links">
<xsl:with-param name="text" select="substring-after($text,']')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="$text"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
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>
I'm working with an XML file that looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="align-test.xsl"?>
<alignTest>
<item name="Some Name" number="3"></item>
<item name="Another Name" number="10"></item>
<item name="A Third One" number="43"></item>
<item name="A Really Long Name" number="100"></item>
</alignTest>
My goal is to output a plain text file with a nicely formatted table in it. I've figured out how to align and pad text columns and a separater using this stylesheet:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:for-each select="alignTest/item">
<!-- Scroll right. The next line keeps going, but might not look like it due to formatting -->
<xsl:value-of select="substring(concat(#name, ' '), 1, 22)"/>
<xsl:text> | </xsl:text>
<xsl:value-of select="#number"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Which outputs:
Some Name | 3
Another Name | 10
A Third One | 43
A Really Long Name | 100
I'd also like the numeric values to be right justified. Like so:
Some Name | 3
Another Name | 10
A Third One | 43
A Really Long Name | 100
How can I use XSLT 1.0 to right justify plain-text in that way?
Here is a more readable and more efficient version of your answer:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:template match="item">
<xsl:call-template name="padRightSide">
<xsl:with-param name="stringToPad" select="#name"/>
<xsl:with-param name="totalLength" select="22"/>
</xsl:call-template>
<xsl:text>|</xsl:text>
<xsl:call-template name="padLeftSide">
<xsl:with-param name="stringToPad" select="#number"/>
<xsl:with-param name="totalLength" select="5"/>
</xsl:call-template>
<xsl:text>
</xsl:text>
</xsl:template>
<!-- template to pad the left side of strings (and right justificy) -->
<xsl:template name="padLeftSide">
<xsl:param name="stringToPad"/>
<xsl:param name="totalLength"/>
<xsl:param name="padChar" select="' '"/>
<xsl:param name="padBuffer" select=
"concat($padChar,$padChar,$padChar,$padChar,$padChar,
$padChar,$padChar,$padChar,$padChar,$padChar
)"/>
<xsl:variable name="vNewString" select=
"concat($padBuffer, $stringToPad)"/>
<xsl:choose>
<xsl:when test="not(string-length($vNewString) >= $totalLength)">
<xsl:call-template name="padLeftSide">
<xsl:with-param name="stringToPad" select="$vNewString"/>
<xsl:with-param name="totalLength" select="$totalLength"/>
<xsl:with-param name="padChar" select="$padChar"/>
<xsl:with-param name="padBuffer" select="$padBuffer"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select=
"substring($vNewString,
string-length($vNewString) - $totalLength + 1)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- template to pad the right side of strings -->
<xsl:template name="padRightSide">
<xsl:param name="totalLength"/>
<xsl:param name="padChar" select="' '"/>
<xsl:param name="stringToPad"/>
<xsl:param name="padBuffer" select=
"concat($padChar,$padChar,$padChar,$padChar,$padChar,
$padChar,$padChar,$padChar,$padChar,$padChar
)"/>
<xsl:variable name="vNewString" select=
"concat($stringToPad, $padBuffer)"/>
<xsl:choose>
<xsl:when test="not(string-length($vNewString) >= $totalLength)">
<xsl:call-template name="padRightSide">
<xsl:with-param name="stringToPad" select="$vNewString"/>
<xsl:with-param name="totalLength" select="$totalLength"/>
<xsl:with-param name="padChar" select="$padChar"/>
<xsl:with-param name="padBuffer" select="$padBuffer"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring($vNewString,1,$totalLength)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Another improvement is to dynamically calculate the maximum string-length and not to have to count it manually:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="vNamesMaxLen">
<xsl:call-template name="maxLength">
<xsl:with-param name="pNodes"
select="/*/item/#name"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="vNumsMaxLen">
<xsl:call-template name="maxLength">
<xsl:with-param name="pNodes"
select="/*/item/#number"/>
</xsl:call-template>
</xsl:variable>
<xsl:template match="item">
<xsl:call-template name="padRightSide">
<xsl:with-param name="stringToPad"
select="#name"/>
<xsl:with-param name="totalLength"
select="$vNamesMaxLen+1"/>
</xsl:call-template>
<xsl:text>|</xsl:text>
<xsl:call-template name="padLeftSide">
<xsl:with-param name="stringToPad"
select="#number"/>
<xsl:with-param name="totalLength"
select="$vNumsMaxLen+1"/>
</xsl:call-template>
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template name="maxLength">
<xsl:param name="pNodes" select="/.."/>
<xsl:for-each select="$pNodes">
<xsl:sort select="string-length()" data-type="number"
order="descending"/>
<xsl:if test="position() = 1">
<xsl:value-of select="string-length()"/>
</xsl:if>
</xsl:for-each>
</xsl:template>
<!-- template to pad the left side of strings (and right justificy) -->
<xsl:template name="padLeftSide">
<xsl:param name="stringToPad"/>
<xsl:param name="totalLength"/>
<xsl:param name="padChar" select="' '"/>
<xsl:param name="padBuffer" select=
"concat($padChar,$padChar,$padChar,$padChar,$padChar,
$padChar,$padChar,$padChar,$padChar,$padChar
)"/>
<xsl:variable name="vNewString" select=
"concat($padBuffer, $stringToPad)"/>
<xsl:choose>
<xsl:when test="not(string-length($vNewString) >= $totalLength)">
<xsl:call-template name="padLeftSide">
<xsl:with-param name="stringToPad" select="$vNewString"/>
<xsl:with-param name="totalLength" select="$totalLength"/>
<xsl:with-param name="padChar" select="$padChar"/>
<xsl:with-param name="padBuffer" select="$padBuffer"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select=
"substring($vNewString,
string-length($vNewString) - $totalLength + 1)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- template to pad the right side of strings -->
<xsl:template name="padRightSide">
<xsl:param name="totalLength"/>
<xsl:param name="padChar" select="' '"/>
<xsl:param name="stringToPad"/>
<xsl:param name="padBuffer" select=
"concat($padChar,$padChar,$padChar,$padChar,$padChar,
$padChar,$padChar,$padChar,$padChar,$padChar
)"/>
<xsl:variable name="vNewString" select=
"concat($stringToPad, $padBuffer)"/>
<xsl:choose>
<xsl:when test="not(string-length($vNewString) >= $totalLength)">
<xsl:call-template name="padRightSide">
<xsl:with-param name="stringToPad" select="$vNewString"/>
<xsl:with-param name="totalLength" select="$totalLength"/>
<xsl:with-param name="padChar" select="$padChar"/>
<xsl:with-param name="padBuffer" select="$padBuffer"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring($vNewString,1,$totalLength)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
I found a few answers on this page.
The simply way described is to do something like:
<xsl:value-of select="substring(concat(' ', #number), string-length(#number) + 1, 4)"/>
The page also lists a couple of templates for padding both on the left and the right. They call themselves recursively and pad the appropriate amount. (Note that they will also truncate if the requested length is less than that of the string being worked on.) The templates also off a feature of changing the character that is used for padding.
They take more code to implement, but might be easier to maintain. Here's a version of the original stylesheet updated with the two templates to show their usage:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:for-each select="alignTest/item">
<xsl:call-template name="padRightSide">
<xsl:with-param name="stringToPad" select="#name"></xsl:with-param>
<xsl:with-param name="totalLength" select="22"></xsl:with-param>
<xsl:with-param name="padCharacter" select="' '"></xsl:with-param>
</xsl:call-template>
<xsl:text>|</xsl:text>
<xsl:call-template name="padLeftSide">
<xsl:with-param name="stringToPad" select="#number"/>
<xsl:with-param name="totalLength" select="5"/>
<xsl:with-param name="padCharacteracter" select="' '"/>
</xsl:call-template>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
<!-- template to pad the left side of strings (and right justificy) -->
<xsl:template name="padLeftSide">
<xsl:param name="stringToPad"/>
<xsl:param name="totalLength"/>
<xsl:param name="padCharacteracter"/>
<xsl:choose>
<xsl:when test="string-length($stringToPad) < $totalLength">
<xsl:call-template name="padLeftSide">
<xsl:with-param name="stringToPad" select="concat($padCharacteracter,$stringToPad)"/>
<xsl:with-param name="totalLength" select="$totalLength"/>
<xsl:with-param name="padCharacteracter" select="$padCharacteracter"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring($stringToPad,string-length($stringToPad) - $totalLength + 1)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- template to pad the right side of strings -->
<xsl:template name="padRightSide">
<xsl:param name="padCharacter"/>
<xsl:param name="stringToPad"/>
<xsl:param name="totalLength"/>
<xsl:choose>
<xsl:when test="string-length($stringToPad) < $totalLength">
<xsl:call-template name="padRightSide">
<xsl:with-param name="padCharacter" select="$padCharacter"/>
<xsl:with-param name="stringToPad" select="concat($stringToPad,$padCharacter)"/>
<xsl:with-param name="totalLength" select="$totalLength"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring($stringToPad,1,$totalLength)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Of course, you could reduce the size of their footprint a little by hard coding the padding character.
I need to make a CSV-parser using XSLT 1.0. I have tried a recursive approach, but I can't seem to match the line endrings, so the input to the printLine-template is always empty.
<xsl:template match="/">
<xsl:call-template name="printLine">
<xsl:with-param name="line">
<xsl:value-of select="substring-before(//csvText, '\n')"/>
</xsl:with-param>
<xsl:with-param name="remaining">
<xsl:value-of select="substring-after(//csvText, '\n')"/>
</xsl:with-param>
</xsl:call-template>
</xsl:template>
<xsl:template name="printLine">
<xsl:param name="line"/>
<xsl:param name="remaining"/>
<xsl:value-of select="$line"/><br />
<xsl:if test="remaining != ''">
<xsl:call-template name="printLine">
<xsl:with-param name="line">
<xsl:value-of select="substring-before($remaining, '\n')"/>
</xsl:with-param>
<xsl:with-param name="remaining">
<xsl:value-of select="substring-after($remaining, '\n')"/>
</xsl:with-param>
</xsl:call-template>
</xsl:if>
</xsl:template>
I found a solution to this:
<xsl:variable name="ls"><xsl:text>
</xsl:text></xsl:variable>
<xsl:variable name="fs"><xsl:text> </xsl:text></xsl:variable>
<xsl:template match="/">
<xsl:call-template name="printLine">
<xsl:with-param name="line" select="substring-before(//csvText, $ls)"/>
<xsl:with-param name="remaining" select="substring-after(//csvText, $ls)"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="printLine">
<xsl:param name="line"/>
<xsl:param name="remaining"/>
<xsl:call-template name="printFields">
<xsl:with-param name="line" select="$line"/>
</xsl:call-template>
<xsl:if test="$remaining != ''">
<xsl:call-template name="printLine">
<xsl:with-param name="line" select="substring-before($remaining, $ls)"/>
<xsl:with-param name="remaining" select="substring-after($remaining, $ls)"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="printFields">
<xsl:param name="line"/>
<!-- render each line and access each field using the getFieldByIndex-template. Example: -->
<div>
<h3>
<xsl:call-template name="getFieldByIndex">
<xsl:with-param name="index" select="1"/>
<xsl:with-param name="line" select="$line"/>
</xsl:call-template>
</h3>
<p>
<xsl:call-template name="getFieldByIndex">
<xsl:with-param name="index" select="4"/>
<xsl:with-param name="line" select="$line"/>
</xsl:call-template>
</p>
</div>
</xsl:template>
<xsl:template name="getFieldByIndex">
<xsl:param name="index"/>
<xsl:param name="line"/>
<xsl:choose>
<xsl:when test="$index > 0">
<xsl:call-template name="getFieldByIndex">
<xsl:with-param name="index" select="$index -1"/>
<xsl:with-param name="line" select="substring-after($line, $fs)"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring-before($line,$fs)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
The line- and field separators are stored in the ls and fs variables. This example traverses a tab-serparated file. The printFields-template has to be customized for each use.
I need to copy a node and its sub-nodes:
first: one identical copy
followed by a modified copy with some attributes values changed
Here is the extract to change:
<Configuration
Name="Debug|Win32"
OutputDirectory=".\Debug"
IntermediateDirectory=".\Debug"
ATLMinimizesCRunTimeLibraryUsage="FALSE"
CharacterSet="2">
<Tool
Name="VCCLCompilerTool"
Optimization="0"
BasicRuntimeChecks="3"
RuntimeLibrary="1"
AssemblerListingLocation=".\Debug/"
ObjectFile=".\Debug/"
ProgramDataBaseFileName=".\Debug/"
WarningLevel="4"
SuppressStartupBanner="TRUE"
DebugInformationFormat="4"
CompileAs="0"/>
</Configuration>
In the second copy I need to change all "Debug" to "Release" and some attribute values also.
Thanks
Thanks Koynov
I found however a solution using template modes:
<xsl:template match="Configuration[#Name='Debug|Win32']">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
<xsl:copy>
<xsl:attribute name="WholeProgramOptimization">TRUE</xsl:attribute>
<xsl:apply-templates select="#*|node()" mode="Release"/>
</xsl:copy>
</xsl:template>
With this template for all attributes:
<xsl:template match="#*" mode="Release">
<xsl:attribute name="{local-name()}">
<xsl:call-template name="replace-string">
<xsl:with-param name="text" select="."/>
<xsl:with-param name="replace" select="'Debug'"/>
<xsl:with-param name="with" select="'Release'"/>
</xsl:call-template>
</xsl:attribute>
</xsl:template>
and of-course the "replace-string" template.
Thanks again.
Here is a xslt transformation for your needs:
<xsl:template match="Configuration">
<xsl:copy-of select="."/>
<xsl:call-template name="CopyWithReplace">
<xsl:with-param name="Node" select="."/>
<xsl:with-param name="PatternToReplace" select="string('Debug')"/>
<xsl:with-param name="ValueToReplaceWith" select="string('Release')"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="CopyWithReplace">
<xsl:param name="Node"/>
<xsl:param name="PatternToReplace"/>
<xsl:param name="ValueToReplaceWith"/>
<xsl:variable name="ReplacedNodeName">
<xsl:call-template name="SearchAndReplace">
<xsl:with-param name="input" select="name($Node)"/>
<xsl:with-param name="search-string" select="$PatternToReplace"/>
<xsl:with-param name="replace-string" select="$ValueToReplaceWith"/> </xsl:call-template>
</xsl:variable>
<xsl:element name="{$ReplacedNodeName}">
<xsl:for-each select="#*">
<xsl:variable name="ReplacedAttributeName">
<xsl:call-template name="SearchAndReplace">
<xsl:with-param name="input" select="name()"/>
<xsl:with-param name="search-string" select="$PatternToReplace"/>
<xsl:with-param name="replace-string" select="$ValueToReplaceWith"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="ReplacedAttributeValue">
<xsl:call-template name="SearchAndReplace">
<xsl:with-param name="input" select="."/>
<xsl:with-param name="search-string" select="$PatternToReplace"/>
<xsl:with-param name="replace-string" select="$ValueToReplaceWith"/>
</xsl:call-template>
</xsl:variable>
<xsl:attribute name="{$ReplacedAttributeName}">
<xsl:value-of select="$ReplacedAttributeValue"/>
</xsl:attribute>
</xsl:for-each>
<xsl:for-each select="child::*" >
<xsl:call-template name="CopyWithReplace">
<xsl:with-param name="Node" select="."/>
<xsl:with-param name="PatternToReplace" select="$PatternToReplace"/>
<xsl:with-param name="ValueToReplaceWith" select="$ValueToReplaceWith"/>
</xsl:call-template>
</xsl:for-each>
</xsl:element>
</xsl:template>
<xsl:template name="SearchAndReplace">
<xsl:param name="input"/>
<xsl:param name="search-string"/>
<xsl:param name="replace-string"/>
<xsl:choose>
<!-- See if the input contains the search string -->
<xsl:when test="$search-string and contains($input,$search-string)">
<!-- If so, then concatenate the substring before the search string to the replacement string
and to the result of recursively applying this template to the remaining substring. -->
<xsl:value-of select="substring-before($input,$search-string)"/>
<xsl:value-of select="$replace-string"/>
<xsl:call-template name="SearchAndReplace">
<xsl:with-param name="input" select="substring-after($input,$search-string)"/>
<xsl:with-param name="search-string" select="$search-string"/>
<xsl:with-param name="replace-string" select="$replace-string"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<!-- There are no more occurrences of the search string so just return the current input string -->
<xsl:value-of select="$input"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Good luck.