5.3. Transformation into a Compact Textual Form

We will now use an XSLT stylesheet (shown in Example 5.9) to produce the compact form of an XML file. The output of this transformation will only be a stream of plain characters without any tags. This shows that XSLT can be used to transform XML input into a plain text file.

Example 5.8. Text compaction of the cellar book of Example 2.2 produced by the stylesheet of Example 5.9.

Some lines and parts of lines indicated by ... have been omitted here.

cellar-book[@noNamespaceSchemaLocation[CellarBook.xsd]
            wine-catalog[wine[@name[Prado Rey Roble]
                              @appellation[Ribera-del-duero]
                              @classification[d.o.]
                              @code[C00929026]
                              @format[magnum]
                              properties[color[red]
                                         alcoholic-strength[12.5]
                                         nature[still]]
                              origin[country[Spain]
                                     region[Old Castille]
                                     producer[Real Sitio de Ventosilla SA]]
                              price[35.25]
                              year[2002]]
                         wine[...]]
            owner[name[first[Jude]
                       family[Raisin]]
                  street[1234 rue des Chateaux]
                  city[St-George]
                  province[ON]
                  postal-code[M7W 7S0]]
            location[street[4587 des Futailles]
                     city[Vallée des crus]
                     province[QC]
                     postal-code[H3C 4J8]]
            cellar[wine[@code[C00043125]
                        purchaseDate[2005-06-20]
                        quantity[2]
                        comment[bold[Guy Lapalme, Montréal]
                                : should reorder soon]]
                   ...
                   wine[@code[C00929026]
                        purchaseDate[2003-10-15]
                        quantity[1]
                        comment[for
                                bold[big]
                                parties]]]]
        


The algorithm given Example 5.9 follows the same pattern as the one explained in Section 5.2.3. We recursively follow the structure of the tree and output a corresponding stream of characters. In our case, only one rule is applied to all element nodes: we output the name of the element and then output its attributes and children, with an indentation corresponding to the number of characters in the name of the parent element. The root node has an indentation of 0. Because we want an output with few blank lines, we only change line after having written the first attribute or child element. This rule is implemented with the template starting on line 40‑8. The * in the match attribute indicates that this rule applies to all element nodes not matched by a more specific rule such as the one on line 8‑3 that matches only the root node. In this latter rule, we apply the general rule to all children with xsl:apply-templates without any select attribute. All templates have a parameter indicating the indentation given as an xsl:with-param element and declared in the template with the xsl:param element.

Characters are put in the output stream literally by xsl:text elements. Text can also be computed with xsl:value-of elements whose select attribute is an XPath expression. Conditions can be introduced with an xsl:if element whose test attribute can refer to the current context; here count(../@*) counts the number of attributes of the parent and position() indicates the rank of the current node among its siblings. With this information, we can decide if the current line should be ended and insert the appropriate number of blanks according to the value of the indent parameter before outputting the information on this line. After the output of the element name, we call the templates for all attributes, elements and text nodes of this node (this is achieved with the select attribute on line 47‑9). In our case, we also update the current indentation that will be given to all these nodes. The output of all these recursive template applications will be enclosed in a pair of square brackets.

Because all characters and new lines in the stylesheets are returned as they appear in the input (including \n and leading and trailing spaces between elements), it can be difficult to achieve a specific output format. This is not really a problem if the output of the transformation is HTML, because in this case these spurious spaces and newlines are removed before being displayed. In our case, the transformation output is given as is to the user so it is simpler to only output the content of stylesheet elements without any \n and without leading and trailing spaces. This is why, on line 5‑1, we declare that all elements (*) of this stylesheet should ignore all spaces in the instance file.

On line 22‑5, we define the template to output the value of an attribute, which is simply the name of the attribute preceded by an @ and followed by its value in square brackets. If the attribute is not the first one, the line is ended and a new indentation is produced.

On line 32‑6, we also check if we need to end the current line and then we output the value of the current node with all extraneous space removed using the normalize-space function. This removes all whitespace at the start and at the end of the value and leaves only one space between non-space characters.

To output a given number of spaces, we have defined an XPath function called nl-and-indent (line 16‑4) with one parameter that it used to create a sequence of spaces, which are joined into one string preceded by a newline.

Example 5.9. [compact.xsl]: Stylesheet used to transform the cellar book instance document (Example 2.2) into Example 5.8

  1 <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:gl="http://www.iro.umontreal.ca/lapalme"
        version="2.0">
        
  5     <xsl:strip-space elements="*"/>                                  (1)
        <xsl:output omit-xml-declaration="yes" method="text"/>           (2)
        
        <xsl:template match="/">                                         (3)
             <xsl:apply-templates>
 10             <xsl:with-param name="indent"  select="0"/>
            </xsl:apply-templates>
            <xsl:text>
    </xsl:text>
        </xsl:template>
 15     
        <xsl:function name="gl:nl-and-indent">                           (4)
            <xsl:param name="nb"/>
            <xsl:value-of select="concat('&#xA;',
                string-join(for $i in 1 to $nb return ' ',''))"/>
 20     </xsl:function>
        
        <xsl:template match="@*">                                        (5)
            <xsl:param name="indent"/>
            <xsl:if test="position()>1">
 25             <xsl:value-of select="gl:nl-and-indent($indent)"/>
            </xsl:if>        
            <xsl:text>@</xsl:text>
            <xsl:value-of select="local-name()"/>
            <xsl:text>[</xsl:text><xsl:value-of select="."/><xsl:text>]</xsl:text>
 30     </xsl:template>
        
        <xsl:template match="text()">                                    (6)
            <xsl:param name="indent"/>
            <xsl:if test="count(../@*)>0 or position()>1">
 35             <xsl:value-of select="gl:nl-and-indent($indent)"/>
            </xsl:if>        
            <xsl:value-of select="normalize-space(.)"/>                  (7)
        </xsl:template>
        
 40     <xsl:template match="*">                                         (8)
            <xsl:param name="indent"/>
            <xsl:if test="count(../@*)>0 or position()>1">
                <xsl:value-of select="gl:nl-and-indent($indent)"/>
            </xsl:if>        
 45         <xsl:value-of select="local-name()"/>
            <xsl:text>[</xsl:text>
            <xsl:apply-templates select="@*|*|text()">                   (9)
                 <xsl:with-param name="indent" 
                     select="$indent + string-length(local-name())+1"/>
 50         </xsl:apply-templates>
            <xsl:text>]</xsl:text>
         </xsl:template>
        
    </xsl:stylesheet>
 55 
    

1

The XSLT processor will ignore all whitespace nodes from the input so that the output only contains spaces explicitly inserted by the stylesheet.

2

Indicates that the output will be plain text, thus we do not want an XML declaration to be emitted.

3

At the root, starts to apply the compaction algorithm with an indentation of 0. Forces a new line at the end of the output with the content of an xsl:text tag.

4

XPath function for outputting a number of spaces given by the indent parameter preceded by a new line indicated by a character entity.

5

For an attribute, outputs the name of the attribute preceded by an @ followed by its value enclosed in square brackets. If this is not the first attribute, indent the following line.

6

For a text node, outputs its normalized value. If there were attributes or if this text node is not the first child, indent the following line.

7

Normalizes a text value, i.e. remove the surrounding spaces and line breaks.

8

For an element, outputs the element name followed by the output of the recursive call to the compaction of attributes, children and text nodes are enclosed in square brackets. If there are any attributes and it this element is not the first one, indent the following line.

9

Updates the indentation for the children nodes with the length of the element name plus one to take into account the open bracket.