views:

118

answers:

3

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?

+2  A: 

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>
Tomalak
Exactly what does `node()` mean? Is it equivalent to `.`?
Eric
I'm a little puzzled by `page/*[starts-with(name(), 'content-')]`. Besides, the names of the elements do not follow a set pattern anyway: I just called them that for generality. Assume that the elements are actually called `Alice`, `Bob`, and `Chris`.
Eric
@Eric: I've completely revised my solution and deleted all comments that referred to the previous version. Please have another look at it.
Tomalak
That looks better. I had to look up `<xsl:sequence />`.Is there any notional difference between `select="'content-a','content-b','content-c'"` and `select="content-a | content-b | content-c"`?
Eric
Yes. The former one is a sequence of strings. The latter one selects elements and would not work in this context.
Tomalak
Nevermind, I worked that out. No longer works client-side though, as nothing supports XSLT 2.0.
Eric
@Eric: There is a way to do it in XSLT 1.0, but that would either require the use of the `node-set()` extension function, or reading the same documents multiple times.
Tomalak
PHP supports XSLT2.0, right?
Eric
@Eric: It does not look like it. I've added an 1.0 solution that relies on the `node-set()` extension function.
Tomalak
I can't get the extension function to work client-side either.
Eric
@Eric: As was to be expected. I thought you would use PHP on the server to do the transformation?
Tomalak
Yes, I will. Only my hosting seems to have disabled all PHP error reporting, so I wanted to test it in a more debug-able environment.
Eric
@Eric: There are plenty of command-line XSLT processors, and EXSLT support is not uncommon. Maybe you should try one of them - developing/debugging XSLT in the browser is not the best idea. There are also XSLT debuggers with breakpoints, variable inspection and line-by-line code execution available.
Tomalak
Ok, I've got it working with an XSLT processor, but it's a no-go with PHP.
Eric
Wait, that was _REALLY_ dumb. It works fine. Thanks a lot. I had chrome's console up, not the source viewer :P.
Eric
+1  A: 
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

  <xsl:template match="page">
    <h1>
      <xsl:value-of select="header"/>
    </h1>
    <div>
      <xsl:apply-templates select="." mode="content-a"/>
    </div>
    <div>
      <xsl:apply-templates select="." mode="content-b"/>
    </div>
    <div>
      <xsl:apply-templates select="." mode="content-c"/>
    </div>
  </xsl:template>

  <xsl:template match="page" mode="content-a">
    <p><xsl:value-of select="content-a"/></p>
    <xsl:if test="@include">
      <xsl:apply-templates select="document(@include)" mode="content-a"/>
    </xsl:if>    
  </xsl:template>

  <xsl:template match="page" mode="content-b">
    <p><xsl:value-of select="content-b"/></p>
    <xsl:if test="@include">
      <xsl:apply-templates select="document(@include)" mode="content-b"/>
    </xsl:if>
  </xsl:template>

  <xsl:template match="page" mode="content-c">
    <p><xsl:value-of select="content-c"/></p>
    <xsl:if test="@include">
      <xsl:apply-templates select="document(@include)" mode="content-c"/>
    </xsl:if>
  </xsl:template>

  <xsl:template match="page" mode="header">
    <xsl:value-of select="header"/>
    <xsl:if test="@include">
      <xsl:apply-templates select="document(@include)" mode="header"/>
    </xsl:if>
  </xsl:template>

</xsl:stylesheet>
nuqqsa
Doesn't that make document C read document B 3 times? Or is that unavoidable?
Eric
Yes, it does. Not sure how does this affect performance, I guess it depends on the XSLT engine. This is a poor-man's solution :) It works, yet I'd actually go for a generic solution as @Tomalak suggests.
nuqqsa
A: 

Here's the solution I came up with. This one only loads each subpage once, rather than once per content:

<xsl:template match="content-a | content-b | content-c">
    <p><xsl:value-of select="."></p>
</xsl:template>

<xsl:template name="get-content">
    <xsl:param name='page' />
    <xsl:param name='content-a' />
    <xsl:param name='content-b' />
    <xsl:param name='content-c' />
    <xsl:choose>
        <xsl:when test="$page/@include">
            <xsl:for-each select="document($page/@include)/page"> <!--set context-->
                <xsl:call-template name="get-members">
                    <xsl:with-param name="content-a" select="$content-a | content-a" />
                    <xsl:with-param name="content-b" select="$content-b | content-b" />
                    <xsl:with-param name="content-c" select="$content-c | content-c" />
                </xsl:call-template>
            </xsl:xsl:for-each>
        </xsl:when>
        <xsl:otherwise>
            <div>
                <xsl:apply-templates select="$content-a" />
            </div>
            <div>
                <xsl:apply-templates select="$content-b" />
            </div>
            <div>
                <xsl:apply-templates select="$content-c" />
            </div>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

<xsl:template match="page">
    <h1><xsl:value-of select="header"></h1>
    <xsl:call-template name="get-members">
        <xsl:with-param name="content-a" select="content-a" />
        <xsl:with-param name="content-b" select="content-b" />
        <xsl:with-param name="content-c" select="content-c" />
    </xsl:call-template>
</xsl:template>

Does this strike you as better or worse than the other solutions?

Eric
Too complex and repetitive, if you ask me. Apart from that it would not produce the output you ask for, see your usage of `<div>`.
Tomalak
Would it not? I can't see why. And it does have the advantage that each file is not read multiple times.
Eric
You enclose your `content-[abc]` in `<div>` in this solution, but in `<p>` in your question. (BTW I've just noticed that you want something different than my solution does. I must re-write it.)
Tomalak