Worksheet-level <hyperlinks><hyperlink/></hyperlinks> entries. External cases emit a sibling <Relationship Type='.../hyperlink' Target='<url>' TargetMode='External'/> in xl/worksheets/_rels/sheet1.xml.rels. Internal-location cases (e.g. Sheet2!A1) emit NO relationship -- the link lives entirely in the cell's @location attribute.
Library verdicts: python-xlsx: — xlsxjs: —
| Feature id | xlsx/hyperlink-cell--internal-location |
|---|---|
| Format | xlsx |
| Category | references |
| Family | xlsx/hyperlink-cell |
| Axis values | variant=internal-location |
| Spec | ecma-376-5-part-1 § 18.3.1.48 x:hyperlink |
| ID / part | Predicate | XPath | python-xlsx | xlsxjs |
|---|---|---|---|---|
hyperlink-element-present-internal-locationxl/worksheets/sheet1.xml | existThe worksheet must carry a <hyperlinks>/<hyperlink ref='A1'/> entry. | //x:hyperlinks/x:hyperlink[@ref='A1'] | — | — |
hyperlink-location-internal-locationxl/worksheets/sheet1.xml | equal = Sheet2!A1For the internal-location variant @location holds the in-workbook target (e.g. 'Sheet2!A1'). For the external variant @location is absent, so string() returns the empty string. | string(//x:hyperlink[@ref='A1']/@location) | — | — |
hyperlink-rid-presence-internal-locationxl/worksheets/sheet1.xml | absentThe external variant's hyperlink element must carry an r:id (resolved against the sibling rels file). The internal-location variant must NOT carry an r:id -- the link target lives entirely in @location. | //x:hyperlink[@ref='A1'][@r:id] | — | — |
hyperlink-location-attr-presence-internal-locationxl/worksheets/sheet1.xml | existOnly the internal-location variant emits the @location attribute; the external variant routes through the rels file via @r:id. | //x:hyperlink[@ref='A1'][@location] | — | — |
rel-target-internal-locationxl/worksheets/sheet1.xml | equal = External variant: the sibling rels file must carry one hyperlink rel with the expected @Target. Internal-location variant: the rels file is absent, so this assertion is routed at the worksheet xml itself (which has no pr:Relationship nodes), yielding '' as expected. | string(//pr:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink']/@Target) | — | — |
rel-targetmode-internal-locationxl/worksheets/sheet1.xml | equal = External variant: TargetMode must be 'External'. Internal-location variant: no rel exists, so string() yields ''. | string(//pr:Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink']/@TargetMode) | — | — |
No render assertions declared.
scripts/gen_xlsx_hyperlink_cell.py
#!/usr/bin/env python3
"""Generate ``fixtures/xlsx/hyperlink-cell--<variant>.xlsx``.
Parameterised across the 2 variants described by
``features/xlsx/hyperlink-cell.json``:
* ``external`` - cell A1 links to https://example.com/ via a
worksheet-level hyperlink rel (TargetMode='External').
* ``internal-location`` - cell A1 links to Sheet2!A1 via the @location
attribute; no rel is emitted.
.. versionadded:: 2026.05.0
"""
from __future__ import annotations
import argparse
import datetime as _dt
from pathlib import Path
from xlsx import Workbook
from xlsx.worksheet.hyperlink import Hyperlink
_REPO_ROOT = Path(__file__).resolve().parent.parent
_REPRODUCIBLE_DT = _dt.datetime(2000, 1, 1, 0, 0, 0)
def _write(variant: str, out: Path) -> None:
wb = Workbook()
ws = wb.active
# The default sheet title is "Sheet" in python-xlsx; rename to "Sheet1"
# so the internal-location string ("Sheet2!A1") parses uniformly even
# if someone re-opens the file.
ws.title = "Sheet1"
ws["A1"] = "Click A1"
ws2 = wb.create_sheet("Sheet2")
ws2["A1"] = "target"
if variant == "external":
# Cell-level .hyperlink = <str> shortcut: builds an External-mode
# relationship + a <hyperlink ref=... r:id=.../> entry.
ws["A1"].hyperlink = "https://example.com/"
elif variant == "internal-location":
# @location shape: no rel, link target is internal.
ws["A1"].hyperlink = Hyperlink(
ref="A1", location="Sheet2!A1", display="Go to Sheet2",
)
else:
raise ValueError(f"Unknown --variant {variant!r}")
out.parent.mkdir(parents=True, exist_ok=True)
wb.save(out, zip_date_time=_REPRODUCIBLE_DT)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--variant", required=True, choices=("external", "internal-location")
)
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())
{
"$schema": "../manifest.schema.json",
"id": "xlsx/hyperlink-cell--internal-location",
"kind": "literal",
"title": "Cell-level hyperlink (external + internal-location)",
"format": "xlsx",
"category": "references",
"summary": "Worksheet-level <hyperlinks><hyperlink/></hyperlinks> entries. External cases emit a sibling <Relationship Type='.../hyperlink' Target='<url>' TargetMode='External'/> in xl/worksheets/_rels/sheet1.xml.rels. Internal-location cases (e.g. Sheet2!A1) emit NO relationship -- the link lives entirely in the cell's @location attribute.",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "18.3.1.48",
"element": "x:hyperlink",
"rnc_reference": "spec/ecma-376-5/part-1/rnc/SpreadsheetML.rnc",
"xsd_reference": "spec/ecma-376-5/part-1/xsd/sml.xsd",
"notes": "xlsx hyperlinks are worksheet-scoped: the <hyperlinks> block sits inside a <worksheet> and each <hyperlink> entry carries either an r:id (external URL, resolved via the per-sheet rels file) or a @location (internal location, e.g. 'Sheet2!A1'). This is structurally distinct from docx/pptx where hyperlinks are inline within text. Internal links must NOT emit a hyperlink relationship -- they live entirely in the worksheet XML."
},
"fixtures": {
"machine": "xlsx/hyperlink-cell--internal-location"
},
"generator": {
"python": "scripts/gen_xlsx_hyperlink_cell.py",
"arg_template": "--variant {variant.id} --out fixtures/xlsx/hyperlink-cell--{variant.id}.xlsx"
},
"_expansion": {
"parent_id": "xlsx/hyperlink-cell",
"bindings": {
"variant": "internal-location"
}
},
"assertions": [
{
"id": "hyperlink-element-present-internal-location",
"part": "xl/worksheets/sheet1.xml",
"namespaces": {
"x": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
},
"xpath": "//x:hyperlinks/x:hyperlink[@ref='A1']",
"must": "exist",
"description": "The worksheet must carry a <hyperlinks>/<hyperlink ref='A1'/> entry."
},
{
"id": "hyperlink-location-internal-location",
"part": "xl/worksheets/sheet1.xml",
"namespaces": {
"x": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
},
"xpath": "string(//x:hyperlink[@ref='A1']/@location)",
"must": "equal",
"value": "Sheet2!A1",
"description": "For the internal-location variant @location holds the in-workbook target (e.g. 'Sheet2!A1'). For the external variant @location is absent, so string() returns the empty string."
},
{
"id": "hyperlink-rid-presence-internal-location",
"part": "xl/worksheets/sheet1.xml",
"namespaces": {
"x": "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
},
"xpath": "//x:hyperlink[@ref='A1'][@r:id]",
"must": "absent",
"description": "The external variant's hyperlink element must carry an r:id (resolved against the sibling rels file). The internal-location variant must NOT carry an r:id -- the link target lives entirely in @location."
},
{
"id": "hyperlink-location-attr-presence-internal-location",
"part": "xl/worksheets/sheet1.xml",
"namespaces": {
"x": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
},
"xpath": "//x:hyperlink[@ref='A1'][@location]",
"must": "exist",
"description": "Only the internal-location variant emits the @location attribute; the external variant routes through the rels file via @r:id."
},
{
"id": "rel-target-internal-location",
"part": "xl/worksheets/sheet1.xml",
"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": "",
"description": "External variant: the sibling rels file must carry one hyperlink rel with the expected @Target. Internal-location variant: the rels file is absent, so this assertion is routed at the worksheet xml itself (which has no pr:Relationship nodes), yielding '' as expected."
},
{
"id": "rel-targetmode-internal-location",
"part": "xl/worksheets/sheet1.xml",
"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": "",
"description": "External variant: TargetMode must be 'External'. Internal-location variant: no rel exists, so string() yields ''."
}
]
}
No rendered reference is available for this case.