sauceboat
sauceboat

Reputation: 43

Grouping Adjacent Items using XSLT 1.0

I'm hoping to get some advice on how to group adjacent elements with the same name in an unordered list using XSLT 1.0.

Here's some example XML:

<Article>
  <TextContent>
    <p>lorem</p>
    <VisualContent id="1" />
    <VisualContent id="2" />
    <VisualContent id="3" />
    <p>ipsum</p>
    <VisualContent id="4" />
    <p>dolor</p>
    <VisualContent id="5" />
  </TextContent>
</Article>

Here's the output I'd like:

<Article>
  <HtmlContent>
    <p>lorem</p>
    <ul>
      <li><img data-id="1" /></li>
      <li><img data-id="2" /></li>
      <li><img data-id="3" /></li>
    </ul>
    <p>ipsum</p>
    <img data-id="4" />
    <p>dolor</p>
    <img data-id="5" />
  </HtmlContent>
</Article>

Unfortunately, XSLT 1.0 is a strict requirement for this one. Any suggestions would be greatly appreciated.

Upvotes: 1

Views: 627

Answers (3)

Mario Klebsch
Mario Klebsch

Reputation: 360

After finding out, that I need a current version of xsltproc (1.1.29), I further changed the style sheet. Grouping worked only for children of TextContent, but I needed grouping to work on any nesting level.

Here is my input:

<Article>
    <TextContent>
        <p>lorem</p>
        <VisualContent id="1" />
        <VisualContent id="2" />
        <VisualContent id="3" />
        <p>ipsum</p>
        <VisualContent id="4" />
        <p>dolor</p>
        <p>sit</p>
        <VisualContent id="5" />
    </TextContent>
    <p>amet</p>
    <VisualContent id="6" />
    <VisualContent id="7" />
    <p>consectetur</p>
    <VisualContent id="8" />
</Article>

I use the following style sheet:

<?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"/>

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

    <xsl:key name="prevByName" match="*/*" use="generate-id(preceding-sibling::*[not(name()=name(current()))][1])" />

    <xsl:template match="VisualContent">
        <xsl:if test="generate-id()=generate-id(key('prevByName', generate-id(preceding-sibling::*[not(name()=name(current()))][1]))[1])">
            <xsl:variable name="myGroup" select="key('prevByName', generate-id(preceding-sibling::*[not(name()=name(current()))][1]))" />
            <xsl:choose>
                <xsl:when test="count($myGroup) > 1">
                    <ul>
                        <xsl:for-each select="$myGroup">
                            <li>
                                <xsl:copy-of select="."/>
                            </li>
                        </xsl:for-each>
                    </ul>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:copy-of select="."/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:if>
    </xsl:template>

</xsl:stylesheet>

Now, grouping works on any element level. :-)

<?xml version="1.0" encoding="utf-8"?>
<Article>
    <TextContent>
        <p>lorem</p>
        <ul><li><VisualContent id="1"/></li><li><VisualContent id="2"/></li><li><VisualContent id="3"/></li></ul>


        <p>ipsum</p>
        <VisualContent id="4"/>
        <p>dolor</p>
        <p>sit</p>
        <VisualContent id="5"/>
    </TextContent>
    <p>amet</p>
    <ul><li><VisualContent id="6"/></li><li><VisualContent id="7"/></li></ul>

    <p>consectetur</p>
    <VisualContent id="8"/>
</Article>

Upvotes: 0

michael.hor257k
michael.hor257k

Reputation: 116959

I have skipped the part where <TextContent> turns into <HtmlContent> and <VisualContent id="n" /> becomes <img data-id="n" /> because the question is difficult enough without these distractions.

The method I have chosen looks at the first preceding sibling whose name is not the same as the current element's name. The unique ID of that sibling is the key by which the adjacent elements of same name can be grouped:

<?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="prevByName" match="TextContent/*" use="generate-id(preceding-sibling::*[not(name()=name(current()))][1])" />

<xsl:template match="/">
<Article><TextContent>
    <xsl:for-each select="Article/TextContent/*[generate-id()=generate-id(key('prevByName', generate-id(preceding-sibling::*[not(name()=name(current()))][1]))[1])]">
        <xsl:variable name="myGroup" select="key('prevByName', generate-id(preceding-sibling::*[not(name()=name(current()))][1]))" />
        <xsl:choose>
            <xsl:when test="count($myGroup) > 1">
                <ul>
                    <xsl:for-each select="$myGroup">
                    <li><xsl:copy-of select="."/></li>
                    </xsl:for-each>
                </ul>
            </xsl:when>
            <xsl:otherwise>
                <xsl:copy-of select="."/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:for-each>
</TextContent></Article>
</xsl:template>
</xsl:stylesheet>

When applied to your input example, the following result is produced:

<?xml version="1.0" encoding="utf-8"?>
<Article>
   <TextContent>
      <p>lorem</p>
      <ul>
         <li>
            <VisualContent id="1"/>
         </li>
         <li>
            <VisualContent id="2"/>
         </li>
         <li>
            <VisualContent id="3"/>
         </li>
      </ul>
      <p>ipsum</p>
      <VisualContent id="4"/>
      <p>dolor</p>
      <VisualContent id="5"/>
   </TextContent>
</Article>

EDIT: Here's a modified version that only groups adjacent "VisualContent" elements:

<?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="prevByName" match="TextContent/*" use="generate-id(preceding-sibling::*[not(name()=name(current()))][1])" />

<xsl:template match="/Article/TextContent">
    <Article><TextContent>
        <xsl:apply-templates select="*"/>
    </TextContent></Article>
</xsl:template>

<xsl:template match="TextContent/*[not(self::VisualContent)]">
    <xsl:copy-of select="."/>
</xsl:template>

<xsl:template match="VisualContent[generate-id()=generate-id(key('prevByName', generate-id(preceding-sibling::*[not(name()=name(current()))][1]))[1])]">
<xsl:variable name="myGroup" select="key('prevByName', generate-id(preceding-sibling::*[not(name()=name(current()))][1]))" />
    <xsl:choose>
        <xsl:when test="count($myGroup) > 1">
            <ul>
                <xsl:for-each select="$myGroup">
                <li><xsl:copy-of select="."/></li>
                </xsl:for-each>
            </ul>
        </xsl:when>
        <xsl:otherwise>
            <xsl:copy-of select="."/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

</xsl:stylesheet>

This could probably use some streamlining, but I think the principle is clear and I need to get some sleep...

Upvotes: 3

spiderman
spiderman

Reputation: 11112

I am also new to XSLT.

This is not your expected output, I could not get the <ul>, but see if you can build on top of this. I will also try.

<xsl:stylesheet version="2.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes" encoding="UTF-8" />
    <xsl:template match="/">
        <Article>
            <HtmlContent>
                <xsl:apply-templates />
            </HtmlContent>
        </Article>
    </xsl:template>

    <xsl:template match="VisualContent">
        <li>
            <xsl:element name="img">
                <xsl:attribute name="date-id"><xsl:number/></xsl:attribute>
            </xsl:element>
        </li>
    </xsl:template>

    <xsl:template match="p">
        <xsl:element name="p">
            <xsl:apply-templates />
        </xsl:element>
    </xsl:template>
</xsl:stylesheet>

This is the output:

<Article>
    <HtmlContent>
        <p>lorem</p>
        <li><img date-id="1" /></li>
        <li><img date-id="2" /></li>
        <li><img date-id="3" /></li>
        <p>ipsum</p>
        <li><img date-id="4" /></li>
        <p>dolor</p>
        <li><img date-id="5" /></li>
    </HtmlContent>
</Article>

Upvotes: 0

Related Questions