diff --git a/crates/xlsx/src/tests/phonetic_tests.rs b/crates/xlsx/src/tests/phonetic_tests.rs index 566057fcc..37f37ed2b 100644 --- a/crates/xlsx/src/tests/phonetic_tests.rs +++ b/crates/xlsx/src/tests/phonetic_tests.rs @@ -80,6 +80,33 @@ fn a_plain_japanese_string_writes_the_plain_si() { assert!(!sst.contains("東京"#; + let body = r#"0"#; + 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#"東京"#), "{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 diff --git a/crates/xlsx/src/xml.rs b/crates/xlsx/src/xml.rs index 8f10f1bc0..c6880de31 100644 --- a/crates/xlsx/src/xml.rs +++ b/crates/xlsx/src/xml.rs @@ -398,6 +398,10 @@ pub fn t_el(s: &str) -> String { /// phonetic (furigana) data — its `…` runs in document order and /// its ``. 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: `` is a valid child of `` 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, "{}", t_el(&r.text)); - runs += 1; - } - if runs == 0 { - return s; } if let Some(pr) = &p.props { let _ = write!(s, "課きくく"); - // Without a single valid run, no phonetic markup at all (a lone is pointless). + // With no run left, valid properties are still written: `` is a legal child of + // `` 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)), "課きく"); + // 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(), diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 14333b7de..0909fa091 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -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: `` offsets and readings and `` 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: `` offsets and readings and `` 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: `` offsets and readings and `` 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: `` offsets and readings and `` are written back next to the same string; the table holds each text once, so duplicate `` 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 (ふりがな, `` with ``) 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 (ふりがな, `` with ``) 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 `` without runs is kept on its own, and when duplicate `` 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 | diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 14ac5a66f..dcf22c21a 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -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 `` 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 `` in `xl/sharedStrings.xml`) is no longer deleted by an open + save; it is kept on the workbook and written back (a `` 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 |