Previous Page Next Page

Recipe 7.4. Displaying a Hierarchy

Problem

You want to create text output that is indented or annotated to reflect the hierarchal nature of the original XML.

Solution

The most obvious hierarchical representation uses indentation to mimic the hierarchical structure of the source XML. You can create a generic stylesheet, shown in Example 7-29 and Example 7-30, which makes reasonable choices for mapping the information in the input document to a hierarchical output.

Example 7-29. text.hierarchy.xslt
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:str="http://www.ora.com/XSLTCookbook/namespaces/strings">
   
<xsl:include href="../strings/str.dup.xslt"/>
<xsl:include href="../strings/str.replace.xslt"/>
   
<xsl:output method="text"/>
   
<!--Levels indented with two spaces by default -->
<xsl:param name="indent" select=" '  ' "/>
   
<xsl:template match="*">
  <xsl:param  name="level" select="count(./ancestor::*)"/>
  
  <!-- Indent this element -->
  <xsl:call-template name="str:dup" >
    <xsl:with-param name="input" select="$indent"/>
    <xsl:with-param name="count" select="$level"/>
  </xsl:call-template>
  
  <!--Process the element name. Default will output local-name -->
  <xsl:apply-templates select="." mode="name">
    <xsl:with-param name="level" select="$level"/>
  </xsl:apply-templates>
  
  <!--Signal the start of processing of attributes. 
      Default will output '(' -->
  <xsl:apply-templates select="." mode="begin-attributes">
    <xsl:with-param name="level" select="$level"/>
  </xsl:apply-templates>
  
  <!--Process attributes. 
      Default will output name="value". -->
  <xsl:apply-templates select="@*">
    <xsl:with-param name="element" select="."/>
    <xsl:with-param name="level" select="$level"/>
  </xsl:apply-templates>
  
  <!--Signal the end of processing of attributes. 
      Default will output ')' -->
  <xsl:apply-templates select="." mode="end-attributes">
    <xsl:with-param name="level" select="$level"/>
  </xsl:apply-templates>
  
  <!-- Process the elements value. -->
  <!-- Default will format the value of a leaf element -->
  <!-- so it is indented at next line -->
  <xsl:apply-templates select="." mode="value">
    <xsl:with-param name="level" select="$level"/>
  </xsl:apply-templates>
  
  <xsl:apply-templates select="." mode="line-break">
    <xsl:with-param name="level" select="$level"/>
  </xsl:apply-templates>
 
  <!-- Process children -->
  <xsl:apply-templates select="*">
    <xsl:with-param name="level" select="$level + 1"/>
  </xsl:apply-templates>
  
</xsl:template>
   
<!--Default handling of element names. -->
<xsl:template match="*"     mode="name">[<xsl:value-of 
                                    select="local-name(.)"/></xsl:template>
   
<!--Default handling of start of attributes. -->
<xsl:template match="*" mode="begin-attributes">
  <xsl:if test="@*"><xsl:text> </xsl:text></xsl:if>
</xsl:template>
   
<!--Default handling of attributes. -->
<xsl:template match="@*">
  <xsl:value-of select="local-name(.)"/>="<xsl:value-of select="."/>"<xsl:text/>
  <xsl:if test="position( ) != last( )">
    <xsl:text> </xsl:text>
  </xsl:if>
</xsl:template>
   
<!--Default handling of end of attributes. -->
<xsl:template match="*" mode="end-attributes">]</xsl:template>
   
<!--Default handling of element values. -->
<xsl:template match="*" mode="value">
  <xsl:param name="level"/>
   
  <!-- Only output value for leaves -->
  <xsl:if test="not(*)">
    <xsl:variable name="indent-str">
      <xsl:call-template name="str:dup" >
        <xsl:with-param name="input" select="$indent"/>
        <xsl:with-param name="count" select="$level"/>
      </xsl:call-template>
    </xsl:variable>
    
    <xsl:text>&#xa;</xsl:text>
    
    <xsl:value-of select="$indent-str"/>
    
    <xsl:call-template name="str:replace">
      <xsl:with-param name="input" select="."/>
      <xsl:with-param name="search-string" select=" '&#xa;' "/>
      <xsl:with-param name="replace-string" 
                      select="concat('&#xa;',$indent-str)"/>
    </xsl:call-template>
  </xsl:if>
</xsl:template>
   
<xsl:template match="*" mode="line-break">
  <xsl:text>&#xa;</xsl:text>
</xsl:template>
  
</xsl:stylesheet>

Example 7-30. Output when used to process ExpenseReport.xml
[ExpenseReport statementNum="123"]
  [Employee]
    [Name]
    Salvatore Mangano
    [SSN]
    999-99-9999
    [Dept]
    XSLT Hacking
    [EmpNo]
    1
    [Position]
    Cook
    [Manager]
    Big Boss O'Reilly
  [PayPeriod]
    [From]
    1/1/02
    [To]
    1/31/02
  [Expenses]
    [Expense]
      [Date]
      12/20/01
      [Account]
      12345
      [Desc]
      Goofing off instead of going to conference.
      [Lodging]
      500.00
      [Transport]
      50.00
      [Fuel]
      0
      [Meals]
      300.00
      [Phone]
      100
      [Entertainment]
      1000.00
      [Other]
      300.00
    [Expense]
      [Date]
      12/20/01
      [Account]
      12345
      [Desc]
      On the beach
      [Lodging]
      500.00
      [Transport]
      50.00
      [Fuel]
      0
      [Meals]
      200.00
      [Phone]
      20
      [Entertainment]
      300.00
      [Other]
      100.00

XSLT 2.0

There are a few improvements that can be made to the preceding code if you are using XSLT 2.0. First, you can use the built-in replace function in XPath 2.0 and the functional dup that we presented in Recipe 7.3.

Discussion

You might object to the particular choices made by this stylesheet for mapping the information items in the source document to a hierarchical layout. That objection is OK because the stylesheet was designed to be customized. For example, you might prefer the results obtained with the customizations shown in Example 7-31 and Example 7-32.

Example 7-31. Customized Expense Report stylesheet
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
<xsl:import href="text.hierarchy.xslt"/>
   
<!--Ignore attributes -->
<xsl:template match="@*"/>
<xsl:template match="*" mode="begin-attributes"/>
<xsl:template match="*" mode="end-attributes"/>
   
<xsl:template match="*"     mode="name">
  <!--Display element loacl name-->
  <xsl:value-of select="local-name(.)"/>
  <!--Follow by a colon+space if a leaf -->
  <xsl:if test="not(*)">: </xsl:if>
</xsl:template>
   
<xsl:template match="*" mode="value">
  <xsl:if test="not(*)">
    <xsl:value-of select="."/>
  </xsl:if>
</xsl:template>
   
</xsl:stylesheet>

Example 7-32. Output with overridden formatting
ExpenseReport
  Employee
    Name: Salvatore Mangano
    SSN: 999-99-9999
    Dept: XSLT Hacking
    EmpNo: 1
    Position: Cook
    Manager: Big Boss O'Reilly
  PayPeriod
    From: 1/1/02
    To: 1/31/02
  Expenses
    Expense
      Date: 12/20/01
      Account: 12345
      Desc: Goofing off instead of going to conference.
      Lodging: 500.00
      Transport: 50.00
      Fuel: 0
      Meals: 300.00
      Phone: 100
      Entertainment: 1000.00
      Other: 300.00
    Expense
      Date: 12/20/01
      Account: 12345
      Desc: On the beach
      Lodging: 500.00
      Transport: 50.00
      Fuel: 0
      Meals: 200.00
      Phone: 20
      Entertainment: 300.00
      Other: 100.00

Or perhaps you like the format in Example 7-33 and Example 7-34, inspired by Jeni Tennison.

Example 7-33. tree-control.xslt
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
<xsl:import href="text.hierarchy.xslt"/>
   
<!--Ignore attributes -->
<xsl:template match="@*"/>
<xsl:template match="*" mode="begin-attributes"/>
<xsl:template match="*" mode="end-attributes"/>
   
<xsl:template match="*"     mode="name">
  <!--Display element loacl name-->
  <xsl:text>[</xsl:text>
  <xsl:value-of select="local-name(.)"/>
  <!--Follow by a colon+space if a leaf -->
  <xsl:text>] </xsl:text>
</xsl:template>
   
<xsl:template match="*" mode="value">
  <xsl:if test="not(*)">
    <xsl:value-of select="."/>
  </xsl:if>
</xsl:template>
   
