docx/interop-formatting-matrix--bold-only

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 verdicts: python-docx: — docxjs: —

Metadata

Feature iddocx/interop-formatting-matrix--bold-only
Formatdocx
Categoryinterop
Familydocx/interop-formatting-matrix
Axis valuescombo=bold-only
Spececma-376-5-part-1 § 17.3.2 w:rPr

XPath assertions

ID / partPredicateXPathpython-docxdocxjs
run-text-present-bold-only
word/document.xml
exist
The run carrying the fixture text must be present.
//w:r[w:t[normalize-space()='Bold interop text']]

Render assertions

IDPredicateSelectorpython-docxdocxjs
interop-text-present-bold-onlycss_selector/ match-text = Bold interop text
The rendered DOM must contain the fixture text.
.docx-wrapper p
interop-font-weight-bold-onlycomputed_style = ^(bold|700)$ style=font-weight
Computed font-weight on the rendered run must match the expected state for this combo.
.docx-wrapper :is(b, strong, span):not(:has(*))
interop-font-style-bold-onlycomputed_style = ^(normal|)$ style=font-style
Computed font-style on the rendered run must match the expected state for this combo.
.docx-wrapper :is(i, em, span):not(:has(*))
interop-text-decoration-bold-onlycomputed_style = ^(none|normal|)$ style=text-decoration
Computed text-decoration on the rendered run must match the expected state for this combo.
.docx-wrapper :is(u, span):not(:has(*))

Generator source

scripts/gen_interop_formatting_matrix.py

#!/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())

Manifest (expanded)

{
  "$schema": "../manifest.schema.json",
  "id": "docx/interop-formatting-matrix--bold-only",
  "kind": "literal",
  "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--bold-only"
  },
  "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"
  },
  "_expansion": {
    "parent_id": "docx/interop-formatting-matrix",
    "bindings": {
      "combo": "bold-only"
    }
  },
  "assertions": [
    {
      "id": "run-text-present-bold-only",
      "part": "word/document.xml",
      "namespaces": {
        "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
      },
      "xpath": "//w:r[w:t[normalize-space()='Bold interop text']]",
      "must": "exist",
      "description": "The run carrying the fixture text must be present."
    }
  ],
  "render_assertions": [
    {
      "id": "interop-text-present-bold-only",
      "kind": "css_selector",
      "selector": ".docx-wrapper p",
      "must": "match-text",
      "value": "Bold interop text",
      "description": "The rendered DOM must contain the fixture text."
    },
    {
      "id": "interop-font-weight-bold-only",
      "kind": "computed_style",
      "selector": ".docx-wrapper :is(b, strong, span):not(:has(*))",
      "style_property": "font-weight",
      "value": "^(bold|700)$",
      "description": "Computed font-weight on the rendered run must match the expected state for this combo."
    },
    {
      "id": "interop-font-style-bold-only",
      "kind": "computed_style",
      "selector": ".docx-wrapper :is(i, em, span):not(:has(*))",
      "style_property": "font-style",
      "value": "^(normal|)$",
      "description": "Computed font-style on the rendered run must match the expected state for this combo."
    },
    {
      "id": "interop-text-decoration-bold-only",
      "kind": "computed_style",
      "selector": ".docx-wrapper :is(u, span):not(:has(*))",
      "style_property": "text-decoration",
      "value": "^(none|normal|)$",
      "description": "Computed text-decoration on the rendered run must match the expected state for this combo."
    }
  ]
}

Fixture

Download interop-formatting-matrix--bold-only.docx (18.6 KB)

Reference preview

No rendered reference is available for this case.

Spec 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.