XHTML tables

These examples only show a fraction of all possible variants to prove XHTML tables. The XHTML table model provides many opportunities to make unstable tables. In most cases, the checking for solid XHTML tables is a very complex task. The definition of QuickFixes for these problems is even more complex and therefore, not all necessary fixes can be provided by Schematron QuickFixes.

Download of the files

Download all files mentioned on this page or the files for each individual example:

Column width

This example shows the problem if the sum of the colum widths totals in more than 100%. The example ignores the fact that widths can be set with different units in the width attribute. So, the example is relatively simple.

XML instance

The instance contains three invalid tables. The first has a total column width of 198%. Both the second and the third table have a total column width of 106%.

9 <table>
10 <col width="33%"/>
11 <col width="33%"/>
12 <col width="66%"/>
13 <col width="66%"/>
   [...]
26 <table>
27 <col width="10%"/>
28 <col width="10%"/>
29 <col width="20%"/>
30 <col width="66%"/>
   [...]
43 <table>
44 <col width="10%"/>
45 <col width="10%"/>
46 <col width="66%"/>
47 <col width="20%"/>
   [...]
59 </table>

Schematron schema

The schema reports all tables having a total column width of more than 100%:

1 <?xml version="1.0" encoding="UTF-8"?>
2 <schema xmlns="http://purl.oclc.org/dsdl/schematron" xmlns:es="http://www.escali.schematron-quickfix.com/" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" queryBinding="xslt2">
3 <es:default-namespace uri="http://www.w3.org/1999/xhtml"/>
4 <pattern>
5 <rule context="table">
6 <let name="colWidthSummarized" value="number(sum(
7 for $colWidth in col/@width
8 return number(substring-before($colWidth,'%'))
9 ))"/>
10 <let name="lastWidth" value="number(substring-before(col[last()]/@width,'%'))"/>
11 <assert test="$colWidthSummarized &lt;= 100">The sum of the column widths (<value-of select="$colWidthSummarized"/>%) is greater than 100%.</assert>
12 </rule>
13 </pattern>
14 </schema>

QuickFixes

11 <assert test="$colWidthSummarized &lt;= 100" sqf:fix="last proportional setOneWidth">The sum of the column widths (<value-of select="$colWidthSummarized"/>%) is greater than 100%.</assert>
12 <sqf:fix id="last" use-when="$colWidthSummarized - 100 lt $lastWidth" role="replace">
13 <sqf:description>
14 <sqf:title>Subtract the excessive width from the last &lt;col&gt; element.</sqf:title>
15 </sqf:description>
16 <let name="delta" value="$colWidthSummarized - 100"/>
17 <sqf:add match="col[last()]" target="width" node-type="attribute">
18 <let name="newWidth" value="number(substring-before(@width,'%')) - $delta"/>
19 <value-of select="concat($newWidth,'%')"/>
20 </sqf:add>
21 </sqf:fix>
22 <sqf:fix id="setOneWidth" role="replace">
23 <sqf:description>
24 <sqf:title>Auto-correct the width of one column.</sqf:title>
25 </sqf:description>
26 <sqf:user-entry name="colNumber" type="xs:integer">
27 <sqf:description>
28 <sqf:title>Enter the column number.</sqf:title>
29 </sqf:description>
30 </sqf:user-entry>
31 <let name="delta" value="$colWidthSummarized - 100"/>
32 <sqf:add match="col" target="width" node-type="attribute">
33 <let name="pos" value="count(preceding-sibling::col) + 1"/>
34 <xsl:choose>
35 <xsl:when test="$pos = xs:integer($colNumber)">
36 <let name="newWidth" value="number(substring-before(@width,'%')) - $delta"/>
37 <value-of select="concat($newWidth,'%')"/>
38 </xsl:when>
39 <xsl:otherwise>
40 <value-of select="@width"/>
41 </xsl:otherwise>
42 </xsl:choose>
43 </sqf:add>
44 </sqf:fix>
45 <sqf:fix id="proportional" role="replace">
46 <sqf:description>
47 <sqf:title>Subtract the excessive width from each &lt;col&gt; element proportionally.</sqf:title>
48 </sqf:description>
49 <let name="delta" value="$colWidthSummarized - 100"/>
50 <sqf:add match="col" target="width" node-type="attribute">
51 <let name="width" value="number(substring-before(@width,'%'))"/>
52 <let name="newWidth" value="$width - ($delta * ($width div $colWidthSummarized))"/>
53 <value-of select="concat($newWidth,'%')"/>
54 </sqf:add>
55 </sqf:fix>
56 </rule>

