Style-Free Stylesheets with Cocoon

by Andreas Hartmann

Introduction

The traditional approach of transforming XML files into, e.g., HTML suffers from mixing up logic and presentation in XSLT stylesheets. Eric van der Vlist's article Style-free XSLT Style Sheets on XML.com introduces a concept to keep the presentation out of the stylesheet. This guide gives an example how to apply this concept in your Cocoon website.

Style-Free Stylesheets? What's That?

One of the major problems about XSLT-based web pages is that most of the presentation is embedded in XSLT stylesheets which almost no common graphical tool is capable of editing. One strategy to avoid this is to extract the style from the stylesheet - that's the meaning the term "style-free stylesheet". In such a case, the purpose of the XSLT is applying some logic to the XML data, whereas the actual look is achieved by applying an XHTML template. This template can be edited using a conventional WYSIWYG HTML editor.

This article is directed to the advanced user who is familiar with XSLT and the Cocoon architecture.

Note:
I neglected whitespaces in all code examples because I didn't want to overload them.

Is This the Right Thing for Me?

If you consider using style-free stylesheets, you should check if your requirements roughly fit the following scheme:

  • My designers want to edit the layout with their conventional tools.
  • The page layout contains large parts of static HTML.
  • I'm quite familiar with XSLT.
  • I don't mind implementing and maintaining a somewhat more complicated architecture.
  • I don't like XSLT programming, but my page composition is very simple and all URIs follow a standard scheme (e. g., everything is located at /section/document.html.

An XSLT Stylesheet that Applies an XHTML Template

So, how does a stylesheet look that uses an external XHTML template to style an XML document? First of all, we need some proprietary tags inside the XHTML to declare where the generated data shall be included. To make the XSLT matcher easy to read, I recommend using something like <template name=""/>:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:tmpl="http://www.cocooncenter.org/template">
  ...
  <tmpl:include name="main-menu"/>
  ...
  <tmpl:include name="document"/>
  ...
</html>

The name of the template tag should be considered quite well. E.g., if your HTML editor is not capable of exporting XHTML, you might want to do an external conversion step with HTML Tidy or even use the HTMLGenerator to convert the HTML template to XHTML. One problem about the HTMLGenerator, more precisely of the underlying JTidy, is that all non-HTML tags are removed from the source. To avoid your template tag being removed, you could name it <span class="template" style="main-menu"/> or something like that.

I agree to Eric that it is useful to include the XHTML template by defining a variable (in contrast to his article, I call it "template"). To distinguish applying template tags from applying XML source tags, I suggest to introduce a mode, e. g., "layout":

<xsl:variable name="page" select="/page"/>
<xsl:variable name="template" select="document('templates/page.xhtml')"/>

<xsl:template match="/">
  <xsl:apply-templates select="$template/html" mode="layout"/>
</xsl:template>

<!-- include main menu -->
<xsl:template match="tmpl:include[@name = 'main-menu']" mode="layout">
  <xsl:apply-templates select="$page/navigation"/>
</xsl:template>

<!-- include document -->
<xsl:template match="tmpl:include[@name = 'document']" mode="layout">
  <xsl:apply-templates select="$page/document"/>
</xsl:template>

In most cases, the <page/> element will be created using Cocoon aggregation. It is up to you if you aggregate the XML page first and then layout the complete document, or if you layout the single parts of the page before aggregating them. I would usually recommend the second approach, because modularization leads to simpler site maintenance.

To simplify the stylesheet and to improve reusability, we can use a common matcher for all templates:

<!-- include page section -->
<xsl:template match="tmpl:include">
  <xsl:apply-templates select="$page/*[name() = current()/@name]" mode="layout"/>
</xsl:template>

Of course, this implies that all XML segments that are used to create page parts have to be arranged inside the <page/> tag.

The Template Link Problem

A serious problem with style-free stylesheets occurs when it comes to links within the HTML templates. If you use the same template to render pages at different URI stages, e. g. for /index.html and home/index.html, all relative links - usually images and CSS files - won't work on one of the pages. There are several possible solutions to this problem:

  • The quick-and-dirty workaround is to include Cocoon matchers, at least for images and CSS files:

    <!-- include page section -->
    <map:match pattern="**images/*.png">
      <map:read src="images/{2}.png" mime-type="image/png"/>
    </map:match>
    

    This leads to lots of image files to be cached by your browser - one copy for every URI stage. When you create static pages, they also need much more space on the server.

  • If your pages follow a simple URI pattern, you can use plain <img src="images/hello.png"/> tags in your template and forward the relative URI to the stylesheet:

    <map:match pattern="*.html">
      <map:aggregate element="page">
        <map:part src="cocoon:/{1}-content"/>
      </map:aggregate>
      <map:transform src="xslt/page.xsl">
        <map:parameter name="relative-uri" value="../"/>
      </map:transform>
      <map:serialize/>
    </map:match>
    
    <map:match pattern="*/*.html">
      <map:aggregate element="page">
        <map:part src="cocoon:/{1}/local-menu"/>
        <map:part src="cocoon:/{1}/{2}-content"/>
      </map:aggregate>
      <map:transform src="xslt/page.xsl">
        <map:parameter name="relative-uri" value="../../"/>
      </map:transform>
      <map:serialize/>
    </map:match>
    
    <xsl:param name="relative-uri"/>
    
    ...
    
    <xsl:template match="@src|@href|@background" mode="layout">
      <xsl:attribute name="{name()}">
        <xsl:value-of select="concat($relative-uri, .)"/>
      </xsl:attribute>
    </xsl:template>
    
  • The maybe most general but complicated solution is using a recursive XSLT template to generate the relative URI on the fly:

    <map:match pattern="**/*.html">
      <map:aggregate element="page">
        <map:part src="cocoon:/{1}/local-menu"/>
        <map:part src="cocoon:/{1}/{2}-content"/>
      </map:aggregate>
      <map:transform src="xslt/page.xsl">
        <map:parameter name="uri" value="{1}/{2}.html"/>
      </map:transform>
      <map:serialize/>
    </map:match>
    
    <xsl:param name="uri"/>
    
    <!-- create relative uri from path -->
    <xsl:template name="create-relative-uri">
      <xsl:param name="local-uri" select="$uri"/>
      <xsl:if test="contains($local-uri, '/')">
        <xsl:text/>../<xsl:call-template name="create-relative-uri">
          <xsl:with-param name="local-uri" select="substring-after($local-uri, '/')"/>
        </xsl:call-template>
      </xsl:if>
    </xsl:template>
    
    <xsl:variable name="relative-uri">
      <xsl:call-template name="create-relative-uri"/>
    </xsl:variable>
    
    ...
    
    <xsl:template match="@src|@href|@background" mode="layout">
      <xsl:attribute name="{name()}">
        <xsl:value-of select="concat($relative-uri, .)"/>
      </xsl:attribute>
    </xsl:template>
    

Conclusion

I guess you have to figure out whether this approach will fit your needs by yourself. I think it's worth considering for quite a lot of sites.

By the way, I'm using style-free stylesheets for the cocooncenter site. I don't want to promise to much, but maybe during the next weeks I'll write an article about the cocooncenter site architecture, so you will have the chance to see a real case study.