nrd22
nrd22

Reputation: 17

Copying and adding a parent node with multiple children

I made an error on a previous post.

I have XML data that works like this (this is only an example and number of chapters and pages are both variable).

<books>
 <chapter></chapter>
 <page></page>
 <page></page>
 <page></page>
 <chapter></chapter>
 <page></page>
 <page></page>
 <chapter></chapter>
 <page></page>
 <page></page>
 <page></page>
 <page></page>
</books>

I am trying to recreate it to look like this

<books>
 <book>
  <chapter></chapter>
  <page></page>
  <page></page>
  <page></page>
 </book>
 <book>
  <chapter></chapter>
  <page></page>
  <page></page>
 </book>
 <book>
  <chapter></chapter>
  <page></page>
  <page></page>
  <page></page>
  <page></page>
 </book>
</books>

As far as I can tell there isn't a way to put a loop inside a loop until there is a new chapter.

Upvotes: 0

Views: 400

Answers (3)

user663031
user663031

Reputation:

@JoelMLamsen has the right idea and his solution will work fine, but it could be simplified a bit to not use counting. We'll try to directly represent the basic logic of

For each chapter, process the following pages whose immediately preceding chapter is this one.

We can do it like this:

<xsl:template match="books">
    <books>
        <xsl:apply-templates select="chapter"/>
    </books>
</xsl:template>

<xsl:template match="chapter">
    <xsl:variable name="this" select="generate-id()"/>
    <book>
        <xsl:copy-of select="."/>
        <xsl:copy-of 
            select="following-sibling::page[generate-id(preceding-sibling::chapter[1]) = $this]"/>
    </book>
</xsl:template>

In case you need help understanding the condition, you can read it in English as:

following-sibling           of all the following
::page                      page elements
[                           take the ones where
  generate-id(              the unique id of 
    preceding-sibling       of all its preceding
    ::chapter               chapter elements
    [1]                     (the most recent one)
  )
  =                         is equal to
  $this                     the unique id of the chapter we are on
]

A couple of notes for those who are newer to XSLT:

  1. We remember the unique id of the current chapter in the this variable. Alternatively, we could use generate-id(current()) inside the [] condition.

  2. The preceding-sibling axis returns the results in reverse document order, so the [1] element is the immediately preceding one.

  3. Instead of looping over chapters in the root template using for-each, this uses templates for books and chapter, which some might say is a bit more idiomatic XSLT. The default root template will take care of invoking the books template.

Upvotes: 0

michael.hor257k
michael.hor257k

Reputation: 116957

I believe the simple - and efficient(!) - way to do this is by using a key:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:key name="page-by-chapter" match="page" use="generate-id(preceding-sibling::chapter[1])" />

<xsl:template match="/">
    <books>
        <xsl:for-each select="books/chapter">
            <book>
                <xsl:copy-of select=". | key('page-by-chapter', generate-id())"/>
            </book>
        </xsl:for-each>
    </books>
</xsl:template>

</xsl:stylesheet>

Upvotes: 0

Joel M. Lamsen
Joel M. Lamsen

Reputation: 7173

try something like this:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="1.0">

    <xsl:output indent="yes"/>

    <xsl:template match="/">
        <books>
            <xsl:for-each select="books/chapter">
                <!-- for each chapter node, record the number of preceding sibling,
                     for the first chapter there is none, so that is why I added +1,
                     so when I count all the preceding sibling chapter of page, I will
                     get a match -->
                <xsl:variable name="chapter_count" select="count(preceding-sibling::chapter) + 1"/>
                <book>
                    <xsl:copy-of select="."/>
                    <!-- This code will ensure that the following sibling pages that
                         will be copied has the same number of preceding sibling
                         chapter (for pages, notice that I did not add 1 in the
                         predicate). So for the first chapter node, $chapter_count is 1
                         and the number of preceding sibling chapters at page node is 1,
                         thus the match -->
                    <xsl:copy-of select="following-sibling::page[count(preceding-sibling::chapter) = $chapter_count]"/>
                </book>
            </xsl:for-each>
        </books>
    </xsl:template>

</xsl:stylesheet>

Upvotes: 1

Related Questions