This is the mail archive of the xsl-list@mulberrytech.com mailing list .


Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]
Other format: [Raw text]

Functional trim() (Was: Re: text::trim())


>   Does anyone know if there is a function such as trim() that can be
> applied on a text node, i.e., remove all leading and trailing white-space
> ('\t', '\n', '\r', ' ', '\f'), given the text node can contain non
> white-space characters.

In Haskell one solution is the following:

> trim      :: [Char] -> [Char]
> trim      = applyTwice (reverse . trim1) 
>     where  trim1 = dropWhile (`elem` delim) 
>            delim    = [' ', '\t', '\n', '\r']
>            applyTwice f = f . f 

This means that we have two functions:

  - a simple function "trim1" that "eats out" the starting group of predefined
whitespace characters from a string. We could call this function "trimLeft". 
It uses the function "dropWhile", which drops from a list all starting elements
satisfying a predicate argument.

  - the function "reverse", the XSLT implementation of which can be found at:
http://aspn.activestate.com/ASPN/Mail/Message/XSL-List/926231

What is described above is quite simple -- we trim-left the string, then reverse it,
then trim-left the reversed string (at this point all the trimming is done), then
finally reverse the reversed and trimmed string.

For brevity the function composition operator "." is used:

(f . g) x = f(g(x))

We can implement exactly the same algorithm in XSLT:

trim.xsl:
--------
<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform";
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:myTrimDropController="f:myTrimDropController" 
xmlns:myTrim1="f:myTrim1" 
xmlns:myReverse="f:myReverse" 
exclude-result-prefixes="xsl myTrimDropController myTrim1 myReverse"
>
  <xsl:import href="str-dropWhile.xsl"/>
  <xsl:import href="compose-flist.xsl"/>
  <xsl:import href="reverse.xsl"/>
  
  <myTrimDropController:myTrimDropController/>
  
  <xsl:template name="trim">
    <xsl:param name="pStr"/>
    
    <xsl:variable name="vrtfParam">
      <myReverse:myReverse/>
      <myTrim1:myTrim1/>
      <myReverse:myReverse/>
      <myTrim1:myTrim1/>
    </xsl:variable>

    <xsl:call-template name="compose-flist">
        <xsl:with-param name="pFunList" select="msxsl:node-set($vrtfParam)/*"/>
        <xsl:with-param name="pArg1" select="$pStr"/>
    </xsl:call-template>
    
  </xsl:template>
  
  <xsl:template name="trim1" match="myTrim1:*">
    <xsl:param name="pArg1"/>
    
  <xsl:variable name="vTab" select="'&#9;'"/>
  <xsl:variable name="vNL" select="'&#10;'"/>
  <xsl:variable name="vCR" select="'&#13;'"/>
  <xsl:variable name="vWhitespace" 
                select="concat(' ', $vTab, $vNL, $vCR)"/>

    <xsl:variable name="vFunController" 
                  select="document('')/*/myTrimDropController:*[1]"/>
           
    
    <xsl:call-template name="str-dropWhile">
      <xsl:with-param name="pStr" select="$pArg1"/>
      <xsl:with-param name="pController" select="$vFunController"/>
      <xsl:with-param name="pContollerParam" select="$vWhitespace"/>
    </xsl:call-template>
  </xsl:template>
  
  <xsl:template match="myTrimDropController:*">
    <xsl:param name="pChar"/>
    <xsl:param name="pParams"/>
    
    <xsl:if test="contains($pParams, $pChar)">1</xsl:if>
  </xsl:template>
  
  <xsl:template name="myReverse" match="myReverse:*">
    <xsl:param name="pArg1"/>
    
    <xsl:call-template name="strReverse">
      <xsl:with-param name="pStr" select="$pArg1"/>
    </xsl:call-template>
</xsl:template>
</xsl:stylesheet>

The "trim" template is short and simple:

  <xsl:template name="trim">
    <xsl:param name="pStr"/>
    
    <xsl:variable name="vrtfParam">
      <myReverse:myReverse/>
      <myTrim1:myTrim1/>
      <myReverse:myReverse/>
      <myTrim1:myTrim1/>
    </xsl:variable>

    <xsl:call-template name="compose-flist">
        <xsl:with-param name="pFunList" select="msxsl:node-set($vrtfParam)/*"/>
        <xsl:with-param name="pArg1" select="$pStr"/>
    </xsl:call-template>
    
  </xsl:template>

It just calls the "compose-flist" template -- a generic template, which is passed a
list of template references "pFunList" and an initial argument "pArg1". It performs
the functional composition of the functions referred to by the template references
in reverse order. In this specific case it will instantiate the "trim1" template
passing to it the string contained in "pArg1", then will pass the result to the
"reverse" template, then will pass the result to the "trim1" template, then finally
will pass the result to the "reverse" template.

If we have the following source xml document:

<someText>
   
   This is    some text   
   
</someText>

and apply the following transformation to it:

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform";>

  <xsl:import href="trim.xsl"/>
  
  <xsl:output method="text"/>
  <xsl:template match="/">
    '<xsl:call-template name="trim">
        <xsl:with-param name="pStr" select="string(/*)"/>
    </xsl:call-template>'
  </xsl:template>
</xsl:stylesheet>

the result is:


    'This is    some text'
  
This is obviously the correct result -- the string is trimmed both from left and
from right, while white space within the string is left intact as it should be.


