views:

36

answers:

5

I have the following xml.

<root query="Smith Antony Blah Jones">

And the following xsl to split the string into different variables.

<xsl:variable name="query">
<xsl:value-of select="substring-before (root/@query, ' ')" />
</xsl:variable>

<xsl:variable name="query1">
<xsl:value-of select="substring-before(substring-after(root/@query, ' '), ' ')" />
</xsl:variable>

<xsl:variable name="query2"><xsl:value-of select="substring-before(substring-after(root/@query, $query1), ' ')"/></xsl:variable>

<xsl:template match="root">
<search data="{$query}" data1="{$query1}" data2="{$query2}" "/>

However I get the following html

<search data="Smith" data1="Antony" data2="" data3=""/>

I can see that the xsl is looking at the first space between 'Antony' and 'Blah' and returning nothing as that is what after the 'Antony' and before the 'Blah'. How can I skip over the first space and capture the 'Blah'?

I'm using XSLT 1.0, BTW.

Thanks!

A: 

Can you just change this line:

<xsl:variable name="query2"><xsl:value-of select="substring-before(substring-after(root/@query, $query1), ' ')"/></xsl:variable>

to this:

<xsl:variable name="query2"><xsl:value-of select="substring-before(concat(substring-after(root/@query, $query1),' '), ' ')"/></xsl:variable>

Just adding a space to the end of $query1. Not the cleanest but I think it will work...

Abe Miessler
+2  A: 

Let's go through your steps because it's pretty clear what is going on and how to fix it.

You start with the following string: "Smith Antony Blah Jones"

Then you assign to the variable query the result of substring-before on the first string. That assigns "Smith" to query.

Then you assign query2 the value of a substring-before of a substring-after of your original string. The substring-after returns "Antony Blah Jones", and the substring-before returns "Antony". So far so good.

Now your next assignment looks for "Antony" in the original string, but then your resulting string is " Blah Jones", so when you run substring-before, you match the very first whitespace.

There are very many possible solutions to this. An obvious one would be calling substring-after before calling substring-before, like this:

<xsl:value-of 
  select="substring-before(substring-after(substring-after(root/@query, $query1), ' '),' ')"
/>

But that's pretty ugly. Alternatively you can use substring or just tack on a space to "Antony" when you do your first substring-after call.

I think you're better off defining a recursive template to grab the next token then pass the remaining string to itself. This will let you grab an arbitrary number of space-separated tokens from a string without having to have so many numbered variables. Something like this:

<xsl:template name="recursive-tokenizer">
  <xsl:param name="input"/>
  <xsl:choose>
    <!-- Test whether the input token contains a space. -->
    <xsl:when test="contains($input,' ')">
      <!-- Output a token. -->
      <xsl:value-of select="substring-before($input,' ')"/>

      <!-- Call this template with the rest of the string. -->
      <xsl:call-template name="recursive-tokenizer">
        <xsl:with-param name="input" select="substring-after($input,' ')"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <!-- There is no space, so just output the input. -->
      <xsl:value-of select="$input"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>
Welbog
@Welbog: +1 for the comprehensive answer!
Dimitre Novatchev
A: 

This stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:template match="root">
        <search>
            <xsl:call-template name="query"/>
        </search>
    </xsl:template>
    <xsl:template name="query">
        <xsl:param name="pString" select="@query"/>
        <xsl:param name="pNumber" select="0"/>
        <xsl:param name="pTop" select="3"/>
        <xsl:choose>
            <xsl:when test="$pNumber > $pTop"/>
            <xsl:when test="contains($pString,' ')">
                <xsl:call-template name="query">
                    <xsl:with-param name="pString" 
                                    select="substring-before($pString,' ')"/>
                    <xsl:with-param name="pNumber" select="$pNumber"/>
                    <xsl:with-param name="pTop" select="$pTop"/>
                </xsl:call-template>
                <xsl:call-template name="query">
                    <xsl:with-param name="pString" 
                                    select="substring-after($pString,' ')"/>
                    <xsl:with-param name="pNumber" select="$pNumber + 1"/>
                    <xsl:with-param name="pTop" select="$pTop"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
                <xsl:attribute 
                     name="data{substring($pNumber, 1 div ($pNumber != 0))}">
                    <xsl:value-of select="$pString"/>
                </xsl:attribute>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>

With this input:

<root query="Smith Antony Blah Jones"/>

Output:

<search data="Smith" data1="Antony" data2="Blah" data3="Jones" />
Alejandro
A: 

What you're really asking is, "How can I use XSLT to parse a string?" While this can be done (the recursive approach that Alejandro proposes is a good one), it's often much simpler to preprocess such a document, parse the strings using a languages that's better at string manipulation than XSLT, and modify the document before you transform it.

For instance, in C#, you could write a simple method like this:

foreach (XmlElement elm in doc.SelectNodes("//*[@query]"))
{
   foreach (string s in elm.GetAttribute("@query")
      .Split(new[] {' '})
      .Where(x => !(string.IsNullOrEmpty(x))))
   {
      XmlElement query = doc.CreateElement("query")
      query.InnerText = s;
      elm.AppendChild(query);
   }
}

Now your elements will look like this:

<root query="Antony Blah Smith Jones">
   <query>Antony</query>
   <query>Blah</query>
   <query>Smith</query>
   <query>Jones</query>
</root>

and it's trivial to access those substring elements in XSLT.

Robert Rossney
A: 

If you were using XSLT 2 then you could do it as:

<search>
  <xsl:for-each select="tokenize(/root/@query,'\s+')">
    <xsl:attribute name="data{(position() - 1)[. &gt; 0]}"
                   select="."/>
  </xsl:for-each>
</search>
Nick Jones