Search code examples
xmlxsltrecursionfile-inclusion

Recursive XSLT, part 2


Ok, following on from my question here.

Lets say my pages are now like this:

A.xml:

<page>
    <header>Page A</header>
    <content-a>Random content for page A</content-a>
    <content-b>More of page A's content</content-b>
    <content-c>More of page A's content</content-c>
    <!-- This doesn't keep going: there are a predefined number of sections -->
</page>

B.xml:

<page include="A.xml">
    <header>Page B</header>
    <content-a>Random content for page B</content-a>
    <content-b>More of page B's content</content-b>
    <content-c>More of page B's content</content-c>
</page>

C.xml:

<page include="B.xml">
    <header>Page C</header>
    <content-a>Random content for page C</content-a>
    <content-b>More of page C's content</content-b>
    <content-c>More of page C's content</content-c>
</page>

After the transform (on C.xml), I'd like to end up with this:

<h1>Page C</h1>
<div>
    <p>Random content for page C</p>
    <p>Random content for page B</p>
    <p>Random content for page A</p>
</div>
<div>
    <p>More of page C's content</p>
    <p>More of page B's content</p>
    <p>More of page A's content</p>
</div>
<div>
    <p>Yet more of page C's content</p>
    <p>Yet more of page B's content</p>
    <p>Yet more of page A's content</p>
</div>

I know that I can use document(@include) to include another document. However, the recursion is a bit beyond me.

How would I go about writing such a transform?


Solution

  • Here is an XSLT 2.0 solution:

    <xsl:stylesheet 
      version="2.0"
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    >
      <xsl:template match="page">
        <xsl:variable name="pages">
          <xsl:apply-templates select="." mode="load" />
        </xsl:variable>
    
        <xsl:copy>
          <h1><xsl:value-of select="header" /></h1>
          <!-- you say there is a fixed number of names, so this should be OK -->
          <xsl:for-each select="'content-a','content-b','content-c'">
            <div>
              <xsl:apply-templates select="$pages/page/*[name() = current()]" />
            </div>
          </xsl:for-each>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="page" mode="load">
        <xsl:sequence select="." />
        <xsl:apply-templates select="document(@include)" mode="load" />
      </xsl:template>
    
      <xsl:template match="content-a|content-b|content-c">
        <p><xsl:value-of select="." /></p>
      </xsl:template>
    </xsl:stylesheet>
    

    EDIT: For XSLT 1.0, the equivalent solution would look like this:

    <xsl:stylesheet 
      version="1.0"
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      xmlns:exsl="http://exslt.org/common"
    >
      <xsl:template match="page">
        <xsl:variable name="pages-rtf"><!-- rtf = result tree fragment -->
          <xsl:apply-templates select="." mode="load" />
        </xsl:variable>
        <xsl:variable name="pages" select="exsl:node-set($pages-rtf)" />
    
        <!-- you say there is a fixed number of names, so this should be OK -->
        <xsl:variable name="nodes-rtf">
          <content-a/><content-b/><content-c/>
        </xsl:variable>
        <xsl:variable name="nodes" select="exsl:node-set($nodes-rtf)" />
    
        <xsl:copy>
          <h1><xsl:value-of select="header" /></h1>
          <xsl:for-each select="$nodes">
            <div>
              <xsl:apply-templates select="$pages/page/*[name() = name(current())]" />
            </div>
          </xsl:for-each>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="page" mode="load">
        <xsl:copy-of select="." />
        <xsl:apply-templates select="document(@include)" mode="load" />
      </xsl:template>
    
      <xsl:template match="content-a|content-b|content-c">
        <p><xsl:value-of select="." /></p>
      </xsl:template>
    </xsl:stylesheet>