Exercise the four w:hyperlink target shapes (web URL, local file URL, mailto URL, internal anchor) against two display-text variants.
Library verdicts: python-docx: — docxjs: —
| Feature id | docx/hyperlink-target--full--web |
|---|---|
| Format | docx |
| Category | references |
| Family | docx/hyperlink-target |
| Axis values | display=full, target=web |
| Spec | ecma-376-5-part-1 § 17.16.22 w:hyperlink |
| ID / part | Predicate | XPath | python-docx | docxjs |
|---|---|---|---|---|
has-hyperlink-web-fullword/document.xml | existA w:hyperlink wrapping a run whose text matches the expected display text must exist. | //w:hyperlink[.//w:t[normalize-space()='The full URL — display']] | — | — |
No render assertions declared.
scripts/gen_hyperlink_target.py
#!/usr/bin/env python3
"""Generate a ``fixtures/docx/hyperlink-target--*.docx`` fixture.
Parameterised across the 4 ``w:hyperlink`` target shapes and 2
display-text variants described by
``features/docx/hyperlink-target.json``.
CLI:
--kind {url|anchor} which target shape to emit
--url <str> external URL (required when --kind=url)
--anchor <str> bookmark name (required when --kind=anchor)
--text <str> visible link text
--out <path> destination docx path (repo-relative allowed)
For ``url`` targets the fixture is a one-paragraph document whose
paragraph contains a ``<w:hyperlink r:id="rIdN">`` pointing at an
external relationship.
For ``anchor`` targets the fixture additionally emits a prior
paragraph carrying a ``<w:bookmarkStart w:name="..."/> ... <w:bookmarkEnd/>``
pair so the internal link actually resolves.
Uses the fork's ``Paragraph.add_hyperlink()`` high-level API
(python-docx >= 2026.05.0) for both shapes. Falls back to raw
``OxmlElement`` construction only if that API is unavailable — the
fallback mirrors the fork's internal implementation so the emitted
XML is byte-compatible.
.. versionadded:: 2026.05.0
"""
from __future__ import annotations
import argparse
from pathlib import Path
from docx import Document
_REPO_ROOT = Path(__file__).resolve().parent.parent
def _add_hyperlink_fallback_url(paragraph, url: str, text: str) -> None:
"""Fallback CT-level builder for an external-URL hyperlink."""
from docx.opc.constants import RELATIONSHIP_TYPE as RT
from docx.oxml.ns import qn
from docx.oxml.parser import OxmlElement
r_id = paragraph.part.relate_to(url, RT.HYPERLINK, is_external=True)
h = OxmlElement("w:hyperlink")
h.set(qn("r:id"), r_id)
r = OxmlElement("w:r")
t = OxmlElement("w:t")
t.text = text
r.append(t)
h.append(r)
paragraph._p.append(h)
def _add_hyperlink_fallback_anchor(paragraph, anchor: str, text: str) -> None:
"""Fallback CT-level builder for an internal-anchor hyperlink."""
from docx.oxml.ns import qn
from docx.oxml.parser import OxmlElement
h = OxmlElement("w:hyperlink")
h.set(qn("w:anchor"), anchor)
r = OxmlElement("w:r")
t = OxmlElement("w:t")
t.text = text
r.append(t)
h.append(r)
paragraph._p.append(h)
def _add_bookmark(doc, name: str) -> None:
"""Emit a paragraph carrying a matching bookmarkStart / bookmarkEnd."""
from docx.oxml.ns import qn
from docx.oxml.parser import OxmlElement
bookmark_para = doc.add_paragraph("Bookmark target")
start = OxmlElement("w:bookmarkStart")
start.set(qn("w:id"), "0")
start.set(qn("w:name"), name)
end = OxmlElement("w:bookmarkEnd")
end.set(qn("w:id"), "0")
bookmark_para._p.insert(0, start)
bookmark_para._p.append(end)
def _write(kind: str, url: str, anchor: str, text: str, out: Path) -> bool:
"""Build and save the fixture. Returns True if the fallback path was used."""
doc = Document()
used_fallback = False
if kind == "url":
paragraph = doc.add_paragraph()
try:
paragraph.add_hyperlink(url=url, text=text, style=None)
except AttributeError:
_add_hyperlink_fallback_url(paragraph, url, text)
used_fallback = True
elif kind == "anchor":
_add_bookmark(doc, anchor)
paragraph = doc.add_paragraph()
try:
paragraph.add_hyperlink(anchor=anchor, text=text, style=None)
except AttributeError:
_add_hyperlink_fallback_anchor(paragraph, anchor, text)
used_fallback = True
else:
raise ValueError(f"Unknown --kind {kind!r}; expected 'url' or 'anchor'")
out.parent.mkdir(parents=True, exist_ok=True)
doc.save(out, reproducible=True)
return used_fallback
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--kind", required=True, choices=("url", "anchor"))
parser.add_argument("--url", default="")
parser.add_argument("--anchor", default="")
parser.add_argument("--text", required=True)
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
if args.kind == "url" and not args.url:
parser.error("--url is required when --kind=url")
if args.kind == "anchor" and not args.anchor:
parser.error("--anchor is required when --kind=anchor")
used_fallback = _write(args.kind, args.url, args.anchor, args.text, out_path)
print(out_path, "(fallback)" if used_fallback else "")
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/hyperlink-target--full--web",
"kind": "literal",
"title": "Hyperlink target variants",
"format": "docx",
"category": "references",
"summary": "Exercise the four w:hyperlink target shapes (web URL, local file URL, mailto URL, internal anchor) against two display-text variants.",
"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": "w:hyperlink may target either an external relationship (carried in @r:id, resolved via word/_rels/document.xml.rels with TargetMode='External') or an internal bookmark (@w:anchor). This parameterised family exercises 4 target types (web URL, local file URL, mailto URL, internal anchor) crossed with 2 display-text variants (short label vs. full URL-like text) for 8 total cases. The sibling manifest `docx/hyperlink` covers the single canonical external-URL case; this manifest exercises the target-shape matrix."
},
"fixtures": {
"machine": "docx/hyperlink-target--full--web"
},
"generator": {
"python": "scripts/gen_hyperlink_target.py",
"arg_template": "--kind {target.kind} --url \"{target.url}\" --anchor \"{target.anchor}\" --text \"{display.text}\" --out fixtures/docx/hyperlink-target--{display.id}--{target.id}.docx"
},
"_expansion": {
"parent_id": "docx/hyperlink-target",
"bindings": {
"display": "full",
"target": "web"
}
},
"assertions": [
{
"id": "has-hyperlink-web-full",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
},
"xpath": "//w:hyperlink[.//w:t[normalize-space()='The full URL — display']]",
"must": "exist",
"description": "A w:hyperlink wrapping a run whose text matches the expected display text must exist."
}
]
}
