xlsx/hyperlink-cell--internal-location

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

Metadata

Feature idxlsx/hyperlink-cell--internal-location
Formatxlsx
Categoryreferences
Familyxlsx/hyperlink-cell
Axis valuesvariant=internal-location
Spececma-376-5-part-1 § 18.3.1.48 x:hyperlink

XPath assertions

ID / partPredicateXPathpython-xlsxxlsxjs
hyperlink-element-present-internal-location
xl/worksheets/sheet1.xml
exist
The worksheet must carry a <hyperlinks>/<hyperlink ref='A1'/> entry.
//x:hyperlinks/x:hyperlink[@ref='A1']
hyperlink-location-internal-location
xl/worksheets/sheet1.xml
equal = Sheet2!A1
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.
string(//x:hyperlink[@ref='A1']/@location)
hyperlink-rid-presence-internal-location
xl/worksheets/sheet1.xml
absent
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.
//x:hyperlink[@ref='A1'][@r:id]
hyperlink-location-attr-presence-internal-location
xl/worksheets/sheet1.xml
exist
Only 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-location
xl/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-location
xl/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)

Render assertions

No render assertions declared.

Generator source

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

Manifest (expanded)

{
  "$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 ''."
    }
  ]
}

Fixture

Download hyperlink-cell--internal-location.xlsx (5.2 KB)

Reference preview

No rendered reference is available for this case.

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