Cross-Linked Fun with XML, XSL, and XPath

Given the following XML snippet:

<recipe id="insalatacaprese">
  <title>Insalata Caprese</title>
  <variation id="inslatacaprese-salad" title="Salad-style">small chunks of tomato and mozzarella</variation>
  <variation id="inslatacaprese-app" title="Appetizer-style">large slices of tomato and mozzarella</variation>
  <-- ingredient list goes here -->
  <step>Slice tomatoes as to your preference, and basil and mozzarella to match the size of the tomatoes.</step>
  <step variation="inslatacaprese-app">Drizzle a small amount of olive oil onto the serving plate.</step>
  <step variation="inslatacaprese-app">Arrange tomato, basil, and mozzarella slices in alternating layers atop each other.</step>
  <step variation="inslatacaprese-salad">Mix tomato, basil, and mozzarella slices gently in the bowl.</step>
  <step optional="true">Season with olive oil, vinegar, pepper, and salt to taste.</step>

Let’s say we want to use an XSL stylesheet to format the instructions for this recipe to look something like this:

Insalata Caprese
Variations
Salad-style
small chunks of tomato and mozzarella
Appetizer-style
large slices of tomato and mozzarella

… ingredients go here …

Preparation
  1. Slice tomatoes as to your preference.
  2. Slice mozzarella to match the size of the tomatoes.
  3. Appetizer-style
    Drizzle a small amount of olive oil onto the serving plate.
  4. Arrange tomato, basil, and mozzarella slices in alternating layers atop each other.
  5. Salad-style
    Mix tomato, basil, and mozzarella slices gently in the bowl.
  6. Optional
    Season with olive oil, vinegar, pepper, and salt to taste.

How can we do the nifty automagic coloring thing such that steps get color-coded according to which variation they belong to? To make it trickier, we can’t hard-code the colors in the XML.

First, define a few styles in your stylesheet:

.var1 { color: #090; }
.var2 { color: #606; }
/* however many more you like */
.optional { font-style: italic; color: #aaa; }

The fun part comes as you are outputting the steps:

<ol type="square">
<xsl:for-each select="step">
  <xsl:element name="li">
    <xsl:if test="@optional = 'true'">
      <xsl:attribute name="class">optional</xsl:attribute>
      <strong>Optional</strong><br/>
    </xsl:if>
    <xsl:if test="@variation">
      <xsl:attribute name="class">var<xsl:value-of select="count(ancestor::recipe/variation[@id = current()/@variation]/preceding-sibling::variation) + 1"/></xsl:attribute>
      <xsl:if test="not(preceding-sibling::step/@variation) or (@variation != preceding-sibling::step/@variation)">
        <strong><xsl:value-of select="ancestor::recipe/variation[@id = current()/@variation]/@title"/></strong><br/>
      </xsl:if>
    </xsl:if>
    <xsl:value-of select="text()"/>
  </xsl:element>
</xsl:for-each>
</ol>

How’s that for nasty, eh? XPath can be rather brain-bending, so let’s dissect that first one:

count(ancestor::recipe/variation[@id = current()/@variation]/preceding-sibling::variation) + 1

Translated: You’re getting the count() of the ::variations for the current recipe that come before (preceding-sibling) the variation[] that has an @id that matches the current() step’s @variation attribute, then incrementing it by one.

The next one is a bit easier:

not(preceding-sibling::step/@variation) or (@variation != preceding-sibling::step/@variation)

Translated: Tell me if the current step either (1) has a ::step immediately preceding it that does not have a @variation attribute, or (2) has a @variation attribute that doesn’t match the previous step’s. If either of those is true, output the title of the variation. Note that the class gets set either wat, so that the colors seem to cascade, but you’re not repeating consecutive variation titles.

The third one is also pretty straightforward:

ancestor::recipe/variation[@id = current()/@variation]/@title

Translated: Give me the @title attribute of the variation (for the current recipe) with an @id attribute matching the current() step’s @variation attribute.

So there you go. Now you know.

Published by

Rick Osborne

I am a web geek who has been doing this sort of thing entirely too long. I rant, I muse, I whine. That is, I am not at all atypical for my breed.