Cross-axis matrix verifying that a docx w:hyperlink's relationship entry in word/_rels/document.xml.rels carries the correct Target URI and TargetMode (External / omitted-for-internal) for each scheme family.
Library verdicts: python-docx: — docxjs: —
| Feature id | docx/hyperlink-target-modes--file-uri |
|---|---|
| Format | docx |
| Category | references |
| Family | docx/hyperlink-target-modes |
| Axis values | scheme=file-uri |
| Spec | ecma-376-5-part-1 § 17.16.22 w:hyperlink |
| ID / part | Predicate | XPath | python-docx | docxjs |
|---|---|---|---|---|
rel-target-equals-file-uriword/_rels/document.xml.rels | equal = file:///C:/docs/other.docxFor external schemes the hyperlink relationship's @Target must equal the URL emitted by the generator. For the anchor-only case no hyperlink relationship exists, so string() returns '' and the expected value is also ''. | string(//pr:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink']/@Target) | — | — |
rel-targetmode-file-uriword/_rels/document.xml.rels | equal = ExternalExternal hyperlink relationships must carry TargetMode='External'; the anchor-only case carries no hyperlink relationship (string() returns ''). | string(//pr:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink']/@TargetMode) | — | — |
anchor-or-rid-present-file-uriword/document.xml | existEvery w:hyperlink must carry either an r:id (external) or a w:anchor (internal). | //w:hyperlink[@r:id or @w:anchor] | — | — |
No render assertions declared.
scripts/gen_hyperlink_target_modes.py
#!/usr/bin/env python3
"""Generate ``fixtures/docx/hyperlink-target-modes--<scheme>.docx``.
Parameterised across the 5 TargetMode axes described by
``features/docx/hyperlink-target-modes.json``.
Exercised URI schemes:
* ``http`` - ``https://...`` - external, TargetMode=External
* ``mailto`` - ``mailto:...`` - external, TargetMode=External
* ``file-uri``- ``file:///...`` - external, TargetMode=External
* ``relative``- ``../sibling/other.docx`` - external (rare but spec-allowed),
TargetMode=External
* ``anchor`` - internal bookmark, no rel
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)
--out <path> destination docx path (repo-relative allowed)
.. 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_bookmark(doc, name: str) -> None:
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, out: Path) -> None:
doc = Document()
if kind == "url":
paragraph = doc.add_paragraph()
# style=None avoids depending on a fresh Document() carrying
# the "Hyperlink" character style.
paragraph.add_hyperlink(url=url, text="Link", style=None)
elif kind == "anchor":
_add_bookmark(doc, anchor)
paragraph = doc.add_paragraph()
paragraph.add_hyperlink(anchor=anchor, text="Jump", style=None)
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)
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("--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")
_write(args.kind, args.url, args.anchor, out_path)
print(out_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/hyperlink-target-modes--file-uri",
"kind": "literal",
"title": "Hyperlink relationship TargetMode matrix (docx)",
"format": "docx",
"category": "references",
"summary": "Cross-axis matrix verifying that a docx w:hyperlink's relationship entry in word/_rels/document.xml.rels carries the correct Target URI and TargetMode (External / omitted-for-internal) for each scheme family.",
"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": "Complements docx/hyperlink-target.json (which checks only the in-document w:hyperlink element). This family pins the cross-cutting Part-2 OPC invariant: every external-scheme hyperlink MUST be carried by a <Relationship .../> in the sibling *.rels file with TargetMode=\"External\". Internal anchor hyperlinks carry @w:anchor and MUST NOT emit a hyperlink relationship at all. Exercised schemes: http(s), mailto, file://, relative (../other.docx) -- the last is spec-allowed but rarely seen in the wild."
},
"fixtures": {
"machine": "docx/hyperlink-target-modes--file-uri"
},
"generator": {
"python": "scripts/gen_hyperlink_target_modes.py",
"arg_template": "--kind {scheme.kind} --url \"{scheme.url}\" --anchor \"{scheme.anchor}\" --out fixtures/docx/hyperlink-target-modes--{scheme.id}.docx"
},
"_expansion": {
"parent_id": "docx/hyperlink-target-modes",
"bindings": {
"scheme": "file-uri"
}
},
"assertions": [
{
"id": "rel-target-equals-file-uri",
"part": "word/_rels/document.xml.rels",
"namespaces": {
"pr": "http://schemas.openxmlformats.org/package/2006/relationships"
},
"xpath": "string(//pr:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink']/@Target)",
"must": "equal",
"value": "file:///C:/docs/other.docx",
"description": "For external schemes the hyperlink relationship's @Target must equal the URL emitted by the generator. For the anchor-only case no hyperlink relationship exists, so string() returns '' and the expected value is also ''."
},
{
"id": "rel-targetmode-file-uri",
"part": "word/_rels/document.xml.rels",
"namespaces": {
"pr": "http://schemas.openxmlformats.org/package/2006/relationships"
},
"xpath": "string(//pr:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink']/@TargetMode)",
"must": "equal",
"value": "External",
"description": "External hyperlink relationships must carry TargetMode='External'; the anchor-only case carries no hyperlink relationship (string() returns '')."
},
{
"id": "anchor-or-rid-present-file-uri",
"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 or @w:anchor]",
"must": "exist",
"description": "Every w:hyperlink must carry either an r:id (external) or a w:anchor (internal)."
}
]
}
No rendered reference is available for this case.