The "trim1" template is also very simple:

  <xsl:template name="trim1" match="myTrim1:*">
    <xsl:param name="pArg1"/>
    
  <xsl:variable name="vTab" select="'&#9;'"/>
  <xsl:variable name="vNL" select="'&#10;'"/>
  <xsl:variable name="vCR" select="'&#13;'"/>
  <xsl:variable name="vWhitespace" 
                select="concat(' ', $vTab, $vNL, $vCR)"/>

    <xsl:variable name="vFunController" 
                  select="document('')/*/myTrimDropController:*[1]"/>
           
    
    <xsl:call-template name="str-dropWhile">
      <xsl:with-param name="pStr" select="$pArg1"/>
      <xsl:with-param name="pController" select="$vFunController"/>
      <xsl:with-param name="pContollerParam" select="$vWhitespace"/>
    </xsl:call-template>
  </xsl:template>

It has a single parameter -- the string to be left-trimmed. It just calls the
standard and generic "str-dropWhile" template to do the processing, passing to it in
"pArg1" the string, from which some starting characters must be dropped, a function
"pController" (template reference to a template) that will signal if any individual
character passed to it is to be dropped, and parameter  "pContollerParam" to be
passed to this controller.

The code of the "drop controller" is straightforward:

  <xsl:template match="myTrimDropController:*">
    <xsl:param name="pChar"/>
    <xsl:param name="pParams"/>
    
    <xsl:if test="contains($pParams, $pChar)">1</xsl:if>
  </xsl:template>

It just tests if the current "pChar" character belongs to the group of characters
"pParams" and returns 1 if this is so, otherwise it doesn't return anything. In the
last case this will signal to "str-dropWhile" that all the necessary starting
characters have been dropped and the remainder of the string will be returned.


Finally, let me present the code of the two templates used by "trim" and "trim1" --
"compose-flist" and "str-dropWhile".

As already described, "compose-flist" performs the functional composition of a list
of functions:

(f1 . f2 .   ... fn)(x) = f1(f2(...(fn(x))...))

This can be defined in Haskell as follows:

 multCompose :: [a -> a] -> a -> a
 multCompose xs y  = foldr ($) y xs

where "$" is the function application operator, xs is a list of functions the
composition of which must be produced, and y is the the argument on which the
functional composition will be applied.

We could use an XSLT implementation of foldr in implementng compose-flist, however
this time I prefer to produce directly the code to make it more understandable:

compose-flist.xsl:
-----------------
<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform";
xmlns:msxsl="urn:schemas-microsoft-com:xslt">
  
  <xsl:template name="compose-flist">
    <xsl:param name="pFunList" select="/.."/>
    <xsl:param name="pArg1"/>
    
    <xsl:choose>
      <xsl:when test="not($pFunList)">
        <xsl:copy-of select="$pArg1"/>
      </xsl:when>
      <xsl:otherwise>
	<xsl:variable name="vrtfFunRest">
	  <xsl:call-template name="compose-flist">
            <xsl:with-param name="pFunList" select="$pFunList[position() > 1]"/>
	    <xsl:with-param name="pArg1" select="$pArg1"/>
	  </xsl:call-template>
	</xsl:variable>
	    
	<xsl:apply-templates select="$pFunList[1]">
	  <xsl:with-param name="pArg1" select="msxsl:node-set($vrtfFunRest)/node()"/>
	</xsl:apply-templates>
      </xsl:otherwise>
    </xsl:choose>

  </xsl:template>
</xsl:stylesheet>

It is worth to note that functional composition is one of the most powerful tools
with which we can create new functions dynamically on the fly.

And finally here's the code of "str-dropWhile":

str-dropWhile.xsl:
-----------------
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform";
xmlns:msxsl="urn:schemas-microsoft-com:xslt" 
exclude-result-prefixes="xsl msxsl"
>
  <xsl:template name="str-dropWhile">
    <xsl:param name="pStr" select="''"/>
    <xsl:param name="pController" select="/.."/>
    <xsl:param name="pContollerParam" select="/.."/>

    <xsl:if test="not($pController)">
      <xsl:message terminate="yes">
         [str-dropWhile]Error: pController not specified.
      </xsl:message>
    </xsl:if>   
    
      <xsl:if test="$pStr">
	<xsl:variable name="vDrop">
	  <xsl:apply-templates select="$pController">
	    <xsl:with-param name="pChar" select="substring($pStr, 1, 1)"/>
	    <xsl:with-param name="pParams" select="$pContollerParam"/>
	  </xsl:apply-templates>
	</xsl:variable>
	    
	<xsl:choose>
	  <xsl:when test="string($vDrop)">
	    <xsl:call-template name="str-dropWhile">
	      <xsl:with-param name="pStr" select="substring($pStr, 2)" />
	      <xsl:with-param name="pController" select="$pController"/>
	      <xsl:with-param name="pContollerParam" select="$pContollerParam"/>
	    </xsl:call-template>
	  </xsl:when>
	  <xsl:otherwise>
	    <xsl:copy-of select="$pStr"/>
	  </xsl:otherwise>
	</xsl:choose>
      
      </xsl:if>
  </xsl:template>

</xsl:stylesheet>

On every character this function calls the provided controller "pController" passing
to it the provided parameters "pContollerParam" and only continues processing if the
controller has output something.

In conclusion, this solution is yet another example how using the functional
programming style and the FP standard library functions in XSLT can help to produce
solutions to a variety of problems by simply combining and reusing existing
functions.

Cheers,
Dimitre Novatchev.


__________________________________________________
Do You Yahoo!?
Send your FREE holiday greetings online!
http://greetings.yahoo.com

 XSL-List info and archive:  http://www.mulberrytech.com/xsl/xsl-list


Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]