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 withh1
. - And the second for the content of the current group (from the first
loop), for groups starting withh2
.
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 ingroup-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
Why Media Queries Has Less Priority Than No Media Queries Css
What Is the Purpose of the "Role" Attribute in Html
How to Have Two Fixed Width Columns With One Flexible Column in the Center
How to Pre-Populate the Sms Body Text Via an HTML Link
Allow 2 Decimal Places in ≪Input Type="Number"≫
What Is the Best Practice to Parse HTML in Swift
Regular Expression For Extracting Tag Attributes
Position: Absolute and Parent Height
Pure CSS Multi-Level Drop-Down Menu
Why Anchor Tag Does Not Take Height and Width of Its Containing Element
How to Make ≪Div≫ Fill ≪Td≫ Height
How to Allow <Input Type="File"> to Accept Only Image Files
How to Fill a Div with an Image While Keeping It Proportional
HTML - Display Image After Selecting Filename
How to Run an External Program, E.G. Notepad, Using Hyperlink