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?

Community
  • 1
  • 1
Eric
  • 95,302
  • 53
  • 242
  • 374
  • You fail to explain exactly what the transformation should do. The problem is that you cannot define the problem, not that XSLT cannot be used to solve (what to solve???) – Dimitre Novatchev May 25 '10 at 22:56
  • The problem is to write an XSLT that will turn `C.xml` into the HTML above. For brevity, I din't include my failed solutions. – Eric May 26 '10 at 07:22

3 Answers3

2

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
  • 332,285
  • 67
  • 532
  • 628
  • Exactly what does `node()` mean? Is it equivalent to `.`? – Eric May 25 '10 at 19:51
  • 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 May 25 '10 at 19:56
  • @Eric: I've completely revised my solution and deleted all comments that referred to the previous version. Please have another look at it. – Tomalak May 25 '10 at 22:44
  • That looks better. I had to look up ``. Is there any notional difference between `select="'content-a','content-b','content-c'"` and `select="content-a | content-b | content-c"`? – Eric May 26 '10 at 07:25
  • Yes. The former one is a sequence of strings. The latter one selects elements and would not work in this context. – Tomalak May 26 '10 at 07:37
  • Nevermind, I worked that out. No longer works client-side though, as nothing supports XSLT 2.0. – Eric May 26 '10 at 07:46
  • @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 May 26 '10 at 08:01
  • @Eric: It does not look like it. I've added an 1.0 solution that relies on the `node-set()` extension function. – Tomalak May 26 '10 at 09:31
  • I can't get the extension function to work client-side either. – Eric May 26 '10 at 10:10
  • @Eric: As was to be expected. I thought you would use PHP on the server to do the transformation? – Tomalak May 26 '10 at 11:22
  • 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 May 26 '10 at 12:10
  • @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 May 26 '10 at 12:22
  • Ok, I've got it working with an XSLT processor, but it's a no-go with PHP. – Eric May 26 '10 at 16:18
  • Wait, that was _REALLY_ dumb. It works fine. Thanks a lot. I had chrome's console up, not the source viewer :P. – Eric May 26 '10 at 16:41
1
<?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
  • 4,511
  • 1
  • 25
  • 30
  • Doesn't that make document C read document B 3 times? Or is that unavoidable? – Eric May 25 '10 at 19:55
  • 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 May 25 '10 at 20:21
-1

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
  • 95,302
  • 53
  • 242
  • 374
  • Too complex and repetitive, if you ask me. Apart from that it would not produce the output you ask for, see your usage of `
    `.
    – Tomalak May 25 '10 at 20:50
  • Would it not? I can't see why. And it does have the advantage that each file is not read multiple times. – Eric May 25 '10 at 21:04
  • You enclose your `content-[abc]` in `
    ` in this solution, but in `

    ` in your question. (BTW I've just noticed that you want something different than my solution does. I must re-write it.)

    – Tomalak May 25 '10 at 21:59