docx/hyperlink-target--plain--internal

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: —

Metadata

Feature iddocx/hyperlink-target--plain--internal
Formatdocx
Categoryreferences
Familydocx/hyperlink-target
Axis valuesdisplay=plain, target=internal
Spececma-376-5-part-1 § 17.16.22 w:hyperlink

XPath assertions

ID / partPredicateXPathpython-docxdocxjs
has-hyperlink-internal-plain
word/document.xml
exist
A w:hyperlink wrapping a run whose text matches the expected display text must exist.
//w:hyperlink[.//w:t[normalize-space()='Click here']]

Render assertions

No render assertions declared.

Generator source

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

Manifest (expanded)

{
  "$schema": "../manifest.schema.json",
  "id": "docx/hyperlink-target--plain--internal",
  "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--plain--internal"
  },
  "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": "plain",
      "target": "internal"
    }
  },
  "assertions": [
    {
      "id": "has-hyperlink-internal-plain",
      "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()='Click here']]",
      "must": "exist",
      "description": "A w:hyperlink wrapping a run whose text matches the expected display text must exist."
    }
  ]
}

Fixture

Download hyperlink-target--plain--internal.docx (18.6 KB)

Reference preview

Reference (machine, page 1 PNG)

docx/hyperlink-target--plain--internal page 1 reference

Reference PDF


Download PDF Open in PDF.js

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