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: —
| Feature id | xlsx/combining-marks-and-zwj--grave-accent-decomposed |
|---|---|
| Format | xlsx |
| Category | cell-data |
| Family | xlsx/combining-marks-and-zwj |
| Axis values | case=grave-accent-decomposed |
| Spec | ecma-376-5-part-1 § 18.4.8 sst |
| ID / part | Predicate | XPath | python-xlsx | xlsxjs |
|---|---|---|---|---|
shared-string-preserves-grave-accent-decomposedxl/sharedStrings.xml | existAfter 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(., 'è')] | — | — |
No render assertions declared.
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())
{
"$schema": "../manifest.schema.json",
"id": "xlsx/combining-marks-and-zwj--grave-accent-decomposed",
"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--grave-accent-decomposed"
},
"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": "grave-accent-decomposed"
}
},
"assertions": [
{
"id": "shared-string-preserves-grave-accent-decomposed",
"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."
}
]
}
No rendered reference is available for this case.