Set a run's foreground color to a theme colour slot, optionally shifted by a themeTint or themeShade.
Library verdicts: python-docx: — docxjs: pass
| Feature id | docx/font-color-theme--shade-40--accent2 |
|---|---|
| Format | docx |
| Category | text-formatting |
| Family | docx/font-color-theme |
| Axis values | shift=shade-40, theme=accent2 |
| Spec | ecma-376-5-part-1 § 17.3.2.6 w:color |
| ID / part | Predicate | XPath | python-docx | docxjs |
|---|---|---|---|---|
theme-color-is-accent2word/document.xml | equal = accent2The run's w:color element must carry w:themeColor bound to the expected theme slot. | //w:r/w:rPr/w:color/@w:themeColor | — | — |
| ID | Predicate | Selector | python-docx | docxjs |
|---|---|---|---|---|
font-color-theme-accent2-shade-40-text-present | css_selector/ existThe rendered DOM must contain the fixture's run (theme-colour resolution itself is out of scope — renderers vary widely). | .docx-wrapper span, .docx-wrapper p | — | pass |
scripts/gen_font_color_theme.py
#!/usr/bin/env python3
"""Generate a ``fixtures/docx/font-color-theme--*.docx`` fixture.
Parameterised generator for the ``docx/font-color-theme`` feature
family (12 theme slots x 7 tint/shade shifts = 84 cases).
Each invocation writes one fixture. The ``features/docx/font-color-theme.json``
manifest's ``generator.arg_template`` drives the arguments at expansion time::
--theme <slot> --tint "<hex|empty>" --shade "<hex|empty>" --out <path>
``<slot>`` is an ``ST_ThemeColor`` enumeration value (e.g. ``accent1``,
``background1``, ``followedHyperlink``). ``--tint`` and ``--shade`` each
take a 2-digit uppercase hex byte (e.g. ``66``, ``99``, ``CC``) OR an
empty string to indicate "no shift". A given case sets at most one of
tint/shade — setting neither produces the base theme colour.
python-docx's ``font.color.rgb`` descriptor only writes ``w:val``, so
this generator reaches down to the oxml layer to add the
``w:themeColor`` / ``w:themeTint`` / ``w:themeShade`` attributes
directly, matching how Word emits the element (clause 17.3.2.6).
"""
from __future__ import annotations
import argparse
from pathlib import Path
from docx import Document
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
_REPO_ROOT = Path(__file__).resolve().parent.parent
def _write(theme: str, tint: str, shade: str, out: Path) -> None:
doc = Document()
paragraph = doc.add_paragraph()
run = paragraph.add_run("Theme colored text")
rPr = run._r.get_or_add_rPr()
color = OxmlElement("w:color")
# ``w:val`` is required by the schema even when the effective colour
# is resolved from the theme — Word writes ``auto`` as a placeholder.
color.set(qn("w:val"), "auto")
color.set(qn("w:themeColor"), theme)
if tint:
color.set(qn("w:themeTint"), tint)
if shade:
color.set(qn("w:themeShade"), shade)
rPr.append(color)
out.parent.mkdir(parents=True, exist_ok=True)
doc.save(out, reproducible=True)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--theme",
default="accent1",
help="ST_ThemeColor enum value (e.g. accent1, background1, followedHyperlink).",
)
parser.add_argument(
"--tint",
default="",
help='2-digit uppercase hex byte (e.g. "66") or "" to skip.',
)
parser.add_argument(
"--shade",
default="",
help='2-digit uppercase hex byte (e.g. "CC") or "" to skip.',
)
parser.add_argument(
"--out",
default=str(
_REPO_ROOT / "fixtures" / "docx" / "font-color-theme.docx"
),
)
args = parser.parse_args()
if args.tint and args.shade:
raise SystemExit(
"font-color-theme: --tint and --shade are mutually exclusive "
"(Word picks one or neither per w:color element)."
)
out_path = Path(args.out)
if not out_path.is_absolute():
out_path = _REPO_ROOT / out_path
_write(args.theme, args.tint, args.shade, out_path)
print(out_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/font-color-theme--shade-40--accent2",
"kind": "literal",
"title": "Font color (theme)",
"format": "docx",
"category": "text-formatting",
"summary": "Set a run's foreground color to a theme colour slot, optionally shifted by a themeTint or themeShade.",
"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=\"auto\" w:themeColor=\"<slot>\" [w:themeTint=\"<hex>\" | w:themeShade=\"<hex>\"]/> binds the run's foreground color to one of the 12 theme-color slots (ST_ThemeColor) from the document's theme1.xml. The optional w:themeTint / w:themeShade attributes shift the resolved colour lighter (tint) or darker (shade) by a 1-byte hex factor (Word's scale: 66=40%, 99=60%, CC=80%). Themes and tint/shade resolution are a rendering concern — authoring assertions only verify the XML attributes round-trip; rendering assertions only verify the run text is visible. This manifest is a parameterised family: 12 theme slots x 7 tint/shade shifts = 84 fixtures, each a distinct .docx named docx/font-color-theme--<theme>--<shift>.docx."
},
"fixtures": {
"machine": "docx/font-color-theme--shade-40--accent2"
},
"generator": {
"python": "scripts/gen_font_color_theme.py",
"arg_template": "--theme {theme.val} --tint \"{shift.themeTint}\" --shade \"{shift.themeShade}\" --out fixtures/docx/font-color-theme--{shift.id}--{theme.id}.docx"
},
"_expansion": {
"parent_id": "docx/font-color-theme",
"bindings": {
"shift": "shade-40",
"theme": "accent2"
}
},
"assertions": [
{
"id": "theme-color-is-accent2",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:r/w:rPr/w:color/@w:themeColor",
"must": "equal",
"value": "accent2",
"description": "The run's w:color element must carry w:themeColor bound to the expected theme slot."
}
],
"render_assertions": [
{
"id": "font-color-theme-accent2-shade-40-text-present",
"kind": "css_selector",
"selector": ".docx-wrapper span, .docx-wrapper p",
"must": "exist",
"description": "The rendered DOM must contain the fixture's run (theme-colour resolution itself is out of scope — renderers vary widely)."
}
]
}
