Parameterised interop matrix across bold x italic x underline x color. python-docx authors the fixture; docxjs renders it; the render_assertions block asserts that each expected run-property shows up in the docxjs DOM. Catches cases where python-docx writes valid OOXML but docxjs misparses or silently drops the property.
| Library | Pass | Fail | Pending |
|---|---|---|---|
python-docx | 0 | 0 | 8 |
docxjs | 0 | 0 | 8 |
combo (8 values): plain, bold-only, italic-only, underline-only, red-only, bold-italic, bold-underline-blue, all-greenscripts/gen_interop_formatting_matrix.py — runs with --arg_template --text "{combo.text}" --bold {combo.bold} --italic {combo.italic} --underline {combo.underline} --color-rgb {combo.color_rgb} --out fixtures/docx/interop-formatting-matrix--{combo.id}.docx
#!/usr/bin/env python3
"""Generate a ``fixtures/docx/interop-formatting-matrix--<id>.docx`` fixture.
Parameterised generator for the ``docx/interop-formatting-matrix`` manifest
(Wave 5-C, cross-library interop). Each expanded case produces a one-run
document that exercises a particular combination of ``bold`` × ``italic`` ×
``underline`` × ``color``. python-docx authors the file; docxjs renders it;
the manifest's ``render_assertions`` block verifies docxjs actually painted
the corresponding CSS bold / italic / underline / colour.
The matrix is deliberately small (8 cases) — the point is catching
cross-library drift (e.g. python-docx writes ``<w:u w:val="single">`` but
docxjs applies no text-decoration), not exhaustively covering the font
combinatorial space.
"""
from __future__ import annotations
import argparse
from pathlib import Path
from docx import Document
from docx.shared import RGBColor
_REPO_ROOT = Path(__file__).resolve().parent.parent
def _hex_to_rgb(hex_value: str) -> RGBColor:
r = int(hex_value[0:2], 16)
g = int(hex_value[2:4], 16)
b = int(hex_value[4:6], 16)
return RGBColor(r, g, b)
def _to_bool(s: str) -> bool:
return s.lower() in ("1", "true", "yes", "on")
def _write(
text: str,
bold: bool,
italic: bool,
underline: bool,
color_rgb: str,
out: Path,
) -> None:
doc = Document()
paragraph = doc.add_paragraph()
run = paragraph.add_run(text)
if bold:
run.bold = True
if italic:
run.italic = True
if underline:
run.underline = True
if color_rgb and color_rgb.lower() != "none":
run.font.color.rgb = _hex_to_rgb(color_rgb)
out.parent.mkdir(parents=True, exist_ok=True)
doc.save(out, reproducible=True)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--text", required=True)
parser.add_argument("--bold", default="false")
parser.add_argument("--italic", default="false")
parser.add_argument("--underline", default="false")
parser.add_argument("--color-rgb", default="none", help="6 hex RRGGBB or 'none'")
parser.add_argument("--out", required=True)
args = parser.parse_args()
out_path = Path(args.out)
if not out_path.is_absolute():
out_path = _REPO_ROOT / out_path
_write(
args.text,
_to_bool(args.bold),
_to_bool(args.italic),
_to_bool(args.underline),
args.color_rgb,
out_path,
)
print(out_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/interop-formatting-matrix",
"kind": "parameterised",
"title": "Cross-library formatting matrix (python-docx -> docxjs)",
"format": "docx",
"category": "interop",
"summary": "Parameterised interop matrix across bold x italic x underline x color. python-docx authors the fixture; docxjs renders it; the render_assertions block asserts that each expected run-property shows up in the docxjs DOM. Catches cases where python-docx writes valid OOXML but docxjs misparses or silently drops the property.",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "17.3.2",
"element": "w:rPr",
"rnc_reference": "spec/ecma-376-5/part-1/rnc/WordprocessingML.rnc",
"xsd_reference": "spec/ecma-376-5/part-1/xsd/wml.xsd",
"notes": "A run's formatting is carried by child elements of <w:rPr>: <w:b/> bold (17.3.2.1), <w:i/> italic (17.3.2.16), <w:u w:val=\"single\"/> underline (17.3.2.40), <w:color w:val=\"RRGGBB\"/> foreground colour (17.3.2.6). Individually each has its own feature manifest; this manifest's purpose is cross-library drift detection: spec-valid XML authored by python-docx must produce the corresponding CSS (font-weight, font-style, text-decoration, color) in the docxjs DOM. A fixture that passes `assertions` but fails `render_assertions` means the two libraries disagree about what the OOXML means. Each combo declares per-property expectations expressed as *regexes over computed CSS*: for properties the combo expects ON, the regex matches the positive CSS (e.g. `^(bold|700)$` for font-weight); for properties the combo expects OFF, the regex matches the idle default (e.g. `^(normal|400|)$`). The same render template therefore covers both positive and negative cases uniformly."
},
"fixtures": {
"machine": "docx/interop-formatting-matrix"
},
"generator": {
"python": "scripts/gen_interop_formatting_matrix.py",
"arg_template": "--text \"{combo.text}\" --bold {combo.bold} --italic {combo.italic} --underline {combo.underline} --color-rgb {combo.color_rgb} --out fixtures/docx/interop-formatting-matrix--{combo.id}.docx"
},
"parameters": {
"combo": [
{
"id": "plain",
"text": "Plain interop baseline",
"bold": "false",
"italic": "false",
"underline": "false",
"color_rgb": "none",
"bold_css": "^(normal|400|)$",
"italic_css": "^(normal|)$",
"underline_css": "^(none|normal|)$",
"color_css": "."
},
{
"id": "bold-only",
"text": "Bold interop text",
"bold": "true",
"italic": "false",
"underline": "false",
"color_rgb": "none",
"bold_css": "^(bold|700)$",
"italic_css": "^(normal|)$",
"underline_css": "^(none|normal|)$",
"color_css": "."
},
{
"id": "italic-only",
"text": "Italic interop text",
"bold": "false",
"italic": "true",
"underline": "false",
"color_rgb": "none",
"bold_css": "^(normal|400|)$",
"italic_css": "^italic$",
"underline_css": "^(none|normal|)$",
"color_css": "."
},
{
"id": "underline-only",
"text": "Underlined interop text",
"bold": "false",
"italic": "false",
"underline": "true",
"color_rgb": "none",
"bold_css": "^(normal|400|)$",
"italic_css": "^(normal|)$",
"underline_css": ".*underline.*",
"color_css": "."
},
{
"id": "red-only",
"text": "Red interop text",
"bold": "false",
"italic": "false",
"underline": "false",
"color_rgb": "FF0000",
"bold_css": "^(normal|400|)$",
"italic_css": "^(normal|)$",
"underline_css": "^(none|normal|)$",
"color_css": "rgb\\(255,\\s*0,\\s*0\\)|#ff0000|#FF0000|red"
},
{
"id": "bold-italic",
"text": "Bold italic interop text",
"bold": "true",
"italic": "true",
"underline": "false",
"color_rgb": "none",
"bold_css": "^(bold|700)$",
"italic_css": "^italic$",
"underline_css": "^(none|normal|)$",
"color_css": "."
},
{
"id": "bold-underline-blue",
"text": "Bold underlined blue interop text",
"bold": "true",
"italic": "false",
"underline": "true",
"color_rgb": "0000FF",
"bold_css": "^(bold|700)$",
"italic_css": "^(normal|)$",
"underline_css": ".*underline.*",
"color_css": "rgb\\(0,\\s*0,\\s*255\\)|#0000ff|#0000FF|blue"
},
{
"id": "all-green",
"text": "Bold italic underlined green interop text",
"bold": "true",
"italic": "true",
"underline": "true",
"color_rgb": "008000",
"bold_css": "^(bold|700)$",
"italic_css": "^italic$",
"underline_css": ".*underline.*",
"color_css": "rgb\\(0,\\s*128,\\s*0\\)|#008000|green"
}
]
},
"assertions_template": [
{
"id": "run-text-present-{combo.id}",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:r[w:t[normalize-space()='{combo.text}']]",
"must": "exist",
"description": "The run carrying the fixture text must be present."
}
],
"render_assertions_template": [
{
"id": "interop-text-present-{combo.id}",
"kind": "css_selector",
"selector": ".docx-wrapper p",
"must": "match-text",
"value": "{combo.text}",
"description": "The rendered DOM must contain the fixture text."
},
{
"id": "interop-font-weight-{combo.id}",
"kind": "computed_style",
"selector": ".docx-wrapper :is(b, strong, span):not(:has(*))",
"style_property": "font-weight",
"value": "{combo.bold_css}",
"description": "Computed font-weight on the rendered run must match the expected state for this combo."
},
{
"id": "interop-font-style-{combo.id}",
"kind": "computed_style",
"selector": ".docx-wrapper :is(i, em, span):not(:has(*))",
"style_property": "font-style",
"value": "{combo.italic_css}",
"description": "Computed font-style on the rendered run must match the expected state for this combo."
},
{
"id": "interop-text-decoration-{combo.id}",
"kind": "computed_style",
"selector": ".docx-wrapper :is(u, span):not(:has(*))",
"style_property": "text-decoration",
"value": "{combo.underline_css}",
"description": "Computed text-decoration on the rendered run must match the expected state for this combo."
}
]
}