I wrote a pipeline of several steps (using several modes), the first transforms your input into the rowspan/colspan
based transformation format the solution in https://stackoverflow.com/a/36106927/252228, based on the already mentioned https://andrewjwelch.com/code/xslt/table/table-normalization.html, uses, then basically the templates from that solution are used, with the sole adaption of matching/selecting row
and entry
instead of tr
and th
/td
and ensuring that the final mode removes the helper rowspan attributes and fills in the <entry><morerows/></entry>
elements:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:key name="col" match="table/tgroup/colspec" use="@colname"/>
<xsl:mode name="html-row-col-span" on-no-match="shallow-copy"/>
<xsl:template mode="html-row-col-span" match="entry/@morerows">
<xsl:next-match/>
<xsl:attribute name="rowspan" select=". + 1"/>
</xsl:template>
<xsl:template mode="html-row-col-span" match="entry[@namest and @nameend]/@namest">
<xsl:next-match/>
<xsl:attribute name="colspan" select="key('col', ../@nameend, ancestor::tgroup)/@colnum - key('col', ., ancestor::tgroup)/@colnum + 1"/>
</xsl:template>
<xsl:mode name="colspan" on-no-match="shallow-copy"/>
<xsl:mode name="rowspan" on-no-match="shallow-copy"/>
<xsl:mode name="final-cleanup" on-no-match="shallow-copy"/>
<xsl:output method="xml" indent="yes" suppress-indentation="entry"/>
<xsl:template match="table/tgroup">
<xsl:variable name="row-and-colspan">
<xsl:apply-templates select="." mode="html-row-col-span"/>
</xsl:variable>
<xsl:variable name="tgroup_with_no_colspans">
<xsl:apply-templates select="$row-and-colspan" mode="colspan" />
</xsl:variable>
<xsl:variable name="tgroup_with_normalized_rowspans">
<xsl:apply-templates select="$tgroup_with_no_colspans" mode="rowspan" />
</xsl:variable>
<xsl:apply-templates select="$tgroup_with_normalized_rowspans" mode="final-cleanup"/>
</xsl:template>
<xsl:template mode="colspan" match="entry[@colspan]">
<xsl:next-match/>
<xsl:for-each select="2 to @colspan">
<entry><col-span-filler/></entry>
</xsl:for-each>
</xsl:template>
<xsl:template mode="colspan" match="@colspan"/>
<xsl:template match="thead | tbody" mode="rowspan">
<xsl:copy>
<xsl:copy-of select="row[1]"/>
<xsl:apply-templates select="row[2]" mode="rowspan">
<xsl:with-param name="previousRow" select="row[1]"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="row" mode="rowspan">
<xsl:param name="previousRow" as="element()" />
<xsl:variable name="currentRow" select="." />
<xsl:variable name="normalizedEntries">
<xsl:for-each select="$previousRow/*">
<xsl:choose>
<xsl:when test="@rowspan > 1">
<xsl:copy>
<xsl:attribute name="rowspan">
<xsl:value-of select="@rowspan - 1" />
</xsl:attribute>
</xsl:copy>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="$currentRow/entry[1 + count(current()/preceding-sibling::*[not(@rowspan) or (@rowspan = 1)])]" />
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="newRow" as="element(row)">
<xsl:copy>
<xsl:copy-of select="$currentRow/@*" />
<xsl:copy-of select="$normalizedEntries" />
</xsl:copy>
</xsl:variable>
<xsl:copy-of select="$newRow" />
<xsl:apply-templates select="following-sibling::row[1]" mode="rowspan">
<xsl:with-param name="previousRow" select="$newRow" />
</xsl:apply-templates>
</xsl:template>
<xsl:template mode="final-cleanup" match="entry[col-span-filler] | entry[@morerows]/@rowspan"/>
<xsl:template mode="final-cleanup" match="entry[@rowspan and not(@morerows)]">
<xsl:copy>
<morerows/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
The code uses XSLT 3 with e.g. xsl:mode
and but you could also write it as XSLT 2, like the solution of Andrew Welch and my adaption of a previous StackOverflow post. The sibling recursion used could probably also reimplemented more efficiently in XSLT 3 using xsl:iterate
.
Here is a pure XSLT 3 solution also working with streaming if SaxonCS or Saxon EE is used that uses an accumulator instead of a key and two nested xsl:iterate
:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
exclude-result-prefixes="#all"
expand-text="yes">
<xsl:output method="xml" indent="yes" suppress-indentation="entry"/>
<xsl:accumulator name="colnums" as="map(xs:string, xs:integer)" initial-value="map{}" streamable="yes">
<xsl:accumulator-rule
match="tgroup" select="map{}"/>
<xsl:accumulator-rule
match="colspec"
select="map:put($value, xs:string(@colname), xs:integer(@colnum))"/>
</xsl:accumulator>
<xsl:mode name="fill-rowspan-colspan" on-no-match="shallow-copy" use-accumulators="colnums" streamable="yes"/>
<xsl:template mode="fill-rowspan-colspan" match="entry">
<xsl:next-match/>
<xsl:iterate select=".[@namest] ! (1 to (accumulator-before('colnums')(@nameend)!xs:integer(.) - accumulator-before('colnums')(@namest)!xs:integer(.)))">
<entry>
<col-span-filler/>
</entry>
</xsl:iterate>
</xsl:template>
<xsl:template mode="fill-rowspan-colspan" match="entry/@morerows">
<xsl:next-match/>
<xsl:attribute name="rowspan" select=". + 1"/>
</xsl:template>
<xsl:mode on-no-match="shallow-copy" use-accumulators="colnums" streamable="yes"/>
<xsl:template match="thead | tbody">
<xsl:copy>
<xsl:apply-templates select="@*"/>
<xsl:iterate select="row">
<xsl:param name="prev-row" as="element(row)?" select="()"/>
<xsl:variable name="new-row" as="element(row)">
<xsl:copy>
<xsl:apply-templates select="@*"/>
<xsl:apply-templates select="entry" mode="fill-rowspan-colspan"/>
</xsl:copy>
</xsl:variable>
<xsl:variable name="new-row" as="element(row)">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:choose>
<xsl:when test="not($prev-row)">
<xsl:copy-of select="$new-row/*"/>
</xsl:when>
<xsl:otherwise>
<xsl:iterate select="$prev-row!entry">
<xsl:choose>
<xsl:when test="@rowspan > 1">
<xsl:copy>
<xsl:attribute name="rowspan" select="@rowspan - 1"/>
</xsl:copy>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="$new-row/entry[1 + count(current()/preceding-sibling::*[not(@rowspan) or @rowspan = 1])]"/>
</xsl:otherwise>
</xsl:choose>
</xsl:iterate>
</xsl:otherwise>
</xsl:choose>
</xsl:copy>
</xsl:variable>
<xsl:apply-templates select="$new-row" mode="clean-up"/>
<xsl:next-iteration>
<xsl:with-param name="prev-row" select="$new-row"/>
</xsl:next-iteration>
</xsl:iterate>
</xsl:copy>
</xsl:template>
<xsl:mode name="clean-up" on-no-match="shallow-copy"/>
<xsl:template mode="clean-up" match="entry[@rowspan = 1]">
<xsl:copy>
<morerows/>
</xsl:copy>
</xsl:template>
<xsl:template mode="clean-up" match="entry[col-span-filler] | entry/@rowspan"/>
<xsl:template match="/" name="xsl:initial-template">
<xsl:next-match/>
<xsl:comment>Run with {system-property('xsl:product-name')} {system-property('xsl:product-version')} {system-property('Q{http://saxon.sf.net/}platform')}</xsl:comment>
</xsl:template>
</xsl:stylesheet>
Note: both presented stylesheets assumes the colspec sections from your first edit of the question are present in the input sample as otherwise it is not possible to compute the colspan form an entry
with @namest
and @nameend
attributes.