You want to create text output that is indented or annotated to reflect the hierarchal nature of the original XML.
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 5-27 and Example 5-28, which makes reasonable choices for mapping the information in the input document to a hierarchical output.
Example 5-27. 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>
</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=" '
' "/> <xsl:with-param name="replace-string" select="concat('
',$indent-str)"/> </xsl:call-template> </xsl:if> </xsl:template> <xsl:template match="*" mode="line-break"> <xsl:text>
</xsl:text> </xsl:template> </xsl:stylesheet>
Example 5-28. 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 confrence. [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
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 5-29 and Example 5-30.
Example 5-29. 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 5-30. 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 confrence. 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 5-31 and Example 5-32, inspired by Jeni Tennison.
Example 5-31. 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>
</xsl:text> </xsl:template> </xsl:stylesheet>
Example 5-32. 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 confrence. | +-[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 5-33 and Example 5-34.
Example 5-33. 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 5-34. 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 confrence. | +-[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.