docx/hyperlink-target-modes--mailto

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

Metadata

Feature iddocx/hyperlink-target-modes--mailto
Formatdocx
Categoryreferences
Familydocx/hyperlink-target-modes
Axis valuesscheme=mailto
Spececma-376-5-part-1 § 17.16.22 w:hyperlink

XPath assertions

ID / partPredicateXPathpython-docxdocxjs
rel-target-equals-mailto
word/_rels/document.xml.rels
equal = mailto:someone@example.com?subject=Hi
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 ''.
string(//pr:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink']/@Target)
rel-targetmode-mailto
word/_rels/document.xml.rels
equal = External
External 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-mailto
word/document.xml
exist
Every w:hyperlink must carry either an r:id (external) or a w:anchor (internal).
//w:hyperlink[@r:id or @w:anchor]

Render assertions

No render assertions declared.

Generator source

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

Manifest (expanded)

{
  "$schema": "../manifest.schema.json",
  "id": "docx/hyperlink-target-modes--mailto",
  "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--mailto"
  },
  "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": "mailto"
    }
  },
  "assertions": [
    {
      "id": "rel-target-equals-mailto",
      "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": "mailto:someone@example.com?subject=Hi",
      "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-mailto",
      "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-mailto",
      "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)."
    }
  ]
}

Fixture

Download hyperlink-target-modes--mailto.docx (18.6 KB)

Reference preview

No rendered reference is available for this case.

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