The schema provides three QuickFixes:

  1. With the first QuickFix, the overflow from the last column is subtracted.

  2. With the second QuickFix, the user can choose from which column the overflow shall be subtracted. The user has to enter the index of the column.

  3. With the third QuickFix, the overflow from each column of the table is subtracted proportionally.

See also

The Schematron QuickFix schema uses the following SQF elements:

Please read the reference in order to learn the functionality of these SQF elements.

Cellspan

This is the most complicated example of XHTML tables.

XML instance

The instance contains misplaced colspan and rowspan attributes. There are also rows consisting of too many or not enough cells:

1 <?xml version="1.0" encoding="UTF-8"?>
2 <html xmlns="http://www.w3.org/1999/xhtml">
3 <head>
4 <title>tables</title>
5 </head>
6 <body>
7 <p>invalid tables:</p>
8 <table border="1">
9 <tr>
10 <td>A1</td>
11 <td>B1</td>
12 <td>C1</td>
13 <td>D1</td>
14 <td colspan="2">E1</td>
15 </tr>
16 <tr>
17 <td>A2</td>
18 <td rowspan="3">B2</td>
19 <td>C2</td>
20 <td>D2</td>
21 <td>E2</td>
22 <td rowspan="5">F2</td>
23 </tr>
24 <tr>
25 <td colspan="2">A3</td>
26 <td>C3</td>
27 <td>D3</td>
28 <td colspan="2">E3</td>
29 </tr>
30 <tr>
31 <td>A4</td>
32 <td>C4</td>
33 <td>D4</td>
34 <td>E4</td>
35 </tr>
36 <tr>
37 <td>A5</td>
38 <td>B5</td>
39 <td>C5</td>
40 <td>D5</td>
41 <td rowspan="5">E5</td>
42 </tr>
43 <tr>
44 <td>A6</td>
45 <td>B6</td>
46 <td>C6</td>
47 <td colspan="2">D6</td>
48 </tr>
49 <tr>
50 <td rowspan="2" colspan="3">A7</td>
51 <td>D7</td>
52 </tr>
53 <tr>
54 <td>D8</td>
55 </tr>
56 <tr>
57 <td>A9</td>
58 <td>B9</td>
59 <td>C9</td>
60 <td>D9</td>
61 <td>F9</td>
62 </tr>
63 <tr>
64 <td>A</td>
65 </tr>
66 </table>
   [...]
235 </body>
236 </html>

Schematron schema

The schema uses XSLT functions in order to calculate the relations of the cells. So, the misplaced spans and the wrong cell count in the rows can be defined.

