Skinning with XSLT


This post is in response to a couple of posts on stackoverflow, WRT how to use XSLT to embed some XML inside some other XML (ie – XHTML). Basically, the posts on stackoverflow weren’t much help. So, I worked it out and this is how you do it.

I have some XSLT which is fed a source document and which transforms that source document to HTML. Essentially:

<xsl:template match="/">
  <html>
  <body>
    <xsl:apply-templates select="*">
  </body>
</xslt:template>

Where there are a variety of other rules for transforming each of the source elements into suitably-classed HTML.

What I want to do is to have a static “frame” document that looks like this:

<html>
  <head> here is where the stylesheet links go </head>
  <body>
      <div id="title-bar">title bar</div>
      <div id="content"><!-- CONTENT GOES HERE --></div>
  </body>
</html>

And I would like the content produced by the existing rules inserted into that “content” div.

Do do this, we

  1. Pull in the “frame.html” using document()
  2. Process that html, using XSL “mode” to separate our template rules from the other rules
  3. Pass the content into the html frame rules as a parameter
  4. Have frame rules that duplicate the frame html, but which recursively pass down the content each time they do. (I don’t bother with this for @* and text(), but you could if you want)
  5. Catch the content div in the frame rules, and exit out of the frame rules by applying templates to the content in the default context.

Thus, our xslt has the following rules.

First – pull out the frame html and pass in the source document. We use “frame” mode to handle these separate rules:

<xsl:template match="/">
  <xsl:apply-templates mode="frame" 
    select="document('/Resources/html/frame.html')">
  <xsl:with-param name="content" select="."/>
  </xsl:apply-templates>
</xsl:template>

Next – duplicate the frame document, passing down the “content” parameter:

<xsl:template match="@*" mode="frame">
  <xsl:copy/>
</xsl:template>
<xsl:template match="text()" mode="frame">
  <xsl:copy/>
</xsl:template>
<xsl:template match="*" mode="frame">
  <xsl:param name="content"/>
  <xsl:copy>
    <xsl:apply-templates select="@*" mode="frame"/>
    <xsl:apply-templates select="node()" mode="frame">
      <xsl:with-param name="content" select="$content"/>
    </xsl:apply-templates>
  </xsl:copy>
</xsl:template>

Finally, manage the special “content” div:

<xsl:template match="div[@id='content']" mode="frame">
  <xsl:param name="content"/>
  <xsl:copy>
    <xsl:apply-templates select="@*" mode="frame"/>
    <xsl:apply-templates select="$content/*"/>
  </xsl:copy>
</xsl:template>

The line in bold exits out of the “frame” mode and into the default mode, which handles the content in the usual way. Note that we select $content/*, not $content, for obvious reasons.

Aaaaaand – it won’t work. Why? Because the very first rule invokes the XSLT default rule which, for context “frame”, copies the root node without passing through the parameter. We need to override it:

<xsl:template match="/" mode="frame">
  <xsl:param name="content"/>
  <xsl:apply-templates select="node()" mode="frame">
    <xsl:with-param name="content" select="$content"/>
  </xsl:apply-templates>
</xsl:template>

Most annoying thing in the world, figuring that bit out. Without this rule, $content winds up being an empty string and XSLT complains that you can’t treat a string as if it were a node set. A very unhelpful error message.

Anyway. That does the job. Obviously, you can do other things. For instance, I insert stuff which is generated via xslt into the “head” section of the target document.

<xsl:template match="/html/head" mode="frame">
  <xsl:param name="content"/>
  <xsl:copy>
    <xsl:apply-templates select="@*" mode="frame"/>
    <xsl:apply-templates select="node()" mode="frame">
      <xsl:with-param name="content" select="$content"/>
    </xsl:apply-templates>
    <xsl:for-each select="$content//ibis:*[@ibis:objectid]">
       create a link element in the head for every
       ibis element that has an objectid.
    </xsl:for-each>
  </xsl:copy>
</xsl:template>

Of course, you can build whatever hooks you like into the frame document and catch them with rules in frame mode.

And that’s it, more or less. This applies a frame around our generated content without disturbing all the other rules which I have spent months writing.

About these ads

One Response to Skinning with XSLT

  1. pdeepie says:

    Thank you for the post it has been very helpful!

    With regard to this section of the code

    create a link element in the head for every
    ibis element that has an objectid.

    in the for-each loop, I would like to copy a node from the ‘frame’ data (and therefore need to refer back to the frame data). Is this possible?

    Thank you
    Pete

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 73 other followers

%d bloggers like this: