Generating short description using the Content Query Web Part

Probably every Web Content Management (WCM) solution out there uses some kind of content aggregation. No matter whether it’s showing the 5 most recent press releases, new job offers or upcoming events: presenting content roll-up to the visitors allows them to get to your content more easily.

If you’re building a WCM solution on top of Microsoft Office SharePoint Server (MOSS) 2007 you are very likely to use the standard Content Query Web Part (CQWP) for that purpose. CQWP is a great performing Web Part with many different configuration options which allow you to customize content roll-up to fit the branding of your site. Additionally the CQWP uses XSLT for the presentation layer which allows you to even include some logic in how the data should be presented.

If you have any experience with XSLT you probably know that one of its biggest shortcomings are the very few XPath functions supported in the default XSLT implementation. The SharePoint team has noticed that issue as well back in SharePoint Portal Server (SPS) 2003 and provided some additional functionality in the ddwrt namespace. Unfortunately that didn’t solve all of our problems. Not even the most common ones like… generating short descriptions.

The case

Let’s take the following case as an example: you have a subsite with Press Releases and would like to display the 5 most recent one on the home page. To make it more interesting to the visitors you would like your press releases roll-up to show the date, the title and a couple of lines of content of each press release. Having configured it using the CQWP you get the following output:

Default result of a content aggregation using CQWP which includes Publishing Content as the Description. Because the content contains HTML you cannot easily shorten the description

Anything odd about it? Instead of seeing the short description the content roll-up shows the whole content. Sure you could ask the content editors to enter the short description in the Description field but as a good citizen you want to keep the content management process as simple as possible.

So what do we have available that might help us solve us this issue?

Out of the box methods for generating short descriptions

XPath substring function

First thing you might want to use is the XPath substring function. The substring function takes the following parameters: the string which you want to process, the start position for extracting the substring and the optional third parameter which allows you to define how many chars you want to extract. Let’s take a look at the following example:

<xsl:variable name="text" select="'12345'"/>  
<xsl:value-of select="substring($text, 1, 3)"/> <!-- Returns '123' –->  

Not bad is it? While you could theoretically use it for generating short descriptions the problems start when your string contains HTML markup:

<xsl:variable name="text">  
  <![CDATA[<strong>12345</strong>]]>
</xsl:variable>  
<xsl:value-of select="substring($text, 1, 3)"/>  
<!-- Returns '<st' -->  

See the problem? The substring function is not aware of the HTML markup and therefore using it has the risk of breaking the markup of the whole page! If only we could strip the HTML markup before passing it to the substring function…

XPath string replacements

XPath in its standard implementation doesn’t contain any function for replacing one string with another. Neither does the ddwrt namespace. There is a replace function in the EXSLT namespace but how to make the standard Content Query Web Part understand the EXSLT namespace? There is a .NET implementation of the EXSLT functions but it would mean that you would have to subclass the standard CQWP and extend the ModifyXsltArgumentList method.

And even if there was a replace function available, it would have to support Regular Expressions (RegEx): how else would you replace all HTML? There is no simple string equivalent of the simple <[^>]+> RegEx pattern used to strip string of all HTML markup.

But isn’t there anything we could do using nothing else than the out-of-the-box functionality? There is one thing…

Custom template for generating short descriptions

The Content Query Web Part uses three separate XSLT files to render the output. The best part is that you can extend each one of them or even make the CQWP use one of your custom files. Given such flexibility we could create a custom XSL template to do the job for us.

Before we dive into the code let’s define the requirements.

First of all we want to be able to generate a short preview of the content starting at the beginning of the string and containing x chars.

We want the short summary to be HTML safe: all HTML markup should be removed before generating the short description. Also the length of the short description should be calculated using the string with HTML removed.

As a nice-to-have it would be great to be able to include some kind of suffix like “…” or “more” if the content turned out to be longer than the short description allows and has been shortened.

Removing HTML markup in XSLT

As we have already found out there is no standard functionality available in XSLT to remove HTML markup. What we can do though, is to define templates which in some way resemble functions: pieces of logic which you can call with different parameters.

We’ve already done this, so what we can do is to grab the existing template and use it in our solution.

The first part of our requirement is complete: we can strip a string of all HTML markup.

Imtech.GenerateSummary template v0.1

By now we should be able to write a simple template to generate short descriptions for us:

<xsl:template name="Imtech.GenerateSummary">  
  <xsl:param name="Content"/>
  <xsl:param name="Length"/>
  <xsl:variable name="cleanContent">
    <xsl:call-template name="Imtech.RemoveHtml">
      <xsl:with-param name="String" select="$Content"/>
    </xsl:call-template>
  </xsl:variable>
  <xsl:value-of select="substring($cleanContent, 1, $Length)"/>
</xsl:template>  

That should do the job, right? Well, there is one problem… Let’s have a look at a test case:

<xsl:call-template name="Imtech.GenerateSummary">  
  <xsl:with-param name="Content">
    <!-- text wrapped for the clarity of this example
    but would normally be in one line to prevent extra chars -->
    Lorem ipsum dolor sit <strong>amet, consectetur</strong>
    adipiscing elit. Nunc a magna quis velit dapibus blandit.
    Pellentesque accumsan tortor ut est
  </xsl:with-param>
  <xsl:with-param name="Length" select="45"/>
</xsl:call-template>  

Curious what the result is? “Lorem ipsum dolor sit amet, consectetur adipi”. In case you haven’t noticed the word adipiscing is being sliced in the middle. Not really nice, is it? We should know better nowadays.

