mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-27 04:06:52 -06:00
XLSX: keep a phoneticPr that has no rPh runs, and document the limits
si_body returned as soon as no valid run was written, so a shared string with a <phoneticPr> and no <rPh> — a shape §18.4.8 allows — lost its properties on save even though the reader keeps them. The runs and the properties are now written independently: valid runs as before, then the valid <phoneticPr> when the workbook has one; with neither, the <si> is exactly the plain t_el output. property_only_shared_string_survives_open_and_save covers read, save and reopen, and the si_body damaged-data test now expects the property-only output. The write-time checks the reader also runs (sb/eb clamped into the UTF-16 length, empty readings skipped, type/alignment filtered through the shared PHONETIC_KINDS/PHONETIC_ALIGNMENTS) were already in the head; the tests above pin them, so no further change there. Docs: the .xlsx row and the revision history now state that the shared string table holds each text once, so duplicate <si> items with the same text but different readings keep only the first reading (reported as a warning on open), and that a <phoneticPr> without runs is kept. Both parity docs' "Last updated" headers match their 2026-10-20 revision rows; the earlier rows keep their dates.
This commit is contained in:
@@ -80,6 +80,33 @@ fn a_plain_japanese_string_writes_the_plain_si() {
|
||||
assert!(!sst.contains("<rPh") && !sst.contains("<phoneticPr"), "{sst}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn property_only_shared_string_survives_open_and_save() {
|
||||
// `phoneticPr` without any `rPh` is valid (a shared string item permits it on its own), so
|
||||
// open + save must not delete it.
|
||||
let sst = r#"<?xml version="1.0"?><sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="1" uniqueCount="1"><si><t>東京</t><phoneticPr fontId="2" type="Hiragana" alignment="left"/></si></sst>"#;
|
||||
let body = r#"<sheetData><row r="1"><c r="A1" t="s"><v>0</v></c></row></sheetData>"#;
|
||||
let bytes = minimal(body, &[("xl/sharedStrings.xml", sst)], SST_RELS, "");
|
||||
let (wb, report) = read_xlsx(&bytes).unwrap();
|
||||
assert!(!report.warnings.iter().any(|w| w.contains("phonetic")), "{:?}", report.warnings);
|
||||
assert_eq!(
|
||||
wb.phonetics,
|
||||
vec![PhoneticString {
|
||||
text: "東京".into(),
|
||||
runs: vec![],
|
||||
props: Some(PhoneticProps { font_id: 2, kind: Some("Hiragana".into()), alignment: Some("left".into()) }),
|
||||
}]
|
||||
);
|
||||
let written = write_xlsx(&wb).unwrap();
|
||||
let out = part(&written, "xl/sharedStrings.xml");
|
||||
assert!(out.contains(r#"<si><t>東京</t><phoneticPr fontId="2" type="Hiragana" alignment="left"/></si>"#), "{out}");
|
||||
// A second open + save reproduces the same part.
|
||||
let (again, report) = read_xlsx(&written).unwrap();
|
||||
assert_eq!(again.phonetics, wb.phonetics);
|
||||
assert!(!report.warnings.iter().any(|w| w.contains("phonetic")), "{:?}", report.warnings);
|
||||
assert_eq!(part(&write_xlsx(&again).unwrap(), "xl/sharedStrings.xml"), out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_phonetic_runs_lose_only_the_runs() {
|
||||
// Missing `eb`, missing `sb`, `sb >= eb`, offsets past the text, junk offsets, no offsets at
|
||||
|
||||
+10
-7
@@ -398,6 +398,10 @@ pub fn t_el(s: &str) -> String {
|
||||
/// phonetic (furigana) data — its `<rPh sb="…" eb="…"><t>…</t></rPh>` runs in document order and
|
||||
/// its `<phoneticPr …/>`. Behaviour reference: ISO/IEC 29500-1 §18.4.8 `si` with §18.4.6 `rPh` and
|
||||
/// §18.4.7 `phoneticPr`. A string without phonetic data gives exactly [`t_el`]'s output.
|
||||
///
|
||||
/// The runs and the properties are independent: `<phoneticPr>` is a valid child of `<si>` on its
|
||||
/// own (§18.4.8's `CT_SharedStringItem` allows it without any `rPh`), so a string whose runs were
|
||||
/// all dropped still keeps its settings.
|
||||
pub fn si_body(text: &str, phonetic: Option<&PhoneticString>) -> String {
|
||||
let mut s = t_el(text);
|
||||
let Some(p) = phonetic else { return s };
|
||||
@@ -406,7 +410,6 @@ pub fn si_body(text: &str, phonetic: Option<&PhoneticString>) -> String {
|
||||
// `sb` before `eb`, both inside the base text, a non-empty reading; §18.18.56/57: a known enum
|
||||
// value). Damaged data is dropped, never written back invalid.
|
||||
let len = u32::try_from(text.encode_utf16().count()).unwrap_or(u32::MAX);
|
||||
let mut runs = 0usize;
|
||||
for r in &p.runs {
|
||||
let start = r.start.min(len);
|
||||
let end = r.end.min(len);
|
||||
@@ -414,10 +417,6 @@ pub fn si_body(text: &str, phonetic: Option<&PhoneticString>) -> String {
|
||||
continue;
|
||||
}
|
||||
let _ = write!(s, "<rPh sb=\"{start}\" eb=\"{end}\">{}</rPh>", t_el(&r.text));
|
||||
runs += 1;
|
||||
}
|
||||
if runs == 0 {
|
||||
return s;
|
||||
}
|
||||
if let Some(pr) = &p.props {
|
||||
let _ = write!(s, "<phoneticPr fontId=\"{}\"", pr.font_id);
|
||||
@@ -482,9 +481,13 @@ mod tests {
|
||||
};
|
||||
let s = si_body(text, Some(&damaged));
|
||||
assert_eq!(s, "<t>課きく</t><rPh sb=\"1\" eb=\"3\"><t>く</t></rPh><phoneticPr fontId=\"7\" alignment=\"center\"/>");
|
||||
// Without a single valid run, no phonetic markup at all (a lone <phoneticPr> is pointless).
|
||||
// With no run left, valid properties are still written: `<phoneticPr>` is a legal child of
|
||||
// `<si>` on its own, and Excel keeps the settings when the last reading is removed.
|
||||
let empty = PhoneticString { text: text.into(), runs: vec![], props: damaged.props.clone() };
|
||||
assert_eq!(si_body(text, Some(&empty)), t_el(text));
|
||||
assert_eq!(si_body(text, Some(&empty)), "<t>課きく</t><phoneticPr fontId=\"7\" alignment=\"center\"/>");
|
||||
// With neither runs nor properties there is nothing to write.
|
||||
let none = PhoneticString { text: text.into(), runs: vec![], props: None };
|
||||
assert_eq!(si_body(text, Some(&none)), t_el(text));
|
||||
// A known kind is written; the schema default ("none") is not invented for a missing one.
|
||||
let known = PhoneticString {
|
||||
text: text.into(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File-format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-17 · **Change:** trivial (Japanese phonetic runs — ふりがな — in XLSX shared strings survive open + save: `<rPh>` offsets and readings and `<phoneticPr>` are kept on the workbook and written back; previously trivial: formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-20 · **Change:** trivial (Japanese phonetic runs — ふりがな — in XLSX shared strings survive open + save: `<rPh>` offsets and readings and `<phoneticPr>` are kept on the workbook and written back; previously trivial: formulas over Excel's 8,192 characters, and `%` or call chains past the nesting limit, are parse errors instead of stack overflows when a file is opened) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
|
||||
Every format Excel opens or saves, with GridCraft's read and write support, fidelity and tests.
|
||||
Part of [target-app-parity.md](target-app-parity.md). **File formats: ~50% ready, 80–120 h**
|
||||
@@ -22,7 +22,7 @@ and repair-free opening remain unverified.
|
||||
|
||||
| Format | Excel | GridCraft read | GridCraft write | Fidelity / notes | Tests |
|
||||
|---|---|---|---|---|---|
|
||||
| `.xlsx` Excel Workbook (transitional) | R/W, native | yes | yes | Cells, shared/array/dynamic formulas, shared strings (with their Japanese phonetic runs — ふりがな — kept through open + save: `<rPh>` offsets and readings and `<phoneticPr>` are written back next to the same string), styles, themes, merges, CF, validation, tables, comments and notes, hyperlinks, names, freeze panes, outline, print settings, pictures, shapes, charts (simplified, see [chart-parity.md](chart-parity.md)), sparklines, PivotTables (caches and tables), protection, calc settings. **Kept unchanged on save (pass-through, `passthrough.rs`):** custom document properties, ribbon customisation and add-in task panes, custom XML, links to external workbooks (never followed or refreshed), data connections, sheet query tables, printer settings (#324), form controls, ActiveX controls, OLE objects and sheet backgrounds (with their VML shapes), and chart sheets (#727: the sheet, drawing, chart, style and colour parts are written back unchanged in their place, and left out when a sheet they plot was deleted or renamed or a worksheet took their name; they can't be edited or created); dropped when their sheet is deleted or their parts point at content GridCraft rewrites. **Not preserved:** slicers and slicer caches, timelines, data model, query tables attached to tables, sheet and workbook `extLst` entries we don't model, chart details; Strict Open XML files keep nothing. Never checked in Excel across a corpus: repair prompts unknown. 100 MB files fail (#175) | `roundtrip.rs`, `fixtures.rs`, `malformed.rs`, `phonetic_tests.rs`, `pivot_tests.rs` |
|
||||
| `.xlsx` Excel Workbook (transitional) | R/W, native | yes | yes | Cells, shared/array/dynamic formulas, shared strings (with their Japanese phonetic runs — ふりがな — kept through open + save: `<rPh>` offsets and readings and `<phoneticPr>` are written back next to the same string; the table holds each text once, so duplicate `<si>` items whose text is the same but whose readings differ keep only the first reading, with a warning on open), styles, themes, merges, CF, validation, tables, comments and notes, hyperlinks, names, freeze panes, outline, print settings, pictures, shapes, charts (simplified, see [chart-parity.md](chart-parity.md)), sparklines, PivotTables (caches and tables), protection, calc settings. **Kept unchanged on save (pass-through, `passthrough.rs`):** custom document properties, ribbon customisation and add-in task panes, custom XML, links to external workbooks (never followed or refreshed), data connections, sheet query tables, printer settings (#324), form controls, ActiveX controls, OLE objects and sheet backgrounds (with their VML shapes), and chart sheets (#727: the sheet, drawing, chart, style and colour parts are written back unchanged in their place, and left out when a sheet they plot was deleted or renamed or a worksheet took their name; they can't be edited or created); dropped when their sheet is deleted or their parts point at content GridCraft rewrites. **Not preserved:** slicers and slicer caches, timelines, data model, query tables attached to tables, sheet and workbook `extLst` entries we don't model, chart details; Strict Open XML files keep nothing. Never checked in Excel across a corpus: repair prompts unknown. 100 MB files fail (#175) | `roundtrip.rs`, `fixtures.rs`, `malformed.rs`, `phonetic_tests.rs`, `pivot_tests.rs` |
|
||||
| `.xlsm` Macro-Enabled Workbook | R/W | yes | yes, macros kept, not run | The VBA project is kept byte-for-byte and written back only into `.xlsm`/`.xltm` (#6); macros never run. Saved with the macro-enabled workbook content type | `roundtrip.rs` (`vba_project_roundtrip`, `package_kind_sets_workbook_content_type`), engine `io.rs` (`macros_are_saved_only_into_xlsm`) |
|
||||
| `.xltx` / `.xltm` templates | R/W | yes | partial | Saved with the template (and macro-enabled template) content type, chosen by extension; `.xltm` keeps a VBA project, `.xltx` drops it; "new from template" behaviour missing | `roundtrip.rs` (`package_kind_sets_workbook_content_type`), engine `io.rs` |
|
||||
| `.xlsb` Binary Workbook | R/W | **values only** (#101) | no | Names, visibility, numbers, text, booleans, errors, merges, cached formula results; no formulas, formatting, charts, names | `xlsb_tests.rs` |
|
||||
@@ -82,7 +82,7 @@ refresh. Counted under features (Get & Transform, ~10% ready), not here.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-20 | trivial | XLSX shared strings: Japanese phonetic runs (ふりがな, `<rPh sb="…" eb="…">` with `<phoneticPr>`) are read into the workbook and written back next to the string with the same text, so an open + save no longer deletes them; bad offsets and readings are dropped with a warning (`phonetic_tests.rs`) |
|
||||
| 2026-10-20 | trivial | XLSX shared strings: Japanese phonetic runs (ふりがな, `<rPh sb="…" eb="…">` with `<phoneticPr>`) are read into the workbook and written back next to the string with the same text, so an open + save no longer deletes them; bad offsets and readings are dropped with a warning, a `<phoneticPr>` without runs is kept on its own, and when duplicate `<si>` items share a text but not a reading only the first reading is written (reported on open) (`phonetic_tests.rs`) |
|
||||
| 2026-10-19 | minor | .xls import skips dialog sheets (WsBool `fDialog`), which share the worksheet BOF type and were read as empty worksheets |
|
||||
| 2026-10-19 | minor | `.xls` import reads WINDOW2: right-to-left sheets (common in older Arabic and Hebrew files) open right-to-left, and hidden gridlines, headings and zero values and Show Formulas carry over |
|
||||
| 2026-10-19 | trivial | XLSX text boxes and shapes keep right-to-left paragraph direction (`a:pPr rtl`, ECMA-376 Part 1 §21.1.2.2.7): read from the first paragraph and written on every paragraph; output without it is unchanged |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Localization parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (Japanese phonetic runs (ふりがな) in XLSX shared strings are preserved through open + save; they are still not displayed) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-20 · **Change:** minor (Japanese phonetic runs (ふりがな) in XLSX shared strings are preserved through open + save; they are still not displayed) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
|
||||
Per-language status against Excel, which ships its UI in dozens of languages (Windows language
|
||||
packs cover 100+), with localized function names, argument separators, number and date formats,
|
||||
@@ -83,7 +83,7 @@ language (none so far) and of localized function names.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-20 | minor | Japanese: furigana (`rPh` phonetic runs and `<phoneticPr>` in `xl/sharedStrings.xml`) is no longer deleted by an open + save; it is kept on the workbook and written back. Display of the readings is still missing (`crates/xlsx/src/tests/phonetic_tests.rs`) |
|
||||
| 2026-10-20 | minor | Japanese: furigana (`rPh` phonetic runs and `<phoneticPr>` in `xl/sharedStrings.xml`) is no longer deleted by an open + save; it is kept on the workbook and written back (a `<phoneticPr>` without runs is kept too, while duplicate texts with different readings keep only the first, reported on open). Display of the readings is still missing (`crates/xlsx/src/tests/phonetic_tests.rs`) |
|
||||
| 2026-10-19 | minor | Defined-name length counts characters, not bytes: Arabic names up to 255 letters are accepted, and Create from Selection keeps long Arabic headers |
|
||||
| 2026-10-19 | minor | Table names are limited to 255 Unicode characters |
|
||||
| 2026-10-19 | minor | Right-to-left sheets in legacy `.xls` files (common in older Arabic workbooks) open right-to-left |
|
||||
|
||||
Reference in New Issue
Block a user