简体   繁体   中英

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.

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>

@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.

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>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM