Xslt: Moving a Grouping HTML Elements into Section Levels

XSLT: moving a grouping html elements into section levels

Here is an XSLT 2.0 stylesheet:

<xsl:stylesheet 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="xs mf"
version="2.0">

<xsl:output indent="yes"/>

<xsl:function name="mf:group" as="node()*">
<xsl:param name="elements" as="element()*"/>
<xsl:param name="level" as="xs:integer"/>
<xsl:for-each-group select="$elements" group-starting-with="*[local-name() eq concat('h', $level)]">
<xsl:choose>
<xsl:when test="self::*[local-name() eq concat('h', $level)]">
<section level="{$level}">
<xsl:element name="header{$level}"><xsl:apply-templates/></xsl:element>
<xsl:sequence select="mf:group(current-group() except ., $level + 1)"/>
</section>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:function>

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

<xsl:template match="/html">
<document>
<xsl:apply-templates select="body"/>
</document>
</xsl:template>

<xsl:template match="body">
<xsl:sequence select="mf:group(*, 1)"/>
</xsl:template>

</xsl:stylesheet>

It should do what you asked for, although it does not stop at four nested levels but rather groups as long as it finds h[n] elements.

Sectioning different heading levels

I would simply let the function group by the current level and stop at the maximum level (which is 6 in HTML):

<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.org/mf"
exclude-result-prefixes="xs mf">

<xsl:function name="mf:group" as="node()*">
<xsl:param name="nodes" as="node()*"/>
<xsl:param name="level" as="xs:integer"/>
<xsl:for-each-group select="$nodes" group-starting-with="*[starts-with(local-name(), concat('h', $level))]">
<xsl:choose>
<xsl:when test="self::*[starts-with(local-name(), concat('h', $level))]">
<section level="{$level}">
<xsl:apply-templates select="."/>
<xsl:sequence select="mf:group(current-group() except ., $level + 1)"/>
</section>
</xsl:when>
<xsl:when test="$level lt 6">
<xsl:sequence select="mf:group(current-group(), $level + 1)"/>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:function>

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

<xsl:template match="body">
<xsl:copy>
<xsl:sequence select="mf:group(node(), 1)"/>
</xsl:copy>
</xsl:template>

</xsl:stylesheet>

Obviously the level to search for could be provided as a parameter instead of hardcoding it in the stylesheet:

<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.org/mf"
exclude-result-prefixes="xs mf">

<xsl:param name="max-level" as="xs:integer" select="6"/>

<xsl:param name="name-prefix" as="xs:string" select="'h'"/>

<xsl:output method="html" indent="yes"/>

<xsl:function name="mf:group" as="node()*">
<xsl:param name="nodes" as="node()*"/>
<xsl:param name="level" as="xs:integer"/>
<xsl:for-each-group select="$nodes" group-starting-with="*[starts-with(local-name(), concat($name-prefix, $level))]">
<xsl:choose>
<xsl:when test="self::*[starts-with(local-name(), concat($name-prefix, $level))]">
<section level="{$level}">
<xsl:apply-templates select="."/>
<xsl:sequence select="mf:group(current-group() except ., $level + 1)"/>
</section>
</xsl:when>
<xsl:when test="$level lt $max-level">
<xsl:sequence select="mf:group(current-group(), $level + 1)"/>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:function>

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

<xsl:template match="body">
<xsl:copy>
<xsl:sequence select="mf:group(*, 1)"/>
</xsl:copy>
</xsl:template>

</xsl:stylesheet>

Grouping 3 elements into a node

Here's a simple method, based on the assumption that the 3 elements to be grouped will always come in a contiguous block that starts with a SequenceX:

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:template match="/Row">
<xsl:copy>
<xsl:copy-of select="Coverage | Admission"/>
<Benefits>
<xsl:for-each select="*[starts-with(name(), 'Sequence')]">
<Benefit>
<xsl:for-each select=". | following-sibling::*[position() < 3]">
<xsl:element name="{translate(name(), '0123456789', '')}">
<xsl:value-of select="." />
</xsl:element>
</xsl:for-each>
</Benefit>
</xsl:for-each>
</Benefits>
</xsl:copy>
</xsl:template>

</xsl:stylesheet>

If the above assumption is not true, you can perform actual grouping based on the number included in the element names:

XSLT 2.0

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

<xsl:template match="/Row">
<xsl:copy>
<xsl:copy-of select="Coverage | Admission"/>
<Benefits>
<xsl:for-each-group select="*[matches(name(), '^Sequence|^Qualifier|^Date')]" group-by="replace(name(), '\D', '')">
<Benefit>
<xsl:for-each select="current-group()">
<xsl:element name="{replace(name(), '\d', '')}">
<xsl:value-of select="." />
</xsl:element>
</xsl:for-each>
</Benefit>
</xsl:for-each-group>
</Benefits>
</xsl:copy>
</xsl:template>

</xsl:stylesheet>

XSLT: moving following sibilings of heading tags into separate topics

Here is my suggestion using XSLT 2.0 and for-each-group group-starting-with in a recursive function:

<xsl:stylesheet
version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="xs mf">

<xsl:output indent="yes"/>

<xsl:function name="mf:get-id-sub" as="xs:string">
<xsl:param name="level" as="xs:integer"/>
<xsl:sequence select="string-join(for $i in 3 to $level return 'sub-', '')"/>
</xsl:function>

<xsl:function name="mf:group" as="element()*">
<xsl:param name="elements" as="element()*"/>
<xsl:param name="level" as="xs:integer"/>
<xsl:for-each-group select="$elements" group-starting-with="*[local-name() eq concat('h', $level)]">
<xsl:choose>
<xsl:when test="not(self::*[local-name() eq concat('h', $level)])">
<body>
<xsl:apply-templates select="current-group()"/>
</body>
</xsl:when>
<xsl:otherwise>
<topic id="{mf:get-id-sub($level)}headingtitle">
<xsl:apply-templates select="."/>
<xsl:sequence select="mf:group(current-group() except ., $level + 1)"/>
</topic>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:function>

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

<xsl:template match="topic[@id = 'pageTitle']/body">
<xsl:sequence select="mf:group(*, 2)"/>
</xsl:template>

<xsl:template match="h2 | h3 | h4 | h5 | h6">
<title>
<xsl:apply-templates/>
</title>
</xsl:template>

</xsl:stylesheet>

It transforms

<topic id="pageTitle">
<title>Page Title</title>
<body>
<h2>heading title</h2>
<p>some more content under h heading</p>
</body>
</topic>

into

<topic id="pageTitle">
<title>Page Title</title>
<topic id="headingtitle">
<title>heading title</title>
<body>
<p>some more content under h heading</p>
</body>
</topic>
</topic>

and

<topic id="pageTitle">
<title>Page Title</title>
<body>
<p>some contents.</p>
<h2>heading title</h2>
<p>some more content under h heading</p>

<h3>sub-heading title</h3>
<p>some more content under sub heading</p>
<p>some more content under sub heading</p>

<h2>heading title</h2>
<p>some more content under h heading</p>
</body>
</topic>

into

<topic id="pageTitle">
<title>Page Title</title>
<body>
<p>some contents.</p>
</body>
<topic id="headingtitle">
<title>heading title</title>
<body>
<p>some more content under h heading</p>
</body>
<topic id="sub-headingtitle">
<title>sub-heading title</title>
<body>
<p>some more content under sub heading</p>
<p>some more content under sub heading</p>
</body>
</topic>
</topic>
<topic id="headingtitle">
<title>heading title</title>
<body>
<p>some more content under h heading</p>
</body>
</topic>
</topic>

Multilevel grouping with summing into HTML table

You don't need Muenchian grouping here - just a key. However, the key needs a "composite" value as the use parameter. try it this way;

<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="val" match="KPI" use="concat(../../@AuditGroup_NM, '|', @kpi_NM)" />

<xsl:template match="/Person">
<html>
<body>
<h2><xsl:value-of select="@Person_NM"/></h2>
<h3><xsl:value-of select="@Title"/></h3>
<table border="1">
<xsl:apply-templates select="AuditGroup"/>
</table>
</body>
</html>
</xsl:template>

<xsl:template match="AuditGroup">
<xsl:variable name="col" select="Audit[1]/KPI" />
<tr>
<th colspan="{count($col) + 1}"><xsl:value-of select="@AuditGroup_NM"/></th>
</tr>
<tr>
<td></td>
<xsl:for-each select="$col">
<th><xsl:value-of select="@kpi_DESC"/></th>
</xsl:for-each>
</tr>
<xsl:apply-templates select="Audit"/>
<tr>
<td align="right"><i><b>Totals</b></i></td>
<xsl:for-each select="$col">
<td align="right"><xsl:value-of select="format-number(sum(key('val', concat(../../@AuditGroup_NM, '|', @kpi_NM))/KPIValue/@strValue), '$#,###')"/></td>
</xsl:for-each>
</tr>
</xsl:template>

<xsl:template match="Audit">
<tr>
<td><xsl:value-of select="@audit_NM"/></td>
<xsl:for-each select="KPI">
<td align="right"><xsl:value-of select="format-number(KPIValue/@strValue, '$#,###')"/></td>
</xsl:for-each>
</tr>
</xsl:template>

</xsl:stylesheet>

Grouping elements not appearing as per required structure

There was a recent, similar question Not able to do XSLT transformation with grouping, to which I posted the answer https://stackoverflow.com/a/70662849/252228, to make it work against your input which has p elements mixed with table elements you would need to type the function parameter as element()* instead of element(p)*:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
exclude-result-prefixes="#all"
xmlns:mf="http://example.com/mf">

<xsl:param name="xml-levels-param">
<levels>
<level key="TRH2">
<container>chapter</container>
<content>title</content>
</level>
<level key="TRH4">
<container>division</container>
<content>title</content>
</level>
<level key="TRH5">
<container>subdivision</container>
<content>title</content>
</level>
<level key="TRH6">
<container>arabicSubdivision</container>
<content>title</content>
</level>
<level key="TRH7">
<container>section</container>
<content>title</content>
</level>
</levels>
</xsl:param>

<xsl:variable name="xml-levels" select="$xml-levels-param/levels/level"/>

<xsl:function name="mf:group-xml" as="node()*">
<xsl:param name="elements" as="element()*"/>
<xsl:param name="level" as="xs:integer"/>
<xsl:for-each-group select="$elements" group-starting-with="p[@style = $xml-levels[$level]/@key]">
<xsl:choose>
<xsl:when test="self::p[@style = $xml-levels[$level]/@key]">
<xsl:element name="{$xml-levels[$level]/container}">
<xsl:element name="{$xml-levels[$level]/content}">
<xsl:value-of select="r"/>
</xsl:element>
<xsl:choose>
<xsl:when test="exists($xml-levels[$level + 1])">
<xsl:sequence select="mf:group-xml(current-group()[position() gt 1], $level + 1)"/>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()[position() gt 1]"/>
</xsl:otherwise>
</xsl:choose>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:choose>
<xsl:when test="exists($xml-levels[$level + 1])">
<xsl:sequence select="mf:group-xml(current-group(), $level + 1)"/>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:function>

<xsl:template match="text">
<xsl:sequence select="mf:group-xml(*, 1)"/>
</xsl:template>

<xsl:template match="p[@style = 'TRNormal']">
<paragraphp>
<xsl:apply-templates select="r/node()"/>
</paragraphp>
</xsl:template>

<xsl:template match="p[not(node())]"/>

<xsl:output method="xml" indent="yes"/>

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

</xsl:stylesheet>

xslt wrap multiple sibling-nodes from one specific node up to next occurance of specific node

The method you can use is two embedded for-each-group commands:

  • One for all elements in root, for groups starting with h1.
  • And the second for the content of the current group (from the first
    loop), for groups starting with h2.

One important feature of for-each-group coupled with group-starting-with
is that when the source content does not start with the tag specified in
group-starting-with, then the "starting part" (without the specified starting
tag) is the first group.

To distinguish between this "initial" group (starting actually with h1)
and groups starting with h2, I used xsl:choose.

So the whole script can look like below:

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

<xsl:template match="root">
<xsl:copy>
<xsl:for-each-group select="*" group-starting-with="h1">
<chapter>
<xsl:for-each-group select="current-group()" group-starting-with="h2">
<xsl:choose>
<xsl:when test="name()='h1'">
<xsl:copy-of select="current-group()"/>
</xsl:when>
<xsl:otherwise>
<section>
<xsl:copy-of select="current-group()"/>
</section>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</chapter>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
</xsl:transform>

For a working example see http://xsltransform.net/pNvs5w8



Related Topics



Leave a reply



Submit