A w:hyperlink's inner run can either inherit its visible formatting from the built-in 'Hyperlink' character style via w:rStyle, or override it directly with an inline w:rPr (color, underline, etc.). This family exercises three variants: style-only (default), explicit-color override, and explicit underline=none override.
| Library | Pass | Fail | Pending |
|---|---|---|---|
python-docx | 0 | 0 | 3 |
docxjs | 0 | 0 | 3 |
variant (3 values): style-only, explicit-color, underline-offscripts/gen_hyperlink_run_properties.py — runs with --arg_template --variant {variant.id} --out fixtures/docx/hyperlink-run-properties--{variant.id}.docx
#!/usr/bin/env python3
"""Generate ``fixtures/docx/hyperlink-run-properties--<variant>.docx``.
Parameterised across the 3 run-property variants described by
``features/docx/hyperlink-run-properties.json``:
* ``style-only`` - w:rStyle=Hyperlink (no explicit overrides)
* ``explicit-color`` - w:color w:val='FF0000' (no rStyle, the override
supersedes the character-style default)
* ``underline-off`` - w:u w:val='none'
The ``style-only`` variant deliberately relies on the built-in
``Hyperlink`` character style. A fresh ``Document()`` does not carry
that style by default; we add it explicitly here so the fixture is
self-contained and matches Word's on-disk shape.
.. versionadded:: 2026.05.0
"""
from __future__ import annotations
import argparse
from pathlib import Path
from docx import Document
from docx.enum.style import WD_STYLE_TYPE
from docx.oxml.ns import qn
from docx.oxml.parser import OxmlElement
from docx.shared import RGBColor
_REPO_ROOT = Path(__file__).resolve().parent.parent
def _ensure_hyperlink_style(doc) -> None:
"""Ensure the 'Hyperlink' character style exists."""
styles = doc.styles
try:
styles["Hyperlink"]
return
except KeyError:
pass
styles.add_style("Hyperlink", WD_STYLE_TYPE.CHARACTER)
def _write(variant: str, out: Path) -> None:
doc = Document()
p = doc.add_paragraph()
if variant == "style-only":
_ensure_hyperlink_style(doc)
p.add_hyperlink(
url="https://example.com/",
text="Click me",
style="Hyperlink",
)
elif variant == "explicit-color":
# style=None suppresses the w:rStyle; then apply explicit color.
hl = p.add_hyperlink(
url="https://example.com/",
text="Click me",
style=None,
)
run = hl.runs[0]
run.font.color.rgb = RGBColor(0xFF, 0x00, 0x00)
elif variant == "underline-off":
hl = p.add_hyperlink(
url="https://example.com/",
text="Click me",
style=None,
)
run = hl.runs[0]
# python-docx emits <w:u w:val='none'/> when underline is set False
run.font.underline = False
# Confirm the XML-level value is 'none' (the default emitted form).
# If not, force it explicitly.
rPr = run._element.get_or_add_rPr()
u = rPr.find(qn("w:u"))
if u is None:
u = OxmlElement("w:u")
rPr.append(u)
u.set(qn("w:val"), "none")
else:
raise ValueError(f"Unknown --variant {variant!r}")
out.parent.mkdir(parents=True, exist_ok=True)
doc.save(out, reproducible=True)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--variant",
required=True,
choices=("style-only", "explicit-color", "underline-off"),
)
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.variant, out_path)
print(out_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/hyperlink-run-properties",
"kind": "parameterised",
"title": "Hyperlink run properties (rPr / Hyperlink style)",
"format": "docx",
"category": "references",
"summary": "A w:hyperlink's inner run can either inherit its visible formatting from the built-in 'Hyperlink' character style via w:rStyle, or override it directly with an inline w:rPr (color, underline, etc.). This family exercises three variants: style-only (default), explicit-color override, and explicit underline=none override.",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "17.16.22",
"element": "w:hyperlink",
"rnc_reference": "spec/ecma-376-5/part-1/rnc/WordprocessingML.rnc",
"xsd_reference": "spec/ecma-376-5/part-1/xsd/wml.xsd",
"notes": "Separate from docx/hyperlink-target-modes (which asserts the relationship side) and docx/hyperlink (which asserts only the element exists). Here we assert what sits INSIDE the hyperlink's w:r/w:rPr. The 'Hyperlink' character style is a built-in character style referenced via <w:rStyle w:val='Hyperlink'/>; explicit overrides appear as sibling elements inside the same w:rPr. Word's Insert Hyperlink dialog emits the style-only form; the override forms appear after the user manually recolors or un-underlines the link text."
},
"fixtures": {
"machine": "docx/hyperlink-run-properties"
},
"generator": {
"python": "scripts/gen_hyperlink_run_properties.py",
"arg_template": "--variant {variant.id} --out fixtures/docx/hyperlink-run-properties--{variant.id}.docx"
},
"parameters": {
"variant": [
{
"id": "style-only",
"expect_rstyle": "exist",
"expect_color": "absent",
"expect_uoff": "absent"
},
{
"id": "explicit-color",
"expect_rstyle": "absent",
"expect_color": "exist",
"expect_uoff": "absent"
},
{
"id": "underline-off",
"expect_rstyle": "absent",
"expect_color": "absent",
"expect_uoff": "exist"
}
]
},
"assertions_template": [
{
"id": "has-hyperlink-{variant.id}",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
},
"xpath": "//w:hyperlink[@r:id]",
"must": "exist",
"description": "The generated fixture must contain one external-URL w:hyperlink."
},
{
"id": "rstyle-{variant.id}",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:hyperlink/w:r/w:rPr/w:rStyle[@w:val='Hyperlink']",
"must": "{variant.expect_rstyle}",
"description": "In the style-only variant the run carries <w:rStyle w:val='Hyperlink'/>. Override variants suppress that so the override can take effect cleanly."
},
{
"id": "color-{variant.id}",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:hyperlink/w:r/w:rPr/w:color[@w:val='FF0000']",
"must": "{variant.expect_color}",
"description": "The explicit-color variant overrides the theme hyperlink color with w:color w:val='FF0000' (pure red)."
},
{
"id": "underline-off-{variant.id}",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:hyperlink/w:r/w:rPr/w:u[@w:val='none']",
"must": "{variant.expect_uoff}",
"description": "The underline-off variant suppresses the default hyperlink underline with w:u w:val='none'."
}
]
}