A 2x3 table whose entire first row is one horizontally merged cell (gridSpan=3).
Library verdicts: python-docx: — docxjs: pass
| Feature id | docx/table-merged-cells |
|---|---|
| Format | docx |
| Category | tables |
| Spec | ecma-376-5-part-1 § 17.4.16 w:gridSpan |
| ID / part | Predicate | XPath | python-docx | docxjs |
|---|---|---|---|---|
tbl-gridspan-3-presentword/document.xml | existAt least one cell must declare a horizontal merge of 3 via <w:tcPr><w:gridSpan w:val="3"/></w:tcPr>. | //w:tc/w:tcPr/w:gridSpan[@w:val='3'] | — | — |
tbl-merged-textword/document.xml | existA cell containing the literal merged-header text must be present in the table. | //w:tbl//w:tc//w:t[contains(normalize-space(), 'Merged header across three columns')] | — | — |
| ID | Predicate | Selector | python-docx | docxjs |
|---|---|---|---|---|
table-merged-cell-rendered | css_selector/ match-text = Merged headerA rendered <td>/<th> with colspan='3' under .docx-wrapper must contain 'Merged header'. | .docx-wrapper table td[colspan='3'], .docx-wrapper table th[colspan='3'] | — | pass |
table-merged-cell-colspan-exists | css_selector/ equal-countExactly one cell with colspan='3' must be present (the merged header row). Catches renderers that drop or duplicate the horizontal merge. | .docx-wrapper table td[colspan='3'], .docx-wrapper table th[colspan='3'] | — | — |
table-row-count | css_selector/ equal-countThe rendered table has 2 rows (one merged header, one data row with three cells). | .docx-wrapper table tr | — | — |
table-merged-row1-cell1-rendered | css_selector/ match-text = ^Cell 1$Data row, column 1 must be exactly 'Cell 1'. Catches the merged header's text leaking into the data row. | .docx-wrapper table tr:nth-of-type(2) td:nth-of-type(1), .docx-wrapper table tr:nth-of-type(2) th:nth-of-type(1) | — | — |
table-merged-row1-cell3-rendered | css_selector/ match-text = ^Cell 3$Data row, column 3 must be exactly 'Cell 3'. Catches a merged row absorbing the data row below it. | .docx-wrapper table tr:nth-of-type(2) td:nth-of-type(3), .docx-wrapper table tr:nth-of-type(2) th:nth-of-type(3) | — | — |
table-merged-data-row-has-three-cells | css_selector/ equal-countThe data row must have 3 cells. A merge-leak bug could collapse the data row to one cell too, which this catches. | .docx-wrapper table tr:nth-of-type(2) td, .docx-wrapper table tr:nth-of-type(2) th | — | — |
merged-header-has-a11y-semantic | css_selector/ existAccessibility: when a docx table's first row is a merged group-header, the renderer should signal header semantics — a <th> with colspan, a role='columnheader' with colspan, a scope='colgroup'/'col' marker, or a <thead>-nested merged <td>. Any of these is accepted; plain <td colspan='3'> with no header hint fails this assertion. | .docx-wrapper table th[colspan='3'], .docx-wrapper table [role='columnheader'][colspan='3'], .docx-wrapper table [scope='colgroup'], .docx-wrapper table [scope='col'], .docx-wrapper table thead td[colspan='3'] | — | — |
scripts/gen_table_merged_cells.py
#!/usr/bin/env python3
"""Generate ``fixtures/docx/table-merged-cells.docx``.
A 2x3 table whose first row is one horizontally merged cell spanning
all three columns (gridSpan=3). Satisfies the assertions in
``features/docx/table-merged-cells.json``.
Uses ``_Cell.merge()`` from the standard python-docx API.
"""
from __future__ import annotations
from pathlib import Path
from docx import Document
_REPO_ROOT = Path(__file__).resolve().parent.parent
_OUT = _REPO_ROOT / "fixtures" / "docx" / "table-merged-cells.docx"
def main() -> int:
doc = Document()
table = doc.add_table(rows=2, cols=3)
# Set the merged-header text on the top-left anchor cell, then merge it
# across the full width of the first row (gridSpan=3).
table.rows[0].cells[0].text = "Merged header across three columns"
a = table.rows[0].cells[0]
b = table.rows[0].cells[2]
a.merge(b)
table.rows[1].cells[0].text = "Cell 1"
table.rows[1].cells[1].text = "Cell 2"
table.rows[1].cells[2].text = "Cell 3"
_OUT.parent.mkdir(parents=True, exist_ok=True)
doc.save(_OUT, reproducible=True)
print(_OUT)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/table-merged-cells",
"title": "Table with horizontally merged cells (gridSpan)",
"format": "docx",
"category": "tables",
"summary": "A 2x3 table whose entire first row is one horizontally merged cell (gridSpan=3).",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "17.4.16",
"element": "w:gridSpan",
"rnc_reference": "spec/ecma-376-5/part-1/rnc/WordprocessingML.rnc",
"xsd_reference": "spec/ecma-376-5/part-1/xsd/wml.xsd",
"notes": "Horizontal merging is expressed via <w:tcPr><w:gridSpan w:val=\"N\"/></w:tcPr>, where N is the number of logical columns the cell covers. Vertical merging uses <w:vMerge> (17.4.29). This fixture exercises horizontal merging only."
},
"fixtures": {
"machine": "docx/table-merged-cells",
"office": "docx/table-merged-cells"
},
"generator": {
"python": "scripts/gen_table_merged_cells.py"
},
"assertions": [
{
"id": "tbl-gridspan-3-present",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:tc/w:tcPr/w:gridSpan[@w:val='3']",
"must": "exist",
"description": "At least one cell must declare a horizontal merge of 3 via <w:tcPr><w:gridSpan w:val=\"3\"/></w:tcPr>."
},
{
"id": "tbl-merged-text",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:tbl//w:tc//w:t[contains(normalize-space(), 'Merged header across three columns')]",
"must": "exist",
"description": "A cell containing the literal merged-header text must be present in the table."
}
],
"render_assertions": [
{
"id": "table-merged-cell-rendered",
"kind": "css_selector",
"selector": ".docx-wrapper table td[colspan='3'], .docx-wrapper table th[colspan='3']",
"must": "match-text",
"value": "Merged header",
"description": "A rendered <td>/<th> with colspan='3' under .docx-wrapper must contain 'Merged header'."
},
{
"id": "table-merged-cell-colspan-exists",
"kind": "css_selector",
"selector": ".docx-wrapper table td[colspan='3'], .docx-wrapper table th[colspan='3']",
"must": "equal-count",
"count": 1,
"description": "Exactly one cell with colspan='3' must be present (the merged header row). Catches renderers that drop or duplicate the horizontal merge."
},
{
"id": "table-row-count",
"kind": "css_selector",
"selector": ".docx-wrapper table tr",
"must": "equal-count",
"count": 2,
"description": "The rendered table has 2 rows (one merged header, one data row with three cells)."
},
{
"id": "table-merged-row1-cell1-rendered",
"kind": "css_selector",
"selector": ".docx-wrapper table tr:nth-of-type(2) td:nth-of-type(1), .docx-wrapper table tr:nth-of-type(2) th:nth-of-type(1)",
"must": "match-text",
"value": "^Cell 1$",
"description": "Data row, column 1 must be exactly 'Cell 1'. Catches the merged header's text leaking into the data row."
},
{
"id": "table-merged-row1-cell3-rendered",
"kind": "css_selector",
"selector": ".docx-wrapper table tr:nth-of-type(2) td:nth-of-type(3), .docx-wrapper table tr:nth-of-type(2) th:nth-of-type(3)",
"must": "match-text",
"value": "^Cell 3$",
"description": "Data row, column 3 must be exactly 'Cell 3'. Catches a merged row absorbing the data row below it."
},
{
"id": "table-merged-data-row-has-three-cells",
"kind": "css_selector",
"selector": ".docx-wrapper table tr:nth-of-type(2) td, .docx-wrapper table tr:nth-of-type(2) th",
"must": "equal-count",
"count": 3,
"description": "The data row must have 3 cells. A merge-leak bug could collapse the data row to one cell too, which this catches."
},
{
"id": "merged-header-has-a11y-semantic",
"kind": "css_selector",
"selector": ".docx-wrapper table th[colspan='3'], .docx-wrapper table [role='columnheader'][colspan='3'], .docx-wrapper table [scope='colgroup'], .docx-wrapper table [scope='col'], .docx-wrapper table thead td[colspan='3']",
"must": "exist",
"description": "Accessibility: when a docx table's first row is a merged group-header, the renderer should signal header semantics — a <th> with colspan, a role='columnheader' with colspan, a scope='colgroup'/'col' marker, or a <thead>-nested merged <td>. Any of these is accepted; plain <td colspan='3'> with no header hint fails this assertion."
}
]
}