That leads us to a new requirement: the short description should be of maximally the given length, but if it’s in a middle of a word, the function should go back until it finds a separator (space in our case) and break the short description there. According to this requirement the short description in the example above should be “Lorem ipsum dolor sit amet, consectetur”.

LastIndexOf in XSLT?

If you were programming in one of the .NET languages you would use the LastIndexOf function to determine the position of the last separator and substring the initial string to that position. Want to hear some bad news? There is no LastIndexOf in XSLT. Somehow you don’t seem very surprised…

Once again let’s use templates to get the job done. Instead of returning the position of the last separator, we will immediately return the string before the last separator.

<xsl:template name="Imtech.SubstringBeforeLast">  
  <xsl:param name="String" />
  <xsl:param name="Char" />
  <xsl:param name="subsequent"/>
  <xsl:choose>
    <xsl:when test="contains($String, $Char)">
      <xsl:if test="$subsequent = 1">
        <xsl:value-of select="$Char"/>
      </xsl:if>
      <xsl:value-of select="substring-before($String, $Char)"/>
      <xsl:call-template name="Imtech.SubstringBeforeLast">
        <xsl:with-param name="String"
                        select="substring-after($String, $Char)" />
        <xsl:with-param name="Char" select="$Char" />
        <xsl:with-param name="subsequent" select="1"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:if test="$subsequent != 1">
        <xsl:value-of select="$String"/>
      </xsl:if>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>  

This template takes two parameters: the string which you want to create a substring of and the character you’re looking for. The third parameter (subsequent) is for internal purposes: it helps the template to determine whether it should output a separator or not.

How it works

First of all the template checks whether the given string contains the character passed through the Char parameter. If the character has been found the string is being passed for further processing. If not the template checks one more thing: if it’s the initial call (subsequent parameter hasn’t been set yet), the string is being output as-is (this to cover the following case SubstringBeforeLast(“Lorem”, “ ”)).

If the template finds the Char character in the string: first the template checks if it is a subsequent call or not. As separators are not being included in the String variable on subsequent calls this check is required to include all other separators than the last one. Then everything before the first occurrence of the separator is being passed to the output. Everything after the separator is being used as the String parameter for a recursive call. Additionally the subsequent parameter is being set to let the template know that it’s being called from itself.

Almost there

So far we have done a lot of work with the XSLT. Let’s put it together and have a look at the result:

Using a custom XSLT template you can easily get the short description displayed properly. One last thing missing is adding a suffix like '...' after shortening a string

Almost there, aren’t we? The last thing that we’re missing is appending a suffix after shortening the content. Let’s finish our template off with this feature:

<xsl:if test="string-length($cleanContent) &gt; $Length">  
  <xsl:value-of select="$Suffix" disable-output-escaping="yes"/>
</xsl:if>  

And just a last check to confirm that it’s all working properly:

Using a custom XSLT template you can generate a properly displayed short description using the full page content as input

The complete short summary template by now should look like this:

<xsl:template name="Imtech.GenerateSummary">  
  <xsl:param name="Content"/>
  <xsl:param name="Length"/>
  <xsl:param name="Suffix"/>
  <xsl:variable name="cleanContent">
    <xsl:call-template name="Imtech.RemoveHtml">
      <xsl:with-param name="String" select="$Content"/>
    </xsl:call-template>
  </xsl:variable>
  <xsl:call-template name="Imtech.SubstringBeforeLast">
      <xsl:with-param name="String"
      select="substring($cleanContent, 1, $Length)"/>
      <xsl:with-param name="Char" select="' '"/>
  </xsl:call-template>
  <xsl:if test="string-length($cleanContent) &gt; $Length">
    <xsl:value-of select="$Suffix"
      disable-output-escaping="yes"/>
  </xsl:if>
</xsl:template>

<xsl:template name="Imtech.RemoveHtml">  
  <xsl:param name="String"/>
  <xsl:choose>
    <xsl:when test="contains($String, '&lt;')">
      <xsl:value-of select="substring-before($String, '&lt;')"/>
      <xsl:call-template name="Imtech.RemoveHtml">
        <xsl:with-param name="String"
          select="substring-after($String, '&gt;')"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$String"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>  
<xsl:template name="Imtech.SubstringBeforeLast">  
  <xsl:param name="String" />
  <xsl:param name="Char" />
  <xsl:param name="subsequent"/>
  <xsl:choose>
    <xsl:when test="contains($String, $Char)">
      <xsl:if test="$subsequent = 1">
        <xsl:value-of select="$Char"/>
      </xsl:if>
      <xsl:value-of select="substring-before($String, $Char)"/>
      <xsl:call-template name="Imtech.SubstringBeforeLast">
        <xsl:with-param name="String"
          select="substring-after($String, $Char)" />
        <xsl:with-param name="Char" select="$Char" />
        <xsl:with-param name="subsequent" select="1"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:if test="$subsequent != 1">
        <xsl:value-of select="$String"/>
      </xsl:if>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>  

And you can use it like this:

<xsl:call-template name="Imtech.GenerateSummary">  
  <xsl:with-param name="Content" select="@Description" />
  <xsl:with-param name="Length" select="255" />
  <xsl:with-param name="Suffix" select="'...'"/>
</xsl:call-template>  

Summary

Content Query Web Part is a very powerful and flexible Web Part. Because it uses XSLT for data presentation it allows you to generate dynamic content aggregation matching your branding and requirements. Although the default XSLT implementation supported by the CQWP contains only the basic functionality, you can easily extend it with custom templates. While they seem more complex than a comparable solution using C# or VB the best part is, that they don’t require any custom code and work in any standard MOSS 2007 installation.

Technorati Tags: SharePoint, SharePoint 2007, MOSS 2007

Comments

comments powered by Disqus