1 <?xml version="1.0" encoding="UTF-8"?>
2 <schema xmlns="http://purl.oclc.org/dsdl/schematron" xmlns:htm="http://www.html.de" xmlns:es="http://www.escali.schematron-quickfix.com/" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:html="http://www.data2type.de/html" queryBinding="xslt2">
3 <es:default-namespace uri="http://www.w3.org/1999/xhtml"/>
4 <xsl:function name="html:getColAndRows">
5 <xsl:param name="preCells"/>
6 <xsl:param name="cells"/>
7 <xsl:variable name="cellsWithRows">
8 <xsl:choose>
9 <xsl:when test="$cells[not(@html:rowNumber)]|$cells[not(@colspan|@rowspan)]">
10 <xsl:for-each select="$cells">
11 <xsl:copy>
12 <xsl:attribute name="html:rowNumber" select="count(../preceding-sibling::tr
13 | ../parent::*[not(self::table)]/preceding-sibling::*/tr)+1"/>
14 <xsl:attribute name="colspan" select="'1'"/>
15 <xsl:attribute name="rowspan" select="'1'"/>
16 <xsl:attribute name="html:tableID" select="generate-id(./ancestor::table[1])"/>
17 <xsl:attribute name="html:cellID" select="generate-id()"/>
18 <xsl:copy-of select="@*"/>
19 <xsl:apply-templates/>
20 </xsl:copy>
21 </xsl:for-each>
22 </xsl:when>
23 <xsl:otherwise>
24 <xsl:copy-of select="$cells"/>
25 </xsl:otherwise>
26 </xsl:choose>
27 </xsl:variable>
28 <xsl:variable name="cells" select="$cellsWithRows//td"/>
29 <xsl:choose>
30 <xsl:when test="count($cells)=0">
31 <xsl:copy-of select="$preCells"/>
32 <xsl:copy-of select="$cells"/>
33 </xsl:when>
34 <xsl:when test="count($cells)=1 and count($preCells)=0">
35 <td>
36 <xsl:attribute name="html:colNumber">1</xsl:attribute>
37 <xsl:copy-of select="$cells/@*"/>
38 <xsl:copy-of select="$cells/node()"/>
39 </td>
40 </xsl:when>
41 <xsl:otherwise>
42 <xsl:variable name="half" select="count($cells) div 2"/>
43 <xsl:variable name="firstHalf" select="html:getColAndRows($preCells,$cells[position()&lt;$half])"/>
44 <xsl:variable name="secondHalf" select="html:getColAndRows(($firstHalf),
45 $cells[position()&gt;=$half][not(position()=last())])"/>
46 <xsl:variable name="cell" select="$cells[last()]"/>
47 <xsl:variable name="preCells" select="($secondHalf)"/>
48 <xsl:variable name="preCell" select="$preCells[last()]"/>
49 <xsl:variable name="colOhneRowspan" select="if (number($preCell/@html:rowNumber) &lt; number($cell/@html:rowNumber))
50 then (1)
51 else ($preCell/@html:colNumber + $preCell/@colspan)"/>
52 <xsl:variable name="colMitRowspan" select="html:getCol($colOhneRowspan,$cell/@html:rowNumber,$preCells[@rowspan &gt; 1])"/>
53 <xsl:copy-of select="$preCells"/>
54 <td>
55 <xsl:attribute name="html:colNumber" select="$colMitRowspan"/>
56 <xsl:copy-of select="$cell/@*"/>
57 <xsl:copy-of select="$cell/node()"/>
58 </td>
59 </xsl:otherwise>
60 </xsl:choose>
61 </xsl:function>
62 <xsl:function name="html:getColAndRows">
63 <xsl:param name="cells"/>
64 <xsl:copy-of select="html:getColAndRows((),$cells)"/>
65 </xsl:function>
66 <xsl:function name="html:getCol">
67 <xsl:param name="curCol"/>
68 <xsl:param name="curRow"/>
69 <xsl:param name="preCells"/>
70 <xsl:choose>
71 <xsl:when test="$preCells[@html:colNumber &lt;= $curCol and $curCol &lt; @html:colNumber + @colspan]
72 [@html:rowNumber + @rowspan &gt; $curRow]">
73 <xsl:value-of select="html:getCol($curCol + 1,$curRow,$preCells)"/>
74 </xsl:when>
75 <xsl:otherwise>
76 <xsl:value-of select="$curCol"/>
77 </xsl:otherwise>
78 </xsl:choose>
79 </xsl:function>
80 <let name="cells" value="for $table in //table return html:getColAndRows($table//td)"/>
81 <pattern>
82 <rule context="td[@colspan &gt; 1]">
83 <let name="tableID" value="generate-id(./ancestor::table[1])"/>
84 <let name="cellID" value="generate-id()"/>
85 <let name="cells" value="$cells[@html:tableID=$tableID]"/>
86 <let name="rowNumber" value="count(../preceding-sibling::tr |
87 ../parent::*[not(self::table)]/preceding-sibling::*/tr)+1"/>
88 <let name="cell" value="$cells[@html:cellID=$cellID]"/>
89 <let name="row" value="$cells[@html:rowNumber + @rowspan &gt; $rowNumber]
90 [@html:rowNumber &lt;= $rowNumber]"/>
91 <let name="cols" value="xs:integer($cell/@html:colNumber) to xs:integer($cell/@html:colNumber + $cell/@colspan - 1)"/>
92 <let name="cross" value="$row[@html:colNumber = $cols]
93 [not(@html:cellID=$cellID)]"/>
94 <let name="crossTd" value="//td[generate-id()=$cross/@html:cellID]"/>
95 <assert test="count($cross) = 0">This cell overlaps the row-spanning cell(s) of the column(s) <value-of select="string-join($cross/@html:colNumber,', ')"/>.</assert>
96 </rule>
97 <rule context="tr">
98 <let name="tableID" value="generate-id(./ancestor::table[1])"/>
99 <let name="cells" value="$cells[@html:tableID=$tableID]"/>
100 <let name="maxCol" value="xs:integer(max(for $cell in $cells return ($cell/@html:colNumber + $cell/@colspan - 1)))"/>
101 <let name="rowNumber" value="count(preceding-sibling::tr | parent::*[not(self::table)]/preceding-sibling::*/tr)+1"/>
102 <let name="row" value="for $i
103 in (1 to $maxCol)
104 return ($cells[@html:colNumber &lt;= $i and $i &lt; @html:colNumber + @colspan]
105 [@html:rowNumber + @rowspan &gt; $rowNumber][@html:rowNumber &lt;= $rowNumber])"/>
106 <let name="forcedColCount" value="if (ancestor::table[1]//col)
107 then (count(ancestor::table[1]//col))
108 else ($maxCol)"/>
109 <let name="rowMaxCol" value="max(for $cell in $row return number($cell/@html:colNumber + $cell/@colspan - 1))"/>
110 <assert test="count($row) &gt;= $forcedColCount">Missing cells. (Counting of missing cells: <value-of select="$forcedColCount - count($row)"/> of <value-of select="$forcedColCount"/>)</assert>
111 <report test="$rowMaxCol &gt; $forcedColCount">Too many cells in this row. (Counting of excessive cells: <value-of select="count($row) - $forcedColCount"/>)</report>
112 </rule>
113 </pattern>
114 </schema>

QuickFixes

95 <assert test="count($cross) = 0" sqf:fix="deleteColspan deleteRowspan">This cell overlaps the row-spanning cell(s) of the column(s) <value-of select="string-join($cross/@html:colNumber,', ')"/>.</assert>
96 <sqf:fix id="deleteColspan">
97 <sqf:description>
98 <sqf:title>Delete the "colspan" attribute from this cell.</sqf:title>
99 </sqf:description>
100 <sqf:delete match="@colspan"/>
101 </sqf:fix>
102 <sqf:fix id="deleteRowspan">
103 <sqf:description>
104 <sqf:title>Delete the "rowspan" attribte from the overlapping cell.</sqf:title>
105 </sqf:description>
106 <sqf:delete match="$crossTd/@rowspan"/>
107 </sqf:fix>
   [...]
122 <assert test="count($row) &gt;= $forcedColCount" sqf:fix="addLost">Cells are missing. (Counting of missing cells: <value-of select="$forcedColCount - count($row)"/> of <value-of select="$forcedColCount"/>)</assert>
123 <report test="$rowMaxCol &gt; $forcedColCount" sqf:fix="deleteÜberschuss">Too many cells in this row. (Counting of excessive cells: <value-of select="count($row) - $forcedColCount"/>)</report>
124 <sqf:fix id="deleteÜberschuss">
125 <sqf:description>
126 <sqf:title>Delete the excessive cells.</sqf:title>
127 </sqf:description>
128 <let name="delete" value="$row[@html:colNumber &gt; $forcedColCount]"/>
129 <sqf:delete match="//td[generate-id()=$delete/@html:cellID]"/>
130 </sqf:fix>
131 <sqf:fix id="addLost">
132 <sqf:description>
133 <sqf:title>Add enough empty cells at the end of the row.</sqf:title>
134 </sqf:description>
135 <sqf:add match="td[last()]" position="after">
136 <xsl:for-each select="1 to xs:integer($forcedColCount - count($row))">
137 <td xmlns="http://www.w3.org/1999/xhtml"/>
138 </xsl:for-each>
139 </sqf:add>
140 </sqf:fix>

The provided QuickFixes are rudimental. The user can delete one of the misplaced spans (colspan or rowspan).

In case there are too many cells in a row, the schema provides the possibility to delete only the excessive cells. If there are not enough cells, the only fix is to add new (empty) cells.

This example demonstrates the power range of Schematron QuickFix. Note: Not for each Schematron error a QuickFix can be defined.

See also

The Schematron QuickFix schema uses the following SQF elements:

Please read the reference in order to learn the functionality of these SQF elements.

© Copyright 2014-2017 Nico Kutscherauer

ImprintContactSitemap