1
votes

I need to create HTML unordered lists from a flat XML structure using XSLT 1.0. The input XML consists of a series of nodes to be transformed into list items. However, this series may be interrupted by non-list nodes of different types:

<input>
  <paragraph>abc</paragraph>
  <paragraph>def</paragraph>
    <listable>123</listable>
    <listable>456</listable>
  <other-block>
    <other-text>Foo</other-text>
  </other-block>
    <listable>789</listable>
    <listable>012</listable>
</input>

My objective is:

<div class="output">
  <p>abc</p>
  <p>def</p>
  <ul>
    <li>123</li>
    <li>456</li>
  </ul>
  <div class="my-block">
    <p class="other">Foo</p>
  </div>
  <ul>
    <li>789</li>
    <li>012</li>
  </ul>
</div>

I found a old thread with a solution that almost works for me (last solution on the page, by Dimitre Novatchev) and adapted it. This is a minimal stylesheet based on that solution:

<?xml version="1.0"?>

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output method="xml" omit-xml-declaration="yes" encoding="utf-8" indent="yes" />
    <xsl:strip-space elements="*" />

    <!-- IDENTITY TRANSFORM: -->
    <xsl:template match="node()|@*" name="identity">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*" />
        </xsl:copy>
    </xsl:template>

    <!-- NON-LIST ITEMS: -->
    <xsl:template match="input">
        <div class="output">
            <xsl:apply-templates />
        </div>
    </xsl:template>

    <xsl:template match="paragraph">
        <p>
            <xsl:apply-templates />
        </p>
    </xsl:template>

    <xsl:template match="other-block">
        <div class="my-block">
            <xsl:apply-templates select="descendant::other-text" /> 
        </div>
    </xsl:template> 

    <xsl:template match="other-text">
        <p class="other">
            <xsl:copy-of select="text()" />
        </p>
    </xsl:template>

    <!-- LIST HANDLING: -->
    <xsl:key name="kFollowingUL" match="listable" 
                use="generate-id(preceding-sibling::*[not(self::listable)][1])"/>

    <xsl:template match="*[not(self::listable) and following-sibling::*[1][self::listable]]">

        <xsl:call-template name="identity" />

        <xsl:variable name="vFolUL"
                select="key('kFollowingUL',generate-id())"/>

        <xsl:if test="$vFolUL">
            <ul>
                <xsl:apply-templates mode="copy"
                        select="key('kFollowingUL',generate-id())" />
            </ul>
        </xsl:if>

    </xsl:template>

    <xsl:template match="listable" mode="copy">
        <li>
            <xsl:value-of select="normalize-space()" />
        </li>
    </xsl:template>   

    <xsl:template match="listable" />

</xsl:stylesheet>

The problem with this approach is that it does not apply transformations to the last non-listable node before each list. The <paragraph> and <other-block> nodes in the input are copied directly to the output, although templates are applied to descendants of <other-block>:

<div class="output">
  <p>abc</p>
  <paragraph>def</paragraph>
  <ul>
    <li>123</li>
    <li>456</li>
  </ul>
  <other-block>
    <p class="other">Foo</p>
  </other-block>
  <ul>
    <li>789</li>
    <li>012</li>
  </ul>
</div>

Can anyone suggest a way to modify the earlier XSLT 1.0 solution and add transformation of the last nodes before the listable groups?

2
Rather than just writing the code for you, we'd prefer to be able to answer a specific question you have about how to do something. With that in mind, it would be helpful if you could provide a minimal, complete, and verifiable example, along with a specific question from you about how something does or does not work. For instance, I don't know what you don't know. In addition, I tried applying Dimitre's code to your sample input, and I got different output from your post. So please also post what XSL you are using.Eiríkr Útlendi
I modified my question to include a working example of the input, XSLT, and output.Peter Parka

2 Answers

1
votes

I would do it this way:

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:strip-space elements="*"/>

<xsl:template match="/input">
    <div class="output">
        <xsl:apply-templates/>
    </div>
</xsl:template>

<xsl:template match="paragraph">
    <p>
        <xsl:apply-templates/>
    </p>
</xsl:template>

<xsl:template match="other-block">
    <div class="my-block">
        <xsl:apply-templates/> 
    </div>
</xsl:template> 

<xsl:template match="other-text">
    <p class="other">
        <xsl:apply-templates/>      
    </p>
</xsl:template>

<xsl:template match="listable">
    <xsl:if test="not(preceding-sibling::*[1][self::listable])">
        <ul>
            <xsl:apply-templates select="." mode="list"/>       
        </ul>
    </xsl:if>
</xsl:template>

<xsl:template match="listable" mode="list">
    <li>
        <xsl:apply-templates/>      
    </li>
    <xsl:apply-templates select="following-sibling::*[1][self::listable]" mode="list"/>         
</xsl:template>

</xsl:stylesheet>
0
votes

Your problem came from this template:

<xsl:template match="*[not(self::listable) and following-sibling::*[1][self::listable]]">

    <xsl:call-template name="identity" />

    <xsl:variable name="vFolUL"
            select="key('kFollowingUL',generate-id())"/>

    <xsl:if test="$vFolUL">
        <ul>
            <xsl:apply-templates mode="copy"
                    select="key('kFollowingUL',generate-id())" />
        </ul>
    </xsl:if>

</xsl:template>

This match any element with a listable element as the first following sibling. Then in the content template it calls the template named identity (in this case, it's the identity rule). This has better default priority than the other template for other-block elements:

<xsl:template match="other-text">
    <p class="other">
        <xsl:apply-templates/>      
    </p>
</xsl:template>

I like michael.hor257k solution wich is the approach I gave in the original answer. Another possible solution would be to follow the same principle:

<xsl:template match="*[not(self::listable) and following-sibling::*[1][self::listable]]">

    <xsl:call-template name="separator" />

    <xsl:variable name="vFolUL"
            select="key('kFollowingUL',generate-id())"/>

    <xsl:if test="$vFolUL">
        <ul>
            <xsl:apply-templates mode="copy"
                    select="key('kFollowingUL',generate-id())" />
        </ul>
    </xsl:if>

</xsl:template>

<xsl:template match="other-text" name="separator">
    <p class="other">
        <xsl:apply-templates/>      
    </p>
</xsl:template>

But, do note that this doesn't scale well.