Displaying the First N Words of a Long Rich Text Column with XSL

When you want to display blog posts and announcements with DVWPs in your SharePoint Site Collection, you usually don’t want to display the full posts, but just enough to indicate what the item is about and to let the user know if they should click to see more.  An example might be showing the last 3 blog posts on your Home Page.  There isn’t any easy out of the box way to do this.

For the following examples, let’s say that the @Body column contains the text: “The <em>quick</em> <span style=”color: #a52a2a;”>brown</span> fox jumped over the lazy dog.”, which actually looks like this: “The quick brown fox jumped over the lazy dog.”

One option is to use the ddwrt:Limit function.  This allows you to specify a number of characters to show, along with some text to postpend if the original text is longer than the limit you set.  So, for instance, ddwrt:Limit(string(@Body), 25, ‘…’) would show the first 25 characters, followed by the ‘…’ string if there are more than 25 characters in the @Body column.  However, since the @Body column usually contains some HTML markup, you usually don’t get what you really want (the tags are all counted as part of the number of characters).  With our example @Body text above, you’ll get “The <em>quick</em> <span …”, which isn’t even valid HTML since the <span> tag isn’t closed.  Depending on the browser you are using, you’ll probably see something like “The quick“.

So, the first thing you might want to do is to strip out all of the HTML.  The StripHTML XSL template below will do this for you.

<xsl:template name="StripHTML">
  <xsl:param name="HTMLText"/>
  <xsl:choose>
   <xsl:when test="contains($HTMLText, '&gt;')">
    <xsl:call-template name="StripHTML">
      <xsl:with-param name="HTMLText" select="concat(substring-before($HTMLText, '&lt;'), substring-after($HTMLText, '&gt;'))"/>
    </xsl:call-template>
   </xsl:when>
   <xsl:otherwise>
    <xsl:value-of select="$HTMLText"/>
   </xsl:otherwise>
  </xsl:choose>
 </xsl:template>

Once you have the HTML stripped out, the ddwrt:Limit function will do what you want, but the text will probably be cut off mid-word.  Looking at our example @Body text again, the StripXSL template will return “The quick brown fox jumped over the lazy dog.”, which with the ddwrt:Limit function above will look like “The quick brown fox jumpe…”

So, an even better solution is to first strip out the HTML and then return a specific word count.  The FirstNWords XSL template below takes care of this for you.

<xsl:template name="FirstNWords">
  <xsl:param name="TextData"/>
  <xsl:param name="WordCount"/>
  <xsl:param name="MoreText"/>
  <xsl:choose>
    <xsl:when test="$WordCount &gt; 1 and
        (string-length(substring-before($TextData, ' ')) &gt; 0 or
        string-length(substring-before($TextData, '  ')) &gt; 0)">
      <xsl:value-of select="concat(substring-before($TextData, ' '), ' ')" disable-output-escaping="yes"/>
      <xsl:call-template name="FirstNWords">
        <xsl:with-param name="TextData" select="substring-after($TextData, ' ')"/>
        <xsl:with-param name="WordCount" select="$WordCount - 1"/>
        <xsl:with-param name="MoreText" select="$MoreText"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:when test="(string-length(substring-before($TextData, ' ')) &gt; 0 or
        string-length(substring-before($TextData, '  ')) &gt; 0)">
      <xsl:value-of select="concat(substring-before($TextData, ' '), $MoreText)" disable-output-escaping="yes"/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$TextData" disable-output-escaping="yes"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

With our example, StripHTML returns “The quick brown fox jumped over the lazy dog.” and then a call to FirstNWords with a WordCount of 5 will give you “The quick brown fox jumped…”  Much nicer!

Note that this won’t do a perfect job if there is a lot of odd spacing or punctuation, but most of the time, it’s a much cleaner solution.

NOTE (2009-02-05): I was working with some data today that had lots of double spaces and some escaped characters, so I tweaked my FirstNWords template to work a little better by adding the test for double spaces (though it isn’t foolproof with different types of white space).

UPDATE (2009-02-27): Here’s an example of how I’ve used these templates in the past to display blog posts.  First, I create a variable called BodyText that contains the contents of the @Body column with the HTML stripped out by using the StripHTML template.  Then I output a row with a link to the post and a second row with the first 25 words of the post, followed by ‘…’, using the FirstNWords template.

<xsl:template name="USG_Blog.rowview">
  <xsl:variable name="BodyText">
    <xsl:call-template name="StripHTML">
      <xsl:with-param name="HTMLText" select="@Body"/>
    </xsl:call-template>
  </xsl:variable>
  <tr>
    <td>
      <a href="{$WebURL}Lists/Posts/Post.aspx?ID={@ID}&amp;Source={$URL}" >
        <xsl:value-of select="@Title"/>
      </a>
    </td>
  </tr>
  <tr>
    <td>
      <xsl:call-template name="FirstNWords">
        <xsl:with-param name="TextData" select="$BodyText"/>
        <xsl:with-param name="WordCount" select="25"/>
        <xsl:with-param name="MoreText" select="'...'"/>
      </xsl:call-template>
    </td>
  </tr>
</xsl:template>

As a side note, I always store these “utility” functions in a separate file for reuse and use the xsl:import tag to pull them into the DVWP I’m working on.  The import should go before the xsl:output tag, as below.

<xsl:import href="/Style Library/XSL Style Sheets/Utilities.xsl"/>
<xsl:output method="html" indent="no"/>

Similar Posts

97 Comments

  1. Hi Marc,

    Great solution! I have one question. I’d like to add a ‘Read more’ link just below the body field and it only appears if the body field is longer than FirstNWords. How can I do that?

    Thanks,
    George

    1. George:

      You can simply emit the ‘Read more…’ link into a separate container beneath the shortened text. Do that after emitting the FirstNWords results.

      M.

        1. George:

          It’s hard to say without seeing more details. Unfortunately, posting XSL here doesn’t really work. Can you post things in one of the public forums and ping a link?

          M.

            1. George:

              Interesting place to post the question! ddwrt:IfNew should work fine in 2010. When you say “it didn’t work well”, what do you mean?

              M.

              1. Currently I use “ddwrt:IfNew(string(@Created_x0020_Date))” and the “new” icon is not showing up at all. If I use @Created instead of @Created_x0020_Date then it works partially. The “new” icon is appearing to the new items but it is also appearing to some old items.

                Someone suggested using “:@Created_x0020_Date.ifnew” but it creates an error in SharePoint.

                Cheers,
                George

                  1. Thanks Marc. I did try that but it doesn’t work properly as I’ve mentioned above.

                    Anyway, did you have a chance to look at my code re how to do a test for FirstNWords results?

                    Thanks,
                    George

  2. Hi Marc,

    Thanks a lot for this blogpost!
    I used your approach in SharePoint 2010, and it’s still valid.

    Since I’m not an XSL guru, I struggled for a while to get all the required syntax in the xsl files, but eventually I managed :)

  3. I’m late to the xsl world, but I found this immensely helpful and ready to use on a SharePoint site I’m working on. They only thing I haven’t been able to figure out is how to use so I can modularize your two templates.

    I created a utilities.xsl file per your example with the two template code blocks pasted in and nested inside a tag, then I added the line in the parent xsl file inside the tag and before everything else, but then when I reload the page with the web part using the parent styling, there’s an error with the web part being unable to display the contents. The templates work beautifully directly pasted in so I don’t understand what I’m missing.

    Thank you for any guidance in this.
    K.

  4. I just realized all of the tag names I wrote were interpreted as code and stripped from my post, here is is again without the tag markup and missing terms:
    =======
    I’m late to the xsl world, but I found this immensely helpful and ready to use on a SharePoint site I’m working on. They only thing I haven’t been able to figure out is how to use xsl:import so I can modularize your two templates.

    I created a utilities.xsl file per your example with the two template code blocks pasted in and nested inside a xsl:stylesheet tag, then I added the line in the parent xsl file inside the xsl:stylesheet tag and before everything else, but then when I reload the page with the web part using the parent styling, there’s an error with the web part being unable to display the contents. The templates work beautifully directly pasted in so I don’t understand what I’m missing.

    Thank you for any guidance in this.
    K.

  5. Wow you’re fast. You replied quicker than I could correct my initial post. I will check out your references. Thanks.

  6. I understand the example in the Usage page, but even if I stripped everything out of my parent stylesheet and only have the xsl:import and xsl:output lines inside xsl:stylesheet which should then show nothing at all in the web part, it still gives a “Unable to display this Web Part” error.

    I read in another forum that one can only import / include at the root level. Is that true? I’m actually not at the root level and don’t have access there. My xsl files and utilities.xsl file resides in a sub-site a few levels down. Could that be why this isn’t working for me?

    Thanks

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.