docx/hyperlink-run-properties

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.

Cases (3)

Feature IDAxis bindingspython-docxdocxjs
docx/hyperlink-run-properties--explicit-colorvariant=explicit-color
docx/hyperlink-run-properties--style-onlyvariant=style-only
docx/hyperlink-run-properties--underline-offvariant=underline-off

Aggregate

LibraryPassFailPending
python-docx003
docxjs003

Parameter axes

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

Generator source

scripts/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())

Manifest (parent, unexpanded)

{
  "$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'."
    }
  ]
}