Using XSL Keys and Variables

Right, let’s get rid of that horrible flatness. To do this, we are going to use a feature of xsl called keys. Keys enable you to access a group elements (or any other sort of node) that are identified by said key. So, for example, all the paragraphs in chapter one will use the ID attribute of chapter one as their key. We can then select them all by using that key.

Let’s see how this magic happens (in this first example I’ll pretend we don’t have automatic styles, so you can see how it all works without the added complexity the automatic styles add). Let’s start by linking the chapters to the parts.


<xsl:key name="chapters" 
     match="/office:document/office:body/office:text/text:p[contains(@text:style-name,'Novel-Chapter-Title')]"
     use="generate-id(preceding-sibling::text:p[@text:style-name='Novel-Part-Title'][1])" />

The first couple of bits are hopefully self explanatory: we create a key called chapters, in it we stuff all <text:p> elements with Novel-Chapter-Title in their @text:style-name attribute (we use “contains()” rather than “=” so we catch elements that also use “Novel-Chapter-Title-First” etc. as their style name). The clever bit is the next line. With each chapter we store the ID of the part they are related to (the “use” attribute). To get at the part they belong to, we select all the preceding siblings i.e all the <text:p> elements that come before it in the document. We then throw away all those that don’t have “Novel-Part-Title” as their style name and then discard all but the first one of the elements left. Because “preceding-sibling” starts at the current item and goes backwards, the first “Novel-Part-Title” is the one that is closest to the current <text:p> element. Now, when we want only the chapters in certain part all we need to do is call generate-id() on that part and use it as our key (generate-id() always returns the same unique id when used with the same element).

We do a similar thing to get all the paragraphs that belong to a chapter.


<xsl:key name="paragraphs" 
     match="/office:document/office:body/office:text/text:p[contains(@text:style-name, 'Novel-Paragraph')]"
     use="generate-id(preceding-sibling::text:p[contains(@text:style-name,'Novel-Chapter-Title')][1])" />

We also do the same thing to link the forward paragraphs to each forward title.

Now you understand the general principle, let’s see what the real code looks like when we have to deal with automatic styles.

 
<xsl:key name="chapters" 
  match="/office:document/office:body/office:text/text:p[@text:style-name = /office:document/office:automatic-styles/style:style[contains(@style:parent-style-name,'Novel-Chapter-Title')]/@style:name or contains(@text:style-name, 'Novel-Chapter-Title')]"
  use="generate-id(preceding-sibling::text:p[@text:style-name = /office:document/office:automatic-styles/style:style[@style:parent-style-name = 'Novel-Part-Title']/@style:name or @text:style-name = 'Novel-Part-Title'][1])"/>

Basically we’ve just added in an “or” statement to both the match and use statements that checks the @style:parent-style-name of the <text:p> element’s automatic style. As you can see, it makes the code a lot less readable.

For completeness here are the other three keys, the part-paragraphs key is used in cases where a book is divided into parts but those parts aren’t divided into chapters:


<xsl:key name="paragraphs" 
     match="/office:document/office:body/office:text/text:p[@text:style-name = /office:document/office:automatic-styles/style:style[contains(@style:parent-style-name,'Novel-Paragraph')]/@style:name or contains(@text:style-name, 'Novel-Paragraph')]"
     use="generate-id(preceding-sibling::text:p[@text:style-name = /office:document/office:automatic-styles/style:style[contains(@style:parent-style-name,'Novel-Chapter-Title')]/@style:name or contains(@text:style-name, 'Novel-Chapter-Title')][1])" />

<xsl:key name="part-paragraphs" 
     match="/office:document/office:body/office:text/text:p[@text:style-name = /office:document/office:automatic-styles/style:style[contains(@style:parent-style-name,'Novel-Paragraph')]/@style:name or contains(@text:style-name, 'Novel-Paragraph')]"
     use="generate-id(preceding-sibling::text:p[@text:style-name = /office:document/office:automatic-styles/style:style[@style:parent-style-name = 'Novel-Part-Title']/@style:name or @text:style-name = 'Novel-Part-Title'][1])" />

<xsl:key name="forward" 
     match="/office:document/office:body/office:text/text:p[@text:style-name = /office:document/office:automatic-styles/style:style[contains(@style:parent-style-name,'Novel-Forward-Paragraph')]/@style:name or contains(@text:style-name, 'Novel-Forward-Paragraph')]"
     use="generate-id(preceding-sibling::text:p[@text:style-name = /office:document/office:automatic-styles/style:style[contains(@style:parent-style-name,'Novel-Forward-Title')]/@style:name or contains(@text:style-name, 'Novel-Forward-Title')][1])" />

Before we give our new keys a test, we are going to define a couple of new variables to save us a bit of typing as we go. These variables are just a short hand way of writing “all chapters”, “all paragraphs” etc. They help to keep our code tidy, especially with the added complexity automatic styles bring. You might be wondering why we didn’t use these variables in our keys to make them neater. The reason is that you can’t use variables in match statements in xsl 1.0. This is fixed in xsl 2.0, but when I wrote this script there wasn’t much support for xsl 2.0. This has changed now, so feel free upgrade to xsl 2.0 and use these variables in match statements.


<xsl:variable name="forward" select="/office:document/office:body/office:text/text:p[@text:style-name = 'Novel-Forward-Title' or @text:style-name = /office:document/office:automatic-styles/style:style[@style:parent-style-name = 'Novel-Forward-Title']/@style:name]"/>

<xsl:variable name="parts" select="/office:document/office:body/office:text/text:p[@text:style-name = 'Novel-Part-Title' or @text:style-name = /office:document/office:automatic-styles/style:style[@style:parent-style-name = 'Novel-Part-Title']/@style:name]"/>

<xsl:variable name="chapters" select="/office:document/office:body/office:text/text:p[contains(@text:style-name,'Novel-Chapter-Title') or @text:style-name = /office:document/office:automatic-styles/style:style[contains(@style:parent-style-name, 'Novel-Chapter-Title')]/@style:name]"/>

<xsl:variable name="paragraphs" select="/office:document/office:body/office:text/text:p[@text:style-name = /office:document/office:automatic-styles/style:style[contains(@style:parent-style-name,'Novel-Paragraph')]/@style:name or contains(@text:style-name, 'Novel-Paragraph')]"/>

<xsl:variable name="about" select="/office:document/office:body/office:text/text:p[contains(@text:style-name, 'Novel-About') or @text:style-name = /office:document/office:automatic-styles/style:style[contains(@style:parent-style-name, 'Novel-About')]/@style:name]"/></pre>

To access the variables you just use their name preceded by the dollar sign e.g $parts.

Series Navigation<< Outputting HTMLCreating a Hierarchical HTML File >>

Leave a Reply

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