Set a run's foreground color to a specific RGB hex value.
Library verdicts: python-docx: — docxjs: pass
| Feature id | docx/font-color--cyan |
|---|---|
| Format | docx |
| Category | text-formatting |
| Family | docx/font-color |
| Axis values | color=cyan |
| Spec | ecma-376-5-part-1 § 17.3.2.6 w:color |
| ID / part | Predicate | XPath | python-docx | docxjs |
|---|---|---|---|---|
color-element-present-cyanword/document.xml | existThe run must have a <w:color> child carrying the foreground color. | //w:r[w:t[normalize-space()='Cyan text']]/w:rPr/w:color | — | — |
color-val-is-cyanword/document.xml | match = ^00FFFF$Foreground color must be RGB #00FFFF (6 uppercase hex digits). | //w:r[w:t[normalize-space()='Cyan text']]/w:rPr/w:color/@w:val | — | — |
| ID | Predicate | Selector | python-docx | docxjs |
|---|---|---|---|---|
font-color-cyan-text-present | css_selector/ match-text = Cyan textThe rendered DOM must contain the fixture text. | .docx-wrapper span, .docx-wrapper p | — | pass |
font-color-cyan-is-correct | computed_style = ^(rgb\(0,\s*255,\s*255\)|#00ffff|#00FFFF|cyan|aqua)$ style=colorThe computed foreground color on the coloured run must resolve to the expected RGB. | .docx-wrapper span:not(:has(*)) | — | pass |
scripts/gen_font_color.py
#!/usr/bin/env python3
"""Generate a ``fixtures/docx/font-color*.docx`` fixture.
Supports two modes:
- **Literal (legacy):** no arguments — writes
``fixtures/docx/font-color.docx`` with the hard-coded "Red text" +
``#FF0000`` shape. Retained so the pre-parameterised manifest form
still works for anyone who pins this file directly.
- **Parameterised:** called with ``--color-rgb <RRGGBB> --text
<text> --out <path>``. Writes the fixture to the given path with
the specified colour and text. The ``features/docx/font-color.json``
manifest's ``generator.arg_template`` field drives these args at
expansion time.
"""
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:
if len(hex_value) != 6:
raise ValueError(f"Expected 6-digit hex RRGGBB, got {hex_value!r}")
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 _write(text: str, color_rgb: str, out: Path) -> None:
doc = Document()
paragraph = doc.add_paragraph()
run = paragraph.add_run(text)
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("--color-rgb", default="FF0000", help="6-digit uppercase hex")
parser.add_argument("--text", default="Red text")
parser.add_argument(
"--out",
default=str(_REPO_ROOT / "fixtures" / "docx" / "font-color.docx"),
)
args = parser.parse_args()
out_path = Path(args.out)
if not out_path.is_absolute():
out_path = _REPO_ROOT / out_path
_write(args.text, args.color_rgb, out_path)
print(out_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/font-color--cyan",
"kind": "literal",
"title": "Font color",
"format": "docx",
"category": "text-formatting",
"summary": "Set a run's foreground color to a specific RGB hex value.",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "17.3.2.6",
"element": "w:color",
"rnc_reference": "spec/ecma-376-5/part-1/rnc/WordprocessingML.rnc",
"xsd_reference": "spec/ecma-376-5/part-1/xsd/wml.xsd",
"notes": "<w:color w:val=\"RRGGBB\"/> sets the foreground color. The value is 6 uppercase hex digits OR the literal 'auto'. This manifest is a parameterised family: one manifest file expands into ten concrete colour test cases (FF0000 through 7F00FF) at runtime. Each expanded case targets a distinct fixture `docx/font-color--<id>.docx` generated from the same gen_font_color.py script parameterised by CLI args."
},
"fixtures": {
"machine": "docx/font-color--cyan",
"office": "docx/font-color--cyan"
},
"generator": {
"python": "scripts/gen_font_color.py",
"arg_template": "--color-rgb {color.rgb} --text \"{color.text}\" --out fixtures/docx/font-color--{color.id}.docx"
},
"_expansion": {
"parent_id": "docx/font-color",
"bindings": {
"color": "cyan"
}
},
"assertions": [
{
"id": "color-element-present-cyan",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:r[w:t[normalize-space()='Cyan text']]/w:rPr/w:color",
"must": "exist",
"description": "The run must have a <w:color> child carrying the foreground color."
},
{
"id": "color-val-is-cyan",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:r[w:t[normalize-space()='Cyan text']]/w:rPr/w:color/@w:val",
"must": "match",
"value": "^00FFFF$",
"description": "Foreground color must be RGB #00FFFF (6 uppercase hex digits)."
}
],
"render_assertions": [
{
"id": "font-color-cyan-text-present",
"kind": "css_selector",
"selector": ".docx-wrapper span, .docx-wrapper p",
"must": "match-text",
"value": "Cyan text",
"description": "The rendered DOM must contain the fixture text."
},
{
"id": "font-color-cyan-is-correct",
"kind": "computed_style",
"selector": ".docx-wrapper span:not(:has(*))",
"style_property": "color",
"value": "^(rgb\\(0,\\s*255,\\s*255\\)|#00ffff|#00FFFF|cyan|aqua)$",
"description": "The computed foreground color on the coloured run must resolve to the expected RGB."
}
]
}