<xsl:template match="*" mode="indent">
  <xsl:for-each select="ancestor::*">
    <xsl:choose>
      <xsl:when test="following-sibling::*"> | </xsl:when>
      <xsl:otherwise><xsl:text>   </xsl:text></xsl:otherwise>
    </xsl:choose>
  </xsl:for-each>
  <xsl:choose>
    <xsl:when test="*"> o-</xsl:when>
    <xsl:when test="following-sibling::*"> +-</xsl:when>
    <xsl:otherwise> `-</xsl:otherwise>
  </xsl:choose>
</xsl:template>
   
<xsl:template match="*" mode="line-break">
  <xsl:text>&#xa;</xsl:text>
</xsl:template>
   
</xsl:stylesheet>

Example 7-34. Output with tree-control-like formatting
o-[ExpenseReport]
    o-[Employee]
    |  +-[Name] Salvatore Mangano
    |  +-[SSN] 999-99-9999
    |  +-[Dept] XSLT Hacking
    |  +-[EmpNo] 1
    |  +-[Position] Cook
    |  `-[Manager] Big Boss O'Reilly
    o-[PayPeriod]
    |  +-[From] 1/1/02
    |  `-[To] 1/31/02
    o-[Expenses]
       o-[Expense]
       |  +-[Date] 12/20/01
       |  +-[Account] 12345
       |  +-[Desc] Goofing off instead of going to conference.
       |  +-[Lodging] 500.00
       |  +-[Transport] 50.00
       |  +-[Fuel] 0
       |  +-[Meals] 300.00
       |  +-[Phone] 100
       |  +-[Entertainment] 1000.00
       |  `-[Other] 300.00
       o-[Expense]
          +-[Date] 12/20/01
          +-[Account] 12345
          +-[Desc] On the beach
          +-[Lodging] 500.00
          +-[Transport] 50.00
          +-[Fuel] 0
          +-[Meals] 200.00
          +-[Phone] 20
          +-[Entertainment] 300.00
          `-[Other] 100.00

You can take this concept even further by creating a stylesheet that imports tree-control.xslt and takes a global parameter containing a list of element names that should be collapsed. Collapsed levels are indicated by an x prefix. See Example 7-35 and Example 7-36.

Example 7-35. Stylesheet creating collapsed levels
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
<xsl:import href="tree-control.xslt"/>
   
<xsl:param name="collapse"/>
<xsl:variable name="collapse-test" select="concat(' ',$collapse,' ')"/>
   
<xsl:template match="*"     mode="name">
    <xsl:if test="not(ancestor::*[contains($collapse-test,
                                   concat(' ',local-name(.),' '))])">
      <xsl:apply-imports/>
    </xsl:if>
</xsl:template>
   
<xsl:template match="*" mode="value">
    <xsl:if test="not(ancestor::*[contains($collapse-test,
                                   concat(' ',local-name(.),' '))])">
      <xsl:apply-imports/>
    </xsl:if>
</xsl:template>
   
<xsl:template match="*" mode="line-break">
    <xsl:if test="not(ancestor::*[contains($collapse-test,
                                   concat(' ',local-name(.),' '))])">
      <xsl:apply-imports/>
    </xsl:if>
</xsl:template>
   
<xsl:template match="*" mode="indent">
  <xsl:choose>
    <xsl:when test="self::*[contains($collapse-test,
                                       concat(' ',local-name(.),' '))]">
      <xsl:for-each select="ancestor::*">
        <xsl:text>   </xsl:text>
      </xsl:for-each>
      <xsl:text> x-</xsl:text>
    </xsl:when>
    <xsl:when test="ancestor::*[contains($collapse-test,
                                 concat(' ',local-name(.),' '))]"/>
    <xsl:otherwise>
      <xsl:apply-imports/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>
   
</xsl:stylesheet>

Example 7-36. Output with $collapse="Employee PayPeriod"
o-[ExpenseReport]
    x-[Employee]
    x-[PayPeriod]
    o-[Expenses]
       o-[Expense]
       |  +-[Date] 12/20/01
       |  +-[Account] 12345
       |  +-[Desc] Goofing off instead of going to conference.
       |  +-[Lodging] 500.00
       |  +-[Transport] 50.00
       |  +-[Fuel] 0
       |  +-[Meals] 300.00
       |  +-[Phone] 100
       |  +-[Entertainment] 1000.00
       |  `-[Other] 300.00
       o-[Expense]
          +-[Date] 12/20/01
          +-[Account] 12345
          +-[Desc] On the beach
          +-[Lodging] 500.00
          +-[Transport] 50.00
          +-[Fuel] 0
          +-[Meals] 200.00
          +-[Phone] 20
          +-[Entertainment] 300.00
          `-[Other] 100.00

There is literally no end to the variety of custom tree formats you can create from overrides to the basic stylesheet. In object-oriented circles, this technique is called the template-method pattern. It involves building the skeleton of an algorithm and allowing subclasses to redefine certain steps without changing the algorithm's structure. In the case of XSLT, importing stylesheets take the place of subclasses. The power of this example does not stem from the fact that creating tree-like rendering is difficult; it is not. Instead, the power lies in the ability to reuse the example's structure while considering only the aspects you want to change.


Previous Page Next Page