XSLT: how to nest flat code?

2019-09-14 01:45发布

问题:

I try to nest the this sample XML-file. I tried to use simple templates, that would pass on the current node.

<?xml version="1.0" encoding="UTF-8"?>
<chapter>
    <h1>h1</h1>
    <p>text1</p>
    <p>text2</p>
    <p>text3</p>
    <h2>h2</h2>
    <p>text1</p>
    <p>text2</p>
    <p>text3</p>
    <p>text4</p>
    <p>text5</p>
    <p>text6</p>
    <h1>h1</h1>
    <p>text1</p>
    <p>text2</p>
    <p>text3</p>
    <h2>h2</h2>
    <h3>h3</h3>
    <p>text1</p>
    <p>text2</p>
    <p>text3</p>
    <h2>h2</h2>
    <p>text1</p>
    <p>text2</p>
    <p>text3</p>
</chapter>

with the following stylesheet:

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

    <xsl:template match="h1">
        <level_1>
            <head>
                <xsl:value-of select="."/>
            </head>
            <xsl:apply-templates select="following::*"/>
        </level_1>
    </xsl:template>

    <xsl:template match="h2">
        <head>
            <xsl:value-of select="."/>
        </head>
        <level_2>
            <head>
                <xsl:value-of select="."/>
            </head>
            <xsl:apply-templates select="following::*"/>
        </level_2>
    </xsl:template>

    <xsl:template match="h3">
        <head>
            <xsl:value-of select="."/>
        </head>
        <level_3>
            <head>
                <xsl:value-of select="."/>
            </head>
            <xsl:apply-templates select="following::*"/>
        </level_3>
    </xsl:template>

    <xsl:template match="p">
        <text>
            <xsl:value-of select="."></xsl:value-of>
        </text>
    </xsl:template>

</xsl:stylesheet>

But the result doesn't work at all. following::* does not work, because h1 also follows h2, so how does the template know, when to close?

The output should be:

<chapter>
<level_1>
<head>h1</head>
<text>text1</text>
<text>text2</text>
<text>text3</text>
<level_2>
<head>h2</head>
<text>text1</text>
<text>text2</text>
<text>text3</text>
<text>text4</text>
<text>text5</text>
<text>text6</text>
</level_2>
...
</level_1>
</chapter>

Thanks alot!

回答1:

OK, here goes.

Define a utility function to test whether an element is an hN:

<xsl:function name="f:isLevel" as="xs:boolean">
  <xsl:param name="h" as="element(*)"/>
  <xsl:param name="level" as="xs:integer"/>
  <xsl:sequence select="name($h) = concat('h', $level)"/>
</xsl:function>

Define a recursive function that performs the grouping for level N:

    <xsl:function name="f:group" as="element(*)*">
      <xsl:param name="input" as="element(*)*"/>
      <xsl:param name="level" as="xs:integer"/>
      <xsl:for-each-group 
                    group-starting-with="*[f:isLevel(., $level)]">
         <xsl:choose>
           <xsl:when test="f:isLevel(., $level)">
             <xsl:element name="level_{$level}">
               <head><xsl:copy-of select="."/></head>
               <xsl:sequence select="
                    f:group(remove(current-group(), 1), $level+1)"/>
             </xsl:element>
           </xsl:when>
           <xsl:otherwise>
              <xsl:sequence select="current-group()"/>
           </xsl:otherwise>
         </xsl:choose>
     </xsl:for-each-group>
  </xsl:function>

How does this work? Given an input like (p, p, h1, p, p, h1, p) it creates three groups: (p, p, p), (h1, p, p), and (h1, p). A group starting with h1 takes the xsl:when branch: it creates a levelN element containing the content of the first element (the h1), then the result of applying grouping recursively to rest of the group following the h1 element.

The initial group (with no leading h1) takes the xsl:otherwise branch and is simply copied to the output.

The recursion terminates when there are no elements named h{N}: in this case the for-each-group instruction only creates a single group, which is processed in the xsl:otherwise branch, causing no further recursion.

To run this you'll want something like

<xsl:template match="body">
  <xsl:copy-of select="f:group(*, 1)"/>
</xsl:template> 


回答2:

Here is one way you could look at it:

XSLT 1.0

<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="h2" match="h2" use="generate-id(preceding-sibling::h1[1])" />
<xsl:key name="h3" match="h3" use="generate-id(preceding-sibling::h2[1])" />
<xsl:key name="p" match="p" use="generate-id(preceding-sibling::*[starts-with(name(), 'h')][1])" />

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

<xsl:template match="h1">
    <level_1>
        <head>
            <xsl:value-of select="."/>
        </head>
        <xsl:apply-templates select="key('h2', generate-id()) | key('p', generate-id())"/>
    </level_1>
</xsl:template>     

<xsl:template match="h2">
    <level_2>
        <head>
            <xsl:value-of select="."/>
        </head>
        <xsl:apply-templates select="key('h3', generate-id()) | key('p', generate-id())"/>
    </level_2>
</xsl:template> 

<xsl:template match="h3">
    <level_3>
        <head>
            <xsl:value-of select="."/>
        </head>
        <xsl:apply-templates select="key('p', generate-id())"/>
    </level_3>
</xsl:template>     

<xsl:template match="p">
    <text>
        <xsl:value-of select="."/>
    </text>
</xsl:template>

</xsl:stylesheet>


标签: xslt