xlsx/combining-marks-and-zwj--emoji-skin-tone-modifier

A cell containing combining diacritics or a zero-width-joiner emoji sequence must survive load+save with no code-point loss. Each fixture is produced by a save → load → save cycle using python-xlsx.

Library verdicts: python-xlsx: — xlsxjs: —

Metadata

Feature idxlsx/combining-marks-and-zwj--emoji-skin-tone-modifier
Formatxlsx
Categorycell-data
Familyxlsx/combining-marks-and-zwj
Axis valuescase=emoji-skin-tone-modifier
Spececma-376-5-part-1 § 18.4.8 sst

XPath assertions

ID / partPredicateXPathpython-xlsxxlsxjs
shared-string-preserves-emoji-skin-tone-modifier
xl/sharedStrings.xml
exist
After a load + save cycle, the sharedStrings.xml <t> element must still contain the exact combining-mark / ZWJ sequence written by the generator.
//x:si/x:t[contains(., '👋🏽')]

Render assertions

No render assertions declared.

Generator source

scripts/gen_combining_marks_and_zwj_xlsx.py

#!/usr/bin/env python3
"""Generate ``fixtures/xlsx/combining-marks-and-zwj--<case>.xlsx``.

Parameterised generator for the ``xlsx/combining-marks-and-zwj`` family.
Mirrors the docx family but exercises python-xlsx. Each case writes a
cell, saves, reloads via python-xlsx's reader, and re-saves. The
committed fixture is the output of the second save.

Usage::

    python scripts/gen_combining_marks_and_zwj_xlsx.py \\
        --case grave-accent-decomposed \\
        --out fixtures/xlsx/combining-marks-and-zwj--grave-accent-decomposed.xlsx
"""

from __future__ import annotations

import argparse
import datetime as _dt
import tempfile
from pathlib import Path

from xlsx import Workbook, load_workbook

_REPO_ROOT = Path(__file__).resolve().parent.parent
_REPRODUCIBLE_DT = _dt.datetime(2000, 1, 1, 0, 0, 0)

_PAYLOADS: dict[str, str] = {
    "grave-accent-decomposed":  "decomposed è è è",
    "acute-accent-decomposed":  "decomposed á á á",
    "tilde-decomposed":         "decomposed ñ ñ ñ",
    "vietnamese-stack":         "vn ả̂ ẩ",
    "devanagari-virama":        "conjunct क्ष",
    "emoji-zwj-family":         "family \U0001F468‍\U0001F469‍\U0001F466",
    "emoji-zwj-rainbow-flag":   "pride \U0001F3F3️‍\U0001F308",
    "emoji-skin-tone-modifier": "wave \U0001F44B\U0001F3FD",
}


def _write(case: str, out: Path) -> None:
    if case not in _PAYLOADS:
        raise SystemExit(
            f"Unknown case id {case!r}; expected one of {sorted(_PAYLOADS)}"
        )
    payload = _PAYLOADS[case]

    # First save.
    wb = Workbook()
    wb.active["A1"] = payload
    with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as first:
        first_path = Path(first.name)
    wb.save(first_path, zip_date_time=_REPRODUCIBLE_DT)

    # Load + save.
    wb2 = load_workbook(first_path)
    out.parent.mkdir(parents=True, exist_ok=True)
    wb2.save(out, zip_date_time=_REPRODUCIBLE_DT)

    first_path.unlink(missing_ok=True)


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--case", 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
    _write(args.case, out_path)
    print(out_path)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Manifest (expanded)

{
  "$schema": "../manifest.schema.json",
  "id": "xlsx/combining-marks-and-zwj--emoji-skin-tone-modifier",
  "kind": "literal",
  "title": "Combining marks and ZWJ sequences (round-trip)",
  "format": "xlsx",
  "category": "cell-data",
  "summary": "A cell containing combining diacritics or a zero-width-joiner emoji sequence must survive load+save with no code-point loss. Each fixture is produced by a save → load → save cycle using python-xlsx.",
  "spec": {
    "source": "ecma-376-5-part-1",
    "clause": "18.4.8",
    "element": "sst",
    "rnc_reference": "spec/ecma-376-5/part-1/rnc/SpreadsheetML.rnc",
    "xsd_reference": "spec/ecma-376-5/part-1/xsd/sml.xsd",
    "notes": "Excel writes shared-string entries as <si><t>...</t></si>. Combining marks, stacked marks, ZWJ (U+200D), VS16 (U+FE0F), and emoji modifier sequences must all survive the load+save boundary as exact code-point sequences — neither NFC-normalised nor collapsed to separate code points. python-xlsx encodes non-ASCII as numeric character references on the wire (&#NNNN;) but that's spec-legal; what matters is that the XML infoset parses back to the same code points. The generator performs a save-then-reload-then-save cycle so the committed fixture exercises both the reader and the writer."
  },
  "fixtures": {
    "machine": "xlsx/combining-marks-and-zwj--emoji-skin-tone-modifier"
  },
  "generator": {
    "python": "scripts/gen_combining_marks_and_zwj_xlsx.py",
    "arg_template": "--case {case.id} --out fixtures/xlsx/combining-marks-and-zwj--{case.id}.xlsx"
  },
  "_expansion": {
    "parent_id": "xlsx/combining-marks-and-zwj",
    "bindings": {
      "case": "emoji-skin-tone-modifier"
    }
  },
  "assertions": [
    {
      "id": "shared-string-preserves-emoji-skin-tone-modifier",
      "part": "xl/sharedStrings.xml",
      "namespaces": {
        "x": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
      },
      "xpath": "//x:si/x:t[contains(., '👋🏽')]",
      "must": "exist",
      "description": "After a load + save cycle, the sharedStrings.xml <t> element must still contain the exact combining-mark / ZWJ sequence written by the generator."
    }
  ]
}

Fixture

Download combining-marks-and-zwj--emoji-skin-tone-modifier.xlsx (4.7 KB)

Reference preview

No rendered reference is available for this case.

Spec notes

Excel writes shared-string entries as <si><t>...</t></si>. Combining marks, stacked marks, ZWJ (U+200D), VS16 (U+FE0F), and emoji modifier sequences must all survive the load+save boundary as exact code-point sequences — neither NFC-normalised nor collapsed to separate code points. python-xlsx encodes non-ASCII as numeric character references on the wire (&#NNNN;) but that's spec-legal; what matters is that the XML infoset parses back to the same code points. The generator performs a save-then-reload-then-save cycle so the committed fixture exercises both the reader and the writer.