From 0aded47e93f05b84469d42cd8c854b29df82c495 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 23 Oct 2026 10:18:03 -0400 Subject: [PATCH 01/20] test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff --- apps/wordcraft/src/file_dialogs.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/wordcraft/src/file_dialogs.rs b/apps/wordcraft/src/file_dialogs.rs index 002354ce2..1d4cb7b0b 100644 --- a/apps/wordcraft/src/file_dialogs.rs +++ b/apps/wordcraft/src/file_dialogs.rs @@ -258,7 +258,9 @@ mod tests { fn an_instant_cancel_retries_then_falls_back() { let quick = Duration::from_millis(200); let slow_cancel = || async { - std::thread::sleep(Duration::from_millis(250)); + // The test polls this future by hand, with no async runtime, so a blocking sleep + // models a slow portal response. + std::thread::sleep(Duration::from_millis(250)); // NOSONAR Vec::::new() }; assert_eq!(pick(async { vec![1u8] }, async { vec![2] }, quick), Ok(vec![1])); From 462ecc8770279c0cf556c85e7d04e93ec6bac5e4 Mon Sep 17 00:00:00 2001 From: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:25:42 +0800 Subject: [PATCH 02/20] Engine: no dead-code warnings on wasm (#3542) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas --- crates/engine/src/cmd/citations.rs | 1 + crates/engine/src/cmd/user_templates.rs | 3 +++ crates/engine/src/speech.rs | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/citations.rs b/crates/engine/src/cmd/citations.rs index b646bd459..6c2cadd41 100644 --- a/crates/engine/src/cmd/citations.rs +++ b/crates/engine/src/cmd/citations.rs @@ -529,6 +529,7 @@ fn auto_tag(doc: &Document, src: &Source) -> String { } /// The author of a source for matching untagged entries: the editor when there is no author. +#[cfg(not(target_arch = "wasm32"))] fn by_author(x: &Source) -> &str { if x.author.is_empty() { x.editor.as_str() } else { x.author.as_str() } } diff --git a/crates/engine/src/cmd/user_templates.rs b/crates/engine/src/cmd/user_templates.rs index db944578e..9180a8fe3 100644 --- a/crates/engine/src/cmd/user_templates.rs +++ b/crates/engine/src/cmd/user_templates.rs @@ -23,6 +23,7 @@ pub const MAX_TEMPLATES: usize = 500; /// Most category folders looked into. pub const MAX_CATEGORIES: usize = 64; /// Most directory entries looked at in one folder. +#[cfg(not(target_arch = "wasm32"))] const MAX_ENTRIES: usize = 10_000; /// Longest folder path taken, in bytes. const MAX_DIR: usize = 32 * 1024; @@ -186,6 +187,7 @@ fn entries(dir: &std::path::Path) -> (Vec, Vec String { let mut label = name; while is_template_name(label) { @@ -355,6 +357,7 @@ mod tests { /// .dotx and .dotm templates show their names the same way, without the extension; two that /// would look alike in one category show their file names. #[test] + #[cfg(not(target_arch = "wasm32"))] fn labels_drop_the_extension_alike_and_tell_twins_apart() { let d = scratch("labels"); template(&d.join("Service contract.dotm"), "a", ""); diff --git a/crates/engine/src/speech.rs b/crates/engine/src/speech.rs index 30d447f78..bbaa5fdbf 100644 --- a/crates/engine/src/speech.rs +++ b/crates/engine/src/speech.rs @@ -249,6 +249,7 @@ pub fn speech_language(text: &str, explicit: Option<&str>, bidi: Option<&str>) - /// The primary language subtag of a plain tag, lowercased (`ru-RU` gives `ru`). `None` for tags /// without a two- or three-letter language, such as Word's `x-none`. +#[cfg(any(unix, windows, test))] fn primary_tag(tag: &str) -> Option { let t = plain_tag(tag)?; t.split('-').next().filter(|p| (2..=3).contains(&p.len()) && p.chars().all(|c| c.is_ascii_alphabetic())).map(str::to_ascii_lowercase) @@ -256,7 +257,7 @@ fn primary_tag(tag: &str) -> Option { /// The language to ask the speech program for, or `None` to keep its default voice: only when /// `lang` isn't the language the default voice speaks (`default`, a primary subtag) already. -#[cfg(any(not(target_os = "windows"), test))] +#[cfg(any(unix, test))] fn voice_language(lang: Option<&str>, default: &str) -> Option { lang.and_then(primary_tag).filter(|p| !p.eq_ignore_ascii_case(default)) } From 957b1c3e6e3432787b2c0892e79918cf9f357587 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:31:21 +0200 Subject: [PATCH 03/20] Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 --- crates/formats/src/odt.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/formats/src/odt.rs b/crates/formats/src/odt.rs index a5d440d91..e1f0fd886 100644 --- a/crates/formats/src/odt.rs +++ b/crates/formats/src/odt.rs @@ -55,6 +55,7 @@ fn x(s: &str) -> String { '>' => o.push_str(">"), '"' => o.push_str("""), '\'' => o.push_str("'"), + '\u{FFFE}' | '\u{FFFF}' => {} c if c.is_control() && !matches!(c, '\t' | '\n') => {} _ => o.push(c), } @@ -2725,6 +2726,11 @@ fn apply_layout(a: &Attrs, sec: &mut SectionProps) { mod tests { use super::*; + #[test] + fn escaper_drops_noncharacters() { + assert_eq!(x("a\u{FFFF}b\u{FFFE}c\u{1}d\te"), "abcd\te"); + } + #[test] fn lengths() { assert_eq!(length("1in"), Some(72.0)); From 51e364fea445fcaa140a42e8e4c024bfe4448122 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:31:30 +0200 Subject: [PATCH 04/20] Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 --- crates/docx/src/read/freeform.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/docx/src/read/freeform.rs b/crates/docx/src/read/freeform.rs index 4881cf7a3..457646581 100644 --- a/crates/docx/src/read/freeform.rs +++ b/crates/docx/src/read/freeform.rs @@ -256,6 +256,10 @@ fn vml_commands(path: &str) -> Vec<(String, Vec)> { }; while let Some(c) = chars.next() { if c.is_ascii_alphabetic() { + // Cap the commands too, so a path of bare letters can't build a million entries. + if out.len() >= MAX_VML_PARAMS { + break; + } if !has_num { push(&mut out, 0.0); } @@ -361,6 +365,13 @@ mod tests { crate::xml::parse(xml.as_bytes()).expect("parse spPr") } + #[test] + fn a_vml_path_of_bare_letters_builds_a_bounded_command_list() { + let path = "x".repeat(MAX_VML_PATH); + let n = vml_commands(&path).len(); + assert!(n <= MAX_VML_PARAMS, "{n} commands"); + } + #[test] fn a_bezier_segment_with_many_points_is_bounded() { // One cubicBezTo stuffed with 50 000 points must not make de Casteljau quadratic: the From 66ff2afc4d26e8a491de5d1eb5fbcd01cbf01bb6 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:31:39 +0200 Subject: [PATCH 05/20] Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 --- crates/doc/src/locale_number.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/doc/src/locale_number.rs b/crates/doc/src/locale_number.rs index 569ab0a4b..072b50a2b 100644 --- a/crates/doc/src/locale_number.rs +++ b/crates/doc/src/locale_number.rs @@ -102,6 +102,9 @@ pub fn parse_number(s: &str, lang: Option<&str>) -> Option { return None; } let v: f64 = format!("0{int_digits}.{frac_digits}0").parse().ok()?; + if !v.is_finite() { + return None; + } Some(if neg { -v } else { v }) } @@ -151,6 +154,15 @@ mod tests { assert_eq!(format_number(-0.001, pl), "0,00", "a rounded zero has no sign"); } + #[test] + fn overflowing_digit_strings_are_not_numbers() { + assert_eq!(parse_number(&"1".repeat(350), None), None); + assert_eq!(parse_number(&"9".repeat(400), Some("pl-PL")), None); + assert_eq!(parse_number(&format!("-{}", "1".repeat(350)), None), None); + let big = parse_number(&"1".repeat(300), None); + assert!(big.is_some_and(f64::is_finite), "{big:?}"); + } + #[test] fn cuban_and_peruvian_numbers_use_decimal_points() { for lang in [Some("es-CU"), Some("es-PE")] { From 32f8e2c2ad77bf98a9631ab86eff70db7db8aae4 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:31:48 +0200 Subject: [PATCH 06/20] =?UTF-8?q?Fix=20spurious=20=E9=9B=B6=20after=20a=20?= =?UTF-8?q?group's=20trailing=20zeros=20in=20Chinese=20numerals=20(#3745)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 --- crates/doc/src/cjk_numbers.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/doc/src/cjk_numbers.rs b/crates/doc/src/cjk_numbers.rs index 64a1757fa..f3b6585c4 100644 --- a/crates/doc/src/cjk_numbers.rs +++ b/crates/doc/src/cjk_numbers.rs @@ -74,6 +74,8 @@ impl Counting { out.push(u); } } + // Trailing zeros of a group are not a gap: the next group starts a new place. + zero_pending = false; match gi { 0 => out.extend(self.myriads.get(1)), 1 => out.extend(self.myriads.first()), @@ -324,6 +326,17 @@ pub(crate) fn chosung(n: u32) -> String { mod tests { use super::*; + #[test] + fn chinese_no_zero_after_group_trailing_zeros() { + assert_eq!(chinese_counting_thousand(201_000), "δΊŒεδΈ‡δΈ€εƒ"); + assert_eq!(chinese_counting_thousand(120_000), "εδΊŒδΈ‡"); + assert_eq!(chinese_counting_thousand(100_100), "十万離一百"); + assert_eq!(taiwanese_counting_thousand(201_000), "δΊŒεθ¬δΈ€εƒ"); + assert_eq!(chinese_counting_thousand(100_000_001), "δΈ€δΊΏι›ΆδΈ€"); + assert_eq!(chinese_counting_thousand(100_010_000), "δΈ€δΊΏι›ΆδΈ€δΈ‡"); + assert_eq!(chinese_counting_thousand(120_000_001), "δΈ€δΊΏδΊŒεƒδΈ‡ι›ΆδΈ€"); + } + #[test] fn chinese_counting_systems() { let c = |n| chinese_counting(n); From 17bacb9a63a4f2bc4d31c2a73d81fbfaf56a1990 Mon Sep 17 00:00:00 2001 From: Joseph Anglada Date: Fri, 23 Oct 2026 10:31:56 -0400 Subject: [PATCH 07/20] picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 --- crates/engine/src/cmd/objects.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/objects.rs b/crates/engine/src/cmd/objects.rs index febd55660..66498178c 100644 --- a/crates/engine/src/cmd/objects.rs +++ b/crates/engine/src/cmd/objects.rs @@ -130,7 +130,8 @@ pub fn specs() -> Vec { } if sharpen > 0.0 { img = img.unsharpen(1.0 + sharpen / 50.0, 2); - } else if sharpen < 0.0 { + } else if sharpen < 0.0 && (-sharpen / 25.0).is_normal() { + // `blur` asserts on a subnormal width. img = img.blur(-sharpen / 25.0); } img @@ -2010,6 +2011,17 @@ mod tests { assert!(s.run("picture.crop", &json!({"left": 0.1})).is_err()); } + /// A `sharpen` too small to be a normal blur width must not reach the blur, which asserts on it. + #[test] + fn a_tiny_sharpen_value_does_not_panic() { + let mut s = Session::new(wordcraft_doc::Document::new()); + let data = super::super::insert::base64_encode(&png(20, 10)); + s.run("insert.picture", &json!({"data": data})).unwrap(); + for sharpen in [-1e-40_f64, -1e-45, -1e-38, 1e-40, -1e-30, 0.0, -0.5] { + s.run("picture.corrections", &json!({"sharpen": sharpen})).unwrap_or_else(|e| panic!("sharpen {sharpen}: {e:?}")); + } + } + /// Transparency is absolute from the original pixels: 80% then 0% is opaque again /// (it used to stay faded), and repeating a value is idempotent. Corrections and color /// re-set the same way: brightness 0 and saturation 100 restore the original. From 2d2123d2512b72e21cd71fd1d9be48969050e50c Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:32:06 +0200 Subject: [PATCH 08/20] Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 --- crates/engine/src/cmd/controls.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/controls.rs b/crates/engine/src/cmd/controls.rs index 24b2788cc..6ad46d76d 100644 --- a/crates/engine/src/cmd/controls.rs +++ b/crates/engine/src/cmd/controls.rs @@ -112,9 +112,11 @@ fn symbol_param(v: &Value, k: &str) -> Option { /// A date `YYYY-MM-DD` (a full ISO date-time is fine too): (year, month, day), checked. pub fn parse_date(t: &str) -> Option<(i64, u32, u32)> { let t = t.trim(); - let y: i64 = t.get(0..4)?.parse().ok()?; - let m: u32 = t.get(5..7)?.parse().ok()?; - let d: u32 = t.get(8..10)?.parse().ok()?; + // Digits only: `str::parse` would also accept a leading '+'. + let field = |r: std::ops::Range| t.get(r).filter(|s| s.bytes().all(|b| b.is_ascii_digit())); + let y: i64 = field(0..4)?.parse().ok()?; + let m: u32 = field(5..7)?.parse().ok()?; + let d: u32 = field(8..10)?.parse().ok()?; if t.get(4..5) != Some("-") || t.get(7..8) != Some("-") || !(1..=9999).contains(&y) || !(1..=12).contains(&m) || d == 0 || d > days_in_month(y, m) { return None; @@ -761,6 +763,15 @@ mod tests { assert_eq!(super::super::civil_from_days(days_from_civil(2000, 3, 1)), (2000, 3, 1)); } + #[test] + fn parse_date_rejects_signs_and_non_digits() { + for bad in ["2024-+1-+5", "+123-01-01", "2024-01-+5", "2024-+1-05", "-024-01-01"] { + assert_eq!(parse_date(bad), None, "{bad}"); + } + assert_eq!(parse_date("2024-01-05"), Some((2024, 1, 5))); + assert_eq!(parse_date("2024-01-05T10:00:00"), Some((2024, 1, 5))); + } + #[test] fn days_from_civil_and_days_in_month_exact_beyond_chronos_range() { // Past `NaiveDate`'s Β±262143 the 400-year cycles carry the result; values from the From fcc09bcc568de28d65bab5808c7f4d9657cc7f5b Mon Sep 17 00:00:00 2001 From: dfgodvan Date: Fri, 23 Oct 2026 17:49:07 +0300 Subject: [PATCH 09/20] Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada --- crates/layout/src/lib.rs | 3 ++- crates/layout/src/tests.rs | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 9d25e5685..904b9b618 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -3791,7 +3791,8 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block: if pl.rp.keep_lines || (pl.rp.keep_next && pb.y + height <= pb.bottom + 0.01) { let need = height + if pl.rp.keep_next { next_first_line(ctx, block, width, 0, &mut ctx.counters.clone()) } else { 0.0 }; // (Against the whole column: balanced columns grow to hold it.) - if pb.y + need > pb.bottom && !pb.at_top() && need <= pb.uncapped_bottom() - pb.top { + // With the 0.01 pt of slack the other fit checks take. + if pb.y + need > pb.bottom + 0.01 && !pb.at_top() && need <= pb.uncapped_bottom() - pb.top { pb.advance(block, body_top); pb.y += pl.rp.space_before.min(0.0); applied_before = pl.rp.space_before.min(0.0); diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index 9fe3a5106..624b05002 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -10688,3 +10688,20 @@ fn layout_cache_invalidates_on_changed_filepath() { let text_b = shown(&l_b); assert_eq!(text_b, "/path/two/a.docx", "second layout with same cache shows /path/two/a.docx"); } + +/// Keep with next takes the same 0.01 pt of slack as the other fit checks: a kept paragraph and +/// the next one's first line that fill the page to within it stay on the page. +#[test] +fn keep_with_next_fits_to_the_hundredth_of_a_point() { + use wordcraft_doc::props::LineSpacing; + let exact = + |h: f32| ParaProps { space_before: Some(0.0), space_after: Some(0.0), line_spacing: Some(LineSpacing::Exactly(h)), ..Default::default() }; + let mut d = Document::from_text("Filler\nKept\nNext"); + let body = d.last_section.page_h - d.last_section.margin_top - d.last_section.margin_bottom; + d.para_mut(StoryRef::Body, &Path::top(0)).unwrap().props = exact(body - 24.0 + 0.005); + d.para_mut(StoryRef::Body, &Path::top(1)).unwrap().props = ParaProps { keep_next: Some(true), ..exact(12.0) }; + d.para_mut(StoryRef::Body, &Path::top(2)).unwrap().props = exact(12.0); + let l = lay(&d); + assert_eq!(l.caret(&Pos::body(1, 0)).unwrap().page, 0, "the kept paragraph stays on the first page"); + assert_eq!(l.caret(&Pos::body(2, 0)).unwrap().page, 0, "and so does the next one's first line"); +} From 6f1ff2a3d7c3b499e2f8c5cef51fb11862d5720e Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:51:00 +0200 Subject: [PATCH 10/20] Parse the Flat OPC part root tag once when inheriting namespaces (#3944) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Parse the Flat OPC part root tag once when inheriting namespaces Fixes #3727 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada * #3944: drop the wall-clock assert from the long-root-tag test Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Haiku 5.5 Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Brandon Thomas Co-authored-by: Joseph Anglada Co-authored-by: dfgodvan Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> --- crates/docx/src/flat.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/crates/docx/src/flat.rs b/crates/docx/src/flat.rs index fca444c22..6c3b0bcef 100644 --- a/crates/docx/src/flat.rs +++ b/crates/docx/src/flat.rs @@ -9,7 +9,7 @@ //! or not (a repeated name), data is checked before it's copied, and nesting and elements are //! capped as the XML tree reader caps a part's. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashSet}; use base64::Engine as _; use quick_xml::events::{BytesStart, Event}; @@ -340,14 +340,21 @@ fn with_scope(frag: &[u8], scope: &[Vec<(String, String)>]) -> Vec { let rest = frag.get(open..).unwrap_or_default(); let name_end = rest.iter().position(|b| b.is_ascii_whitespace() || *b == b'>' || *b == b'/').unwrap_or(rest.len()); let tag_end = rest.iter().position(|b| *b == b'>').unwrap_or(rest.len()); - let tag = rest.get(..tag_end).unwrap_or_default(); + let tag = String::from_utf8_lossy(rest.get(1..tag_end).unwrap_or_default()); + let start = BytesStart::from_content(tag.as_ref(), name_end.saturating_sub(1)); + let declared: HashSet> = start + .attributes() + .with_checks(false) + .flatten() + .filter(|a| a.key.as_ref() == b"xmlns" || a.key.as_ref().starts_with(b"xmlns:")) + .map(|a| a.key.as_ref().to_vec()) + .collect(); let mut out = Vec::with_capacity(rest.len() + 256); out.extend_from_slice(rest.get(..name_end).unwrap_or_default()); // Innermost declarations win: walk from the data element out, skipping prefixes seen. let mut seen: Vec<&str> = Vec::new(); for (k, v) in decls.iter().rev() { - let declared = tag.windows(k.len() + 1).any(|w| w.get(..k.len()) == Some(k.as_bytes()) && matches!(w.last(), Some(b'=' | b' '))); - if declared || seen.contains(&k.as_str()) { + if declared.contains(k.as_bytes()) || seen.contains(&k.as_str()) { continue; } seen.push(k); @@ -463,6 +470,15 @@ mod tests { assert_eq!(with_scope(b"no element", &scope), b"no element".to_vec()); } + #[test] + fn inherited_namespaces_do_not_rescan_a_long_root_tag() { + let scope = vec![(0..256).map(|i| (format!("xmlns:p{i}"), format!("urn:{i}"))).collect::>()]; + let frag = format!("", " a=\"1\"".repeat(4_000_000)); + let out = with_scope(frag.as_bytes(), &scope); + let out = String::from_utf8(out).unwrap(); + assert_eq!(out.matches("xmlns:p").count(), 256); + } + #[test] fn nesting_is_capped_where_the_xml_tree_reader_stops() { let inline = |levels: usize| format!("{}{}", "".repeat(levels), "".repeat(levels)); From 413af75ded01dd61e3d6d3ed74e631e9ce0eed97 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:52:24 +0200 Subject: [PATCH 11/20] Write a text-box story shared by many shapes only once on docx save (#3983) Fixes #3653 Co-authored-by: Claude Opus 5.5 --- crates/docx/src/write/mod.rs | 4 ++++ crates/docx/src/write/story.rs | 8 ++++++-- crates/docx/tests/text_boxes.rs | 13 +++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/docx/src/write/mod.rs b/crates/docx/src/write/mod.rs index c3f02cc51..7e8601902 100644 --- a/crates/docx/src/write/mod.rs +++ b/crates/docx/src/write/mod.rs @@ -116,6 +116,9 @@ pub(crate) struct Writer<'d> { used_media: std::collections::BTreeSet, /// Bounds writing text boxes inside text boxes (as layout shows them). boxes: wordcraft_doc::BoxBudget, + /// Text box stories already written: a story shown by many shapes is written once, the other + /// shapes get an empty box, so the file doesn't grow as shapes Γ— story size. + written_boxes: std::collections::HashSet, /// Charts, diagrams and OLE objects kept from a file: the parts they need. embeds: embed::EmbedWriter<'d>, /// Linked text boxes: part id β†’ (`wps:txbx`/`wps:linkedTxbx` id, seq; 0 for the box holding @@ -175,6 +178,7 @@ pub(crate) fn write_parts(doc: &Document, flavor: Flavor) -> Result { None => w.open("wps:txbx", &[]), } w.open("w:txbxContent", &[]); - let blocks = part.blocks.clone(); - self.blocks(w, &blocks, rels, false, depth + 1); + if self.written_boxes.insert(id) { + let blocks = part.blocks.clone(); + self.blocks(w, &blocks, rels, false, depth + 1); + } else { + w.empty("w:p", &[]); + } w.close("w:txbxContent"); w.close("wps:txbx"); self.boxes.leave(); diff --git a/crates/docx/tests/text_boxes.rs b/crates/docx/tests/text_boxes.rs index dda7f614a..0040e942c 100644 --- a/crates/docx/tests/text_boxes.rs +++ b/crates/docx/tests/text_boxes.rs @@ -234,3 +234,16 @@ fn picture_in_box_keeps_its_frame() { let p = read.para(StoryRef::Body, &wordcraft_doc::Path::top(0)).unwrap(); assert!(matches!(&p.objects[0], InlineObject::Shape { w, h, .. } if *w == 144.0 && *h == 72.0)); } + +#[test] +fn story_shared_by_many_shapes_is_written_once() { + let mut d = Document::from_text("x"); + let text = "abcdefghij".repeat(5_000); + let o = text_box(&mut d, &text, TextBody::default()); + for _ in 0..500 { + d.insert_object(&wordcraft_doc::Pos::body(0, 0), o.clone(), &Default::default()).unwrap(); + } + let xml = document_xml(&wordcraft_docx::write(&d).unwrap()); + assert!(xml.len() < 2_000_000, "document.xml is {} bytes", xml.len()); + assert_eq!(xml.matches(text.as_str()).count(), 1); +} From 6f484970a95a41a877dc01a020b4432fcf251559 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:56:06 +0200 Subject: [PATCH 12/20] Keep pending formatting revisions when Style Inspector clears formatting (#3971) Fixes #3671 Co-authored-by: Claude Haiku 5.5 --- crates/engine/src/cmd/inspector.rs | 9 ++++++++- crates/engine/src/cmd/review.rs | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/inspector.rs b/crates/engine/src/cmd/inspector.rs index 8d304c9a3..30cabbab5 100644 --- a/crates/engine/src/cmd/inspector.rs +++ b/crates/engine/src/cmd/inspector.rs @@ -122,7 +122,13 @@ fn clear(s: &mut Session, v: &Value) -> CmdResult { } "paragraphFormatting" => { s.doc.format_paragraphs(&a, &b, &|p| { - *p = ParaProps { style: p.style.clone(), numbering: p.numbering, ..Default::default() }; + *p = ParaProps { + style: p.style.clone(), + numbering: p.numbering, + num_change: p.num_change.take(), + fmt_change: p.fmt_change.take(), + ..Default::default() + }; })?; } "characterStyle" => return super::format::apply(s, &|c| c.style = None), @@ -136,6 +142,7 @@ fn clear(s: &mut Session, v: &Value) -> CmdResult { lang: c.lang.clone(), no_proof: c.no_proof, rtl: c.rtl, + fmt_change: c.fmt_change.take(), ..Default::default() }; }); diff --git a/crates/engine/src/cmd/review.rs b/crates/engine/src/cmd/review.rs index 516ddd3dc..420774959 100644 --- a/crates/engine/src/cmd/review.rs +++ b/crates/engine/src/cmd/review.rs @@ -1683,6 +1683,21 @@ mod tests { assert!(s.run("review.changes", &json!({})).unwrap().as_array().unwrap().is_empty()); } + /// Clearing formatting in the Style Inspector keeps pending formatting revisions. + #[test] + fn inspector_clear_keeps_formatting_changes() { + let mut s = formatted_while_tracking(); + s.run("review.trackChanges", &json!({"value": false})).unwrap(); + s.run("styles.inspectorClear", &json!({"level": "paragraphFormatting"})).unwrap(); + assert!(body_para(&s).props.fmt_change.is_some(), "paragraph formatting change survives"); + + let mut s = formatted_while_tracking(); + s.run("review.trackChanges", &json!({"value": false})).unwrap(); + s.run("select.range", &json!({"anchor": {"block": 0, "off": 6}, "focus": {"block": 0, "off": 11}})).unwrap(); + s.run("styles.inspectorClear", &json!({"level": "characterFormatting"})).unwrap(); + assert!(body_para(&s).props_of_char(6).fmt_change.is_some(), "character formatting change survives"); + } + /// Comment anchors left in any story (the body, headers, footers, notes and text boxes), with /// the story each sits in. fn anchors(s: &Session) -> Vec<(wordcraft_doc::StoryRef, u32)> { From 1272cebf3006db8c396f536d2d3698b81d294ba0 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:04:03 +0200 Subject: [PATCH 13/20] Cap the headless ui_requests queue and keep one Paste Special request (#3997) Fixes #3641 Co-authored-by: Claude Haiku 5.5 --- crates/engine/src/cmd/chart.rs | 2 +- crates/engine/src/cmd/edit.rs | 10 +++++----- crates/engine/src/cmd/file.rs | 14 +++++++------- crates/engine/src/cmd/font.rs | 4 ++-- crates/engine/src/cmd/format.rs | 2 +- crates/engine/src/cmd/insert.rs | 6 +++--- crates/engine/src/cmd/options.rs | 2 +- crates/engine/src/cmd/page.rs | 2 +- crates/engine/src/cmd/para.rs | 4 ++-- crates/engine/src/cmd/paste.rs | 4 +++- crates/engine/src/cmd/phonetic.rs | 2 +- crates/engine/src/cmd/tools.rs | 8 ++++---- crates/engine/src/cmd/view.rs | 10 +++++----- crates/engine/src/session.rs | 11 +++++++++++ crates/engine/src/tests.rs | 13 +++++++++++++ 15 files changed, 60 insertions(+), 34 deletions(-) diff --git a/crates/engine/src/cmd/chart.rs b/crates/engine/src/cmd/chart.rs index 50321fbb0..7fd4aba1c 100644 --- a/crates/engine/src/cmd/chart.rs +++ b/crates/engine/src/cmd/chart.rs @@ -145,7 +145,7 @@ fn insert(s: &mut Session, v: &Value) -> CmdResult { let kind = chart_type(v)?; // No type and no data: Insert β€Ί Chart's picker, as the ribbon button does. if kind.is_none() && v.get("data").is_none() { - s.ui_requests.push(json!({"open": "insertChart"})); + s.push_ui_request(json!({"open": "insertChart"})); return sel_result(s); } let kind = kind.unwrap_or_default(); diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 8c890afd0..0dfb5e149 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -134,7 +134,7 @@ pub fn specs() -> Vec { t @ ("find" | "replace" | "goto") => t, other => return Err(CmdError::Params(format!("`tab` is find, replace or goto, not `{other}`"))), }; - s.ui_requests.push(json!({"open": tab})); + s.push_ui_request(json!({"open": tab})); Ok(json!({"open": tab})) }) // Word for Mac's own key is βŒƒH. @@ -603,7 +603,7 @@ fn find(s: &mut Session, v: &Value) -> CmdResult { // Word's Ctrl+F: the Navigation pane with its search box ready to type in (#712); the // Find and Replace dialog is Advanced Find. s.view.nav_pane = true; - s.ui_requests.push(json!({"focus": "navSearch"})); + s.push_ui_request(json!({"focus": "navSearch"})); return Ok(json!({"count": s.find.results.len()})); } let story = s.sel.focus.story; @@ -624,7 +624,7 @@ fn find(s: &mut Session, v: &Value) -> CmdResult { /// Find in the classic dialog: every match in the body or the selection, optionally highlighted while you read. fn advanced_find(s: &mut Session, v: &Value) -> CmdResult { if p::str(v, "text").is_none() && v.get("highlight").is_none() { - s.ui_requests.push(json!({"open": "find"})); + s.push_ui_request(json!({"open": "find"})); return Ok(json!({"count": s.find.results.len()})); } read_opts(s, v); @@ -748,7 +748,7 @@ fn goto(s: &mut Session, v: &Value) -> CmdResult { s.sel = Selection::caret(t); s.page_hint = idx; } - s.ui_requests.push(json!({"scrollTo": "caret"})); + s.push_ui_request(json!({"scrollTo": "caret"})); return sel_result(s); } if let Some(name) = p::str(v, "bookmark") { @@ -769,7 +769,7 @@ fn goto(s: &mut Session, v: &Value) -> CmdResult { } return sel_result(s); } - s.ui_requests.push(json!({"open": "goto"})); + s.push_ui_request(json!({"open": "goto"})); sel_result(s) } diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 98564e214..08bdaf0ce 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -101,7 +101,7 @@ pub fn specs() -> Vec { // No path (the command search, say): the app asks where to save, as File β€Ί Export // does, instead of failing on the required parameter (#2313). let Some(path) = p::str(v, "path") else { - s.ui_requests.push(json!({"open": "exportPdf"})); + s.push_ui_request(json!({"open": "exportPdf"})); return Ok(json!({"exported": false})); }; let path = if path.to_ascii_lowercase().ends_with(".pdf") { path.to_string() } else { format!("{path}.pdf") }; @@ -127,13 +127,13 @@ pub fn specs() -> Vec { ) .pure(), CommandSpec::new("file.print", "Print", "File", |s, _| { - s.ui_requests.push(json!({"open": "print"})); + s.push_ui_request(json!({"open": "print"})); sel_result(s) }) .key("Mod+P / Mod+Shift+F12 / Mod+F2 / Mod+Alt+I") .pure(), CommandSpec::new("file.close", "Close", "File", |s, _| { - s.ui_requests.push(json!({"close": true})); + s.push_ui_request(json!({"close": true})); sel_result(s) }) .key("Mod+W / Mod+F4") @@ -227,7 +227,7 @@ fn normal_document(s: &Session) -> Document { /// Open a document, using the saved proofing default only for plain text and Markdown. fn open(s: &mut Session, v: &Value) -> CmdResult { let Some(path) = p::str(v, "path") else { - s.ui_requests.push(json!({"open": "openFile"})); + s.push_ui_request(json!({"open": "openFile"})); return sel_result(s); }; let password = p::str(v, "password"); @@ -290,7 +290,7 @@ fn save(s: &mut Session, v: &Value) -> CmdResult { let path = match p::str(v, "path").map(std::path::PathBuf::from).or_else(|| s.path.clone()) { Some(p) => p, None => { - s.ui_requests.push(json!({"open": "saveAs"})); + s.push_ui_request(json!({"open": "saveAs"})); return Ok(json!({"saved": false})); } }; @@ -357,7 +357,7 @@ fn is_word_2003_file(_path: &std::path::Path) -> bool { fn save_as(s: &mut Session, v: &Value) -> CmdResult { if p::str(v, "path").is_none() { - s.ui_requests.push(json!({"open": "saveAs"})); + s.push_ui_request(json!({"open": "saveAs"})); return Ok(json!({"saved": false})); } save(s, v) @@ -373,7 +373,7 @@ pub fn export_options(s: &Session, v: &Value) -> crate::io::ExportOptions { fn export_png(s: &mut Session, v: &Value) -> CmdResult { // No path: the app asks where to save, as file.exportPdf does (#2313). let Some(path) = p::str(v, "path") else { - s.ui_requests.push(json!({"open": "exportPng"})); + s.push_ui_request(json!({"open": "exportPng"})); return Ok(json!({"exported": false})); }; let page = p::u64(v, "page").unwrap_or(1).max(1) as usize - 1; diff --git a/crates/engine/src/cmd/font.rs b/crates/engine/src/cmd/font.rs index 0460c2274..dfb7a4816 100644 --- a/crates/engine/src/cmd/font.rs +++ b/crates/engine/src/cmd/font.rs @@ -23,7 +23,7 @@ pub fn specs() -> Vec { } fn load_system(s: &mut Session, _v: &Value) -> CmdResult { - s.ui_requests.push(json!({"open": "loadSystemFonts"})); + s.push_ui_request(json!({"open": "loadSystemFonts"})); Ok(json!({"pending": true})) } @@ -68,7 +68,7 @@ fn add(s: &mut Session, v: &Value) -> CmdResult { } } else { // From the ribbon: the UI shows a file picker and runs this again with the file. - s.ui_requests.push(json!({"open": "addFont"})); + s.push_ui_request(json!({"open": "addFont"})); return Ok(json!({"pending": true})); }; let r = FontDb::global().install_font(&name, &bytes).map_err(|e| CmdError::Failed(e.to_string()))?; diff --git a/crates/engine/src/cmd/format.rs b/crates/engine/src/cmd/format.rs index bc2d4fdef..6dbd06935 100644 --- a/crates/engine/src/cmd/format.rs +++ b/crates/engine/src/cmd/format.rs @@ -139,7 +139,7 @@ pub fn specs() -> Vec { CommandSpec::new("format.charStyle", "Apply Character Style", "Home β€Ί Styles", char_style).params(r#"{"style": string}"#), CommandSpec::new("format.set", "Set Character Formatting", "Home β€Ί Font β€Ί Font", set).params(r#"{"props": CharProps}"#), CommandSpec::new("format.fontDialog", "Font Dialog", "Home β€Ί Font", |s, _| { - s.ui_requests.push(json!({"open": "font"})); + s.push_ui_request(json!({"open": "font"})); sel_result(s) }) .key("Mod+D") diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index a381f3bda..2493d07f7 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -221,7 +221,7 @@ pub(crate) fn picture(s: &mut Session, v: &Value) -> CmdResult { } else if let Some(path) = p::str(v, "path") { crate::io::read_file(path).map_err(CmdError::Failed)? } else { - s.ui_requests.push(json!({"open": "insertPicture"})); + s.push_ui_request(json!({"open": "insertPicture"})); return sel_result(s); }; place_picture(s, bytes, p::f32(v, "width"), p::str(v, "alt").unwrap_or("")) @@ -295,7 +295,7 @@ fn icon(s: &mut Session, v: &Value) -> CmdResult { .iter() .map(|i| json!({"id": i.id, "name": i.name, "category": i.category.name(), "keywords": i.keywords})) .collect(); - s.ui_requests.push(json!({"open": "icons"})); + s.push_ui_request(json!({"open": "icons"})); return Ok(json!({"icons": icons})); }; let icon = icon_lib::find(id).ok_or_else(|| CmdError::Params(format!("no icon {id:?} (insert.icon without an id lists them)")))?; @@ -441,7 +441,7 @@ fn link(s: &mut Session, v: &Value) -> CmdResult { None => match place_target(s, v)? { Some(t) => t, None => { - s.ui_requests.push(json!({"open": "link"})); + s.push_ui_request(json!({"open": "link"})); return sel_result(s); } }, diff --git a/crates/engine/src/cmd/options.rs b/crates/engine/src/cmd/options.rs index a2d2720ba..74777db99 100644 --- a/crates/engine/src/cmd/options.rs +++ b/crates/engine/src/cmd/options.rs @@ -201,7 +201,7 @@ fn options(s: &mut Session, v: &Value) -> CmdResult { // Marks, hidden text and proofing change what is laid out. s.relayout(); } else { - s.ui_requests.push(json!({"open": "options", "pane": pane})); + s.push_ui_request(json!({"open": "options", "pane": pane})); } let mut r = current(s); if let Some(o) = r.as_object_mut() { diff --git a/crates/engine/src/cmd/page.rs b/crates/engine/src/cmd/page.rs index 5d8055a72..ff1d113c9 100644 --- a/crates/engine/src/cmd/page.rs +++ b/crates/engine/src/cmd/page.rs @@ -45,7 +45,7 @@ pub fn specs() -> Vec { validate_section(&mut new)?; return with_sect(s, |x| *x = new.clone()); } - s.ui_requests.push(json!({"open": "pageSetup"})); + s.push_ui_request(json!({"open": "pageSetup"})); Ok(serde_json::to_value(sect(s)).unwrap_or(Value::Null)) }) .params(r#"{"section"?: SectionProps} (replaces the whole section, headers and footers included; checked like Size, Margins, Columns, Line Numbers, Footnotes and Page Borders: non-finite numbers and margins that leave no room for text are refused; page sides 7.2–1584 pt, 1–12 columns with 0–300 pt gaps, header/footer/gutter 0–1584 pt, page border width 0.25–6 pt and distance 0–31 pt, note start 1–32767, line numbers start 1–32767, countBy 1–100, distance 0–1584 pt are clamped)"#), diff --git a/crates/engine/src/cmd/para.rs b/crates/engine/src/cmd/para.rs index 3ce6d739b..c87b15200 100644 --- a/crates/engine/src/cmd/para.rs +++ b/crates/engine/src/cmd/para.rs @@ -199,7 +199,7 @@ pub fn specs() -> Vec { r#"{"props": ParaProps, "onPage"?: {"align"?: "left|center|right|justify|distribute", "left"?: pt, "right"?: pt} (alignment and indents as seen on the page: each paragraph's own direction, after `props`, says which edge is its start)}"#, ), CommandSpec::new("para.dialog", "Paragraph Settings", "Home β€Ί Paragraph", |s, _| { - s.ui_requests.push(json!({"open": "paragraph"})); + s.push_ui_request(json!({"open": "paragraph"})); sel_result(s) }) .pure(), @@ -213,7 +213,7 @@ pub fn specs() -> Vec { return Err(CmdError::Params(format!("unknown sort `{sort}` (recommended|alphabetical)"))); } let list = manage_list(s, sort == "alphabetical"); - s.ui_requests.push(json!({"open": "manageStyles"})); + s.push_ui_request(json!({"open": "manageStyles"})); Ok(list) }) .params(r#"{"sort"?: "recommended|alphabetical"} β†’ [{id, name, type: "paragraph|character|linked|table", builtIn, inGallery, hidden, basedOn, priority, format}]"#) diff --git a/crates/engine/src/cmd/paste.rs b/crates/engine/src/cmd/paste.rs index b4353ac0b..5327e8e02 100644 --- a/crates/engine/src/cmd/paste.rs +++ b/crates/engine/src/cmd/paste.rs @@ -106,7 +106,9 @@ fn paste_special(s: &mut Session, v: &Value) -> CmdResult { req[k] = json!(t); } } - s.ui_requests.push(req); + // Only the newest dialog request matters; older ones would pin their payloads. + s.ui_requests.retain(|r| r["open"] != "pasteSpecial"); + s.push_ui_request(req); return Ok(json!({"formats": formats_json(&ids)})); }; let fmt = match fmt.to_ascii_lowercase().as_str() { diff --git a/crates/engine/src/cmd/phonetic.rs b/crates/engine/src/cmd/phonetic.rs index 50262aa50..9c4c900a8 100644 --- a/crates/engine/src/cmd/phonetic.rs +++ b/crates/engine/src/cmd/phonetic.rs @@ -23,7 +23,7 @@ pub fn specs() -> Vec { /// dialog and returns the selection's phonetic guide (`base`, `ruby`, `align`, `size`, `offset`). fn phonetic(s: &mut Session, v: &Value) -> CmdResult { let Some(rv) = v.get("ruby") else { - s.ui_requests.push(json!({"open": "phonetic"})); + s.push_ui_request(json!({"open": "phonetic"})); return Ok(state(s)); }; let text: String = match rv { diff --git a/crates/engine/src/cmd/tools.rs b/crates/engine/src/cmd/tools.rs index 2de3cfb97..b35921501 100644 --- a/crates/engine/src/cmd/tools.rs +++ b/crates/engine/src/cmd/tools.rs @@ -539,7 +539,7 @@ pub fn specs() -> Vec { }) .pure(), CommandSpec::new("file.share", "Share", "File", |s, _| { - s.ui_requests.push(json!({"open": "saveAs"})); + s.push_ui_request(json!({"open": "saveAs"})); Ok(json!({"hint": "Save or export a copy (PDF, Word document) to share it."})) }) .pure(), @@ -1879,7 +1879,7 @@ fn customize_keyboard(s: &mut Session, v: &Value) -> CmdResult { } out.insert("custom".into(), serde_json::to_value(&s.keymap).unwrap_or(Value::Null)); if !asked { - s.ui_requests.push(json!({"open": "customizeKeyboard"})); + s.push_ui_request(json!({"open": "customizeKeyboard"})); } Ok(Value::Object(out)) } @@ -1888,7 +1888,7 @@ fn customize_keyboard(s: &mut Session, v: &Value) -> CmdResult { /// with no query, the Search box (`ui_requests` β†’ the UI's command search). fn search_commands(s: &mut Session, v: &Value) -> CmdResult { let Some(query) = p::str(v, "query") else { - s.ui_requests.push(json!({"open": "commands"})); + s.push_ui_request(json!({"open": "commands"})); return Ok(json!({"matches": []})); }; let words: Vec = query.split_whitespace().map(str::to_lowercase).collect(); @@ -1984,7 +1984,7 @@ fn customize_ribbon(s: &mut Session, v: &Value) -> CmdResult { out.insert("tabs".into(), serde_json::to_value(l.tabs().as_ref()).unwrap_or(Value::Null)); out.insert("qat".into(), json!(l.qat())); if !asked { - s.ui_requests.push(json!({"open": "customizeRibbon"})); + s.push_ui_request(json!({"open": "customizeRibbon"})); } Ok(Value::Object(out)) } diff --git a/crates/engine/src/cmd/view.rs b/crates/engine/src/cmd/view.rs index 99ac8b5ec..bc46df315 100644 --- a/crates/engine/src/cmd/view.rs +++ b/crates/engine/src/cmd/view.rs @@ -91,12 +91,12 @@ pub fn specs() -> Vec { // light and dark, as the manual switch did. CommandSpec::new("view.interfaceTheme", "Interface Theme", "View β€Ί Dark Mode", |s, v| match v.get("value") { None => { - s.ui_requests.push(json!({"toggle": "interfaceTheme"})); + s.push_ui_request(json!({"toggle": "interfaceTheme"})); Ok(json!({"value": "toggle"})) } Some(Value::String(name)) => match name.as_str() { "light" | "dark" | "system" => { - s.ui_requests.push(json!({"interfaceTheme": name})); + s.push_ui_request(json!({"interfaceTheme": name})); Ok(json!({"value": name})) } _ => Err(CmdError::Params("interface theme: light|dark|system".into())), @@ -121,12 +121,12 @@ pub fn specs() -> Vec { .params(r#"{"value"?: bool}"#) .pure(), CommandSpec::new("view.newWindow", "New Window", "View β€Ί Window", |s, _| { - s.ui_requests.push(json!({"open": "newWindow"})); + s.push_ui_request(json!({"open": "newWindow"})); sel_result(s) }) .pure(), CommandSpec::new("view.split", "Split", "View β€Ί Window", |s, _| { - s.ui_requests.push(json!({"toggle": "split"})); + s.push_ui_request(json!({"toggle": "split"})); sel_result(s) }) .key("Mod+Alt+S") @@ -256,7 +256,7 @@ fn zoom(s: &mut Session, v: &Value) -> CmdResult { s.view.multi_page = false; } None => { - s.ui_requests.push(json!({"open": "zoom"})); + s.push_ui_request(json!({"open": "zoom"})); } } Ok(json!({"zoom": s.view.zoom, "fit": s.view.fit})) diff --git a/crates/engine/src/session.rs b/crates/engine/src/session.rs index ff0aa7938..68ec37f8e 100644 --- a/crates/engine/src/session.rs +++ b/crates/engine/src/session.rs @@ -637,12 +637,23 @@ fn doc_bib_style(doc: &Document) -> String { doc.settings.bib_style.as_deref().and_then(crate::cmd::citations::find_style).map_or("APA", |st| st.name).to_string() } +/// Most pending front-end requests a session keeps. +pub const MAX_UI_REQUESTS: usize = 64; /// Maximum undo depth. const MAX_UNDO: usize = 500; /// How many steps the Undo button's list shows (#701); `edit.undoTo` reaches no further. pub const UNDO_LIST_MAX: usize = 100; impl Session { + /// Queue a request for the front end. A headless session never drains the queue, so only + /// the newest [`MAX_UI_REQUESTS`] are kept. + pub fn push_ui_request(&mut self, req: Value) { + if self.ui_requests.len() >= MAX_UI_REQUESTS { + self.ui_requests.remove(0); + } + self.ui_requests.push(req); + } + /// File name of the persisted custom dictionary inside the settings folder. pub const USER_DICT_FILE: &str = "custom-dictionary.txt"; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 6a8b3d6b5..8c777dba6 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -9329,3 +9329,16 @@ fn table_cell_margins_preserves_unpassed_sides() { let margins = s.doc.table(StoryRef::Body, &tp).unwrap().props.cell_margins.unwrap(); assert_eq!(margins, [0.0, 10.0, 0.0, 15.0]); } + +#[test] +fn headless_ui_requests_stay_bounded() { + let mut s = s(); + let big = "x".repeat(1 << 20); + for _ in 0..200 { + run(&mut s, "edit.pasteSpecial", json!({"text": big})); + run(&mut s, "edit.goto", json!({})); + } + assert!(s.ui_requests.len() <= 64, "len = {}", s.ui_requests.len()); + let size: usize = s.ui_requests.iter().map(|r| r.to_string().len()).sum(); + assert!(size < 4 << 20, "queued {size} bytes"); +} From 4045d76052081d51364d688a5a26c9d70a8b28ee Mon Sep 17 00:00:00 2001 From: Michael Higgins <55243596+Higgs32584@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:06:17 -0400 Subject: [PATCH 14/20] Validate PNG extension in file.exportPng (#3837) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * changes to filter dialogue * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada * Parse the Flat OPC part root tag once when inheriting namespaces (#3944) * Parse the Flat OPC part root tag once when inheriting namespaces Fixes #3727 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada * #3944: drop the wall-clock assert from the long-root-tag test Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Haiku 5.5 Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Brandon Thomas Co-authored-by: Joseph Anglada Co-authored-by: dfgodvan Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> * Write a text-box story shared by many shapes only once on docx save (#3983) Fixes #3653 Co-authored-by: Claude Opus 5.5 * Keep pending formatting revisions when Style Inspector clears formatting (#3971) Fixes #3671 Co-authored-by: Claude Haiku 5.5 * Cap the headless ui_requests queue and keep one Paste Special request (#3997) Fixes #3641 Co-authored-by: Claude Haiku 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada Co-authored-by: dfgodvan Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> --- crates/engine/src/cmd/file.rs | 3 +++ crates/engine/src/tests.rs | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 08bdaf0ce..6494aa55d 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -384,6 +384,9 @@ fn export_png(s: &mut Session, v: &Value) -> CmdResult { // the on-screen cue while editing the body, not part of the page. let mut opts = wordcraft_render::RenderOptions::default(); opts.display.dim_header = false; + if !path.to_ascii_lowercase().ends_with(".png") { + return Err(CmdError::Params("path must end in .png".into())); + } let img = wordcraft_render::render_page(&s.doc, pg, scale, &opts); let png = img.to_png(); crate::io::write_file(path, &png).map_err(CmdError::Failed)?; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 8c777dba6..305e640d7 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -9342,3 +9342,27 @@ fn headless_ui_requests_stay_bounded() { let size: usize = s.ui_requests.iter().map(|r| r.to_string().len()).sum(); assert!(size < 4 << 20, "queued {size} bytes"); } + +/// `file.exportPng` writes only to `.png` paths, so automation can't replace other files with +/// page images (#3696). +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn export_png_refuses_a_path_that_is_not_png() { + let dir = scratch_dir("export-png-extension"); + let mut s = s(); + run(&mut s, "text.insert", json!({"text": "Hello"})); + for name in ["notes.txt", "config", "report.docx", "page.png.txt"] { + let target = dir.join(name); + std::fs::write(&target, b"precious user notes").unwrap(); + let r = s.run("file.exportPng", &json!({"path": target.to_string_lossy()})); + assert!(matches!(r, Err(crate::CmdError::Params(_))), "{name}: {r:?}"); + assert_eq!(std::fs::read(&target).unwrap(), b"precious user notes", "{name} was changed"); + } + // A .png path still works, whatever its case. + for name in ["page.png", "PAGE.PNG"] { + let target = dir.join(name); + s.run("file.exportPng", &json!({"path": target.to_string_lossy()})).unwrap(); + assert!(std::fs::read(&target).unwrap().starts_with(b"\x89PNG"), "{name} is not a PNG"); + } + let _ = std::fs::remove_dir_all(&dir); +} From 58f830843bac415b17773c6606a72d45963d2422 Mon Sep 17 00:00:00 2001 From: In Mean <6434741+inmean@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:18:30 +0700 Subject: [PATCH 15/20] Khmer: ignore w14:ligatures when shaping Khmer text (#3921) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents that Word saves with w14:ligatures="historicalDiscretional" (every Khmer run in typical Khmer OS files) turned liga and clig off. Khmer OS fonts build their syllables there, so pre-base vowels and stacked signs came out in the wrong order (αžŸαŸŠαžΎαž”αž’αž„αŸ’αž€αŸαž drawn as αžŸαžΈαž”αž’αž„αŸ’αž€αžαŸ). Khmer text is now shaped with the font's defaults, as Word does. Co-authored-by: Claude Sonnet 5.5 --- crates/layout/src/para.rs | 12 ++++++++++-- crates/layout/src/tests_complex.rs | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/layout/src/para.rs b/crates/layout/src/para.rs index 8e56aea82..6e762d338 100644 --- a/crates/layout/src/para.rs +++ b/crates/layout/src/para.rs @@ -646,6 +646,11 @@ pub(crate) fn ligature_features(l: wordcraft_doc::props::Ligatures) -> [wordcraf [f(b"liga", l.standard), f(b"clig", l.contextual), f(b"hlig", l.historical), f(b"dlig", l.discretional)] } +/// A Khmer letter, sign or digit (U+1780–U+17FF, Khmer Symbols U+19E0–U+19FF). +fn is_khmer(c: char) -> bool { + matches!(u32::from(c), 0x1780..=0x17FF | 0x19E0..=0x19FF) +} + /// Whether `c`'s capital is more than one character (ß β†’ SS), so All Caps draws more /// characters than the document holds. fn caps_expands(c: char) -> bool { @@ -1060,7 +1065,10 @@ impl<'a> Builder<'a> { // The shaper kerns by default; Word kerns only when Kerning for fonts covers the size. // Without `w14:ligatures` the shaper's default (standard and contextual) stays. let [kern_off] = no_kern; - let all = match rc.ligatures { + // Khmer shaping builds its syllables (pre-base vowels, stacked signs) in `liga` and + // `clig`, so a run's ligature setting never reaches Khmer text: Word ignores it there. + let ligatures = rc.ligatures.filter(|_| !shape_text.chars().any(is_khmer)); + let all = match ligatures { Some(l) => { let [a, b, c, d] = ligature_features(l); [kern_off, a, b, c, d] @@ -1068,7 +1076,7 @@ impl<'a> Builder<'a> { None => [kern_off; 5], }; let from = usize::from(rc.kerned()); - let to = if rc.ligatures.is_some() { all.len() } else { 1 }; + let to = if ligatures.is_some() { all.len() } else { 1 }; let features: &[wordcraft_fonts::Feature] = all.get(from..to).unwrap_or_default(); let shaped = if use_levels { wordcraft_fonts::shape_run(&st.face, shape_text, features, map, key.rtl) diff --git a/crates/layout/src/tests_complex.rs b/crates/layout/src/tests_complex.rs index b83a92715..613681987 100644 --- a/crates/layout/src/tests_complex.rs +++ b/crates/layout/src/tests_complex.rs @@ -266,3 +266,21 @@ fn a_missing_roman_thai_font_is_not_drawn_in_cambria() { assert_ne!(face(true).id(), cambria.id()); assert_eq!(face(true).id(), face(false).id()); } + +/// `w14:ligatures` (Word writes `historicalDiscretional` on whole Khmer documents) turns `liga` +/// and `clig` off, which breaks Khmer OS syllables: Khmer text is shaped the same either way. +#[test] +fn khmer_ignores_the_ligature_setting() { + use wordcraft_doc::props::{CharProps, Ligatures}; + let text = "\u{179F}\u{17CA}\u{17BE}\u{1794}\u{17A2}\u{1784}\u{17D2}\u{1780}\u{17C1}\u{178F} \u{1795}\u{17D2}\u{178F}\u{1793}\u{17D2}\u{1791}\u{17B6}\u{1791}\u{17C4}\u{179F}"; + let gids = |ligatures: Option| { + let chr = CharProps { font: Some("Khmer OS Siemreap".into()), size: Some(12.0), ligatures, ..Default::default() }; + let mut d = Document::new(); + d.body = vec![wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text(text, chr))]; + let l = lay(&d); + para_layout(&l, 0).glyphs.iter().map(|g| g.gid).collect::>() + }; + let hd = Ligatures { historical: true, discretional: true, ..Default::default() }; + assert_eq!(gids(Some(hd)), gids(None)); + assert_eq!(gids(Ligatures::from_ooxml("none")), gids(None)); +} From 112dab414845175c590e1feb4e6cb751458a598c Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:18:40 +0200 Subject: [PATCH 16/20] Skip quoted attribute values when scanning settings.xml tags (#3954) Fixes #3682 Co-authored-by: Claude Haiku 5.5 --- crates/docx/src/write/settings_merge.rs | 31 +++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/docx/src/write/settings_merge.rs b/crates/docx/src/write/settings_merge.rs index ef7b6c785..981b9c098 100644 --- a/crates/docx/src/write/settings_merge.rs +++ b/crates/docx/src/write/settings_merge.rs @@ -153,11 +153,26 @@ struct Child { end: usize, } +/// Byte index of the `>` that ends the tag at the start of `s`, skipping quoted attribute values. +fn tag_end(s: &str) -> Option { + let mut quote = None; + for (i, c) in s.char_indices() { + match (quote, c) { + (Some(q), c) if c == q => quote = None, + (Some(_), _) => {} + (None, '"' | '\'') => quote = Some(c), + (None, '>') => return Some(i), + _ => {} + } + } + None +} + /// The root start tag's span end and the root's direct children, or `None` for anything the /// scanner doesn't expect (CDATA, DOCTYPE, a different root). fn children(xml: &str, root: &str) -> Option<(usize, usize, Vec)> { let open = xml.find(&format!("<{root}"))?; - let open_end = open + xml.get(open..)?.find('>')? + 1; + let open_end = open + tag_end(xml.get(open..)?)? + 1; if xml.get(open_end - 2..open_end)? == "/>" { return None; } @@ -182,7 +197,7 @@ fn children(xml: &str, root: &str) -> Option<(usize, usize, Vec)> { i = at + tail.find("?>")? + 2; continue; } - let gt = at + tail.find('>')? + 1; + let gt = at + tag_end(tail)? + 1; let tag = xml.get(at..gt)?; if tag.starts_with(""#, GEN.as_bytes()).is_none()); } + #[test] + fn a_gt_inside_an_attribute_value_does_not_hide_the_kept_settings() { + for v in [r#"w:val="x>y""#, "w:val='x>y'"] { + let orig = format!( + r#""# + ); + assert!(has_unowned(orig.as_bytes()), "{v}"); + let out = String::from_utf8(merge(orig.as_bytes(), GEN.as_bytes()).unwrap()).unwrap(); + assert!(out.contains(r#"w:name="a""#) && out.contains(r#"w:name="b""#), "{v}"); + } + } + #[test] fn a_compat_block_without_a_mode_gets_the_generated_one() { let orig = r#""#; From c023a71d7f86ca72341ac2e9f248fbdc5bc89c69 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:20:30 +0200 Subject: [PATCH 17/20] Rename kept docx parts whose names are not valid OPC part names (#3947) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename kept docx parts whose names are not valid OPC part names Fixes #3691 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada * Parse the Flat OPC part root tag once when inheriting namespaces (#3944) * Parse the Flat OPC part root tag once when inheriting namespaces Fixes #3727 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada * #3944: drop the wall-clock assert from the long-root-tag test Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Haiku 5.5 Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Brandon Thomas Co-authored-by: Joseph Anglada Co-authored-by: dfgodvan Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> * Write a text-box story shared by many shapes only once on docx save (#3983) Fixes #3653 Co-authored-by: Claude Opus 5.5 * Keep pending formatting revisions when Style Inspector clears formatting (#3971) Fixes #3671 Co-authored-by: Claude Haiku 5.5 * Cap the headless ui_requests queue and keep one Paste Special request (#3997) Fixes #3641 Co-authored-by: Claude Haiku 5.5 * Validate PNG extension in file.exportPng (#3837) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * changes to filter dialogue * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada * Parse the Flat OPC part root tag once when inheriting namespaces (#3944) * Parse the Flat OPC part root tag once when inheriting namespaces Fixes #3727 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-au… * Khmer: ignore w14:ligatures when shaping Khmer text (#3921) Documents that Word saves with w14:ligatures="historicalDiscretional" (every Khmer run in typical Khmer OS files) turned liga and clig off. Khmer OS fonts build their syllables there, so pre-base vowels and stacked signs came out in the wrong order (αžŸαŸŠαžΎαž”αž’αž„αŸ’αž€αŸαž drawn as αžŸαžΈαž”αž’αž„αŸ’αž€αžαŸ). Khmer text is now shaped with the font's defaults, as Word does. Co-authored-by: Claude Sonnet 5.5 * Skip quoted attribute values when scanning settings.xml tags (#3954) Fixes #3682 Co-authored-by: Claude Haiku 5.5 * #3947: rename part-name segments that end in a dot Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Haiku 5.5 Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Brandon Thomas Co-authored-by: Joseph Anglada Co-authored-by: dfgodvan Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Michael Higgins <55243596+Higgs32584@users.noreply.github.com> Co-authored-by: In Mean <6434741+inmean@users.noreply.github.com> --- crates/docx/src/write/embed.rs | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/docx/src/write/embed.rs b/crates/docx/src/write/embed.rs index f04f83ee3..e0764387c 100644 --- a/crates/docx/src/write/embed.rs +++ b/crates/docx/src/write/embed.rs @@ -121,6 +121,8 @@ impl<'d> EmbedWriter<'d> { /// A free part name for (a copy of) `orig`: `orig` itself if free, else `stem_N.ext`. fn alloc(&mut self, orig: &str) -> String { + let clean = valid_part_name(orig); + let orig = clean.as_str(); // Suffixes cannot escape reserved directories or a .rels extension. Such a kept // part needs a different base name, otherwise every candidate would be reserved. let orig = if Self::reserved(&orig.to_ascii_lowercase()) { "word/embeddings/part.xml" } else { orig }; @@ -684,6 +686,29 @@ fn pt(v: f32) -> String { s.trim_end_matches('0').trim_end_matches('.').to_string() } +/// `name` as a valid OPC part name: no backslashes, empty or dot segments, segments ending in a +/// dot, or encoded separators. +fn valid_part_name(name: &str) -> String { + let mut out = Vec::new(); + for seg in name.split('/') { + let mut seg = seg.replace('\\', "_"); + for enc in ["%2f", "%5c"] { + while let Some(i) = seg.to_ascii_lowercase().find(enc) { + seg.replace_range(i..i + 3, "_"); + } + } + if seg.is_empty() || seg == "." || seg == ".." { + continue; + } + // A segment can't end in a dot either (`word/a.`). + if seg.ends_with('.') { + seg = format!("{}_", seg.trim_end_matches('.')); + } + out.push(seg); + } + if out.is_empty() { "word/embeddings/part.xml".to_string() } else { out.join("/") } +} + #[cfg(test)] mod tests { use super::*; @@ -714,4 +739,16 @@ mod tests { // Unturned: no rotation or flip left behind. assert_eq!(vml_style_with("rotation:90;flip:y;width:1pt", 2.0, 3.0, &Float::default()), "width:2pt;height:3pt"); } + + #[test] + fn kept_part_names_are_valid_opc_names() { + let doc = wordcraft_doc::Document::default(); + let mut w = EmbedWriter::new(&doc); + for orig in ["word/a\\b.bin", "word/..\\..\\x.bin", "word/a/../b%2Fc.bin", "word/a//./b%5Cc.bin", "word/a./b.bin", "word/a..."] { + let name = w.alloc(orig); + assert!(!name.contains('\\'), "{name}"); + assert!(!name.to_ascii_lowercase().contains("%2f") && !name.to_ascii_lowercase().contains("%5c"), "{name}"); + assert!(name.split('/').all(|s| !s.is_empty() && !s.ends_with('.')), "{name}"); + } + } } From 4e692b4123b5b30a7cb1053114afb7baf7e5ee79 Mon Sep 17 00:00:00 2001 From: Jayesh Prajapati <156836276+Jayesh-Prajapati9@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:50:58 +0530 Subject: [PATCH 18/20] #3637: A table cell span from a .json file no longer overflows (#3927) Insert Column, Merge Cells and Delete Column did arithmetic on the raw CellProps::span, which a .json document can set to any u32, so a span near u32::MAX overflowed (a panic in debug, a wrapped span in release). They now work from the clamped span() and cap the result at MAX_COLS. --- ROADMAP.md | 4 +++- crates/doc/src/table.rs | 33 ++++++++++++++++++++++++++++++--- docs/file-format-parity.md | 5 +++-- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index e899cf6a0..ac92f361f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** Β· next: beta, ~15 points (60% β†’ 75% ready for real work) and ~150–230 h away -> **Last reviewed:** 2026-10-10 Β· **Last updated:** 2026-10-22 Β· **Change:** trivial (progress log: a click in the footer while editing the header moves there; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Slovenian interface catalog, 2,614 entries; previously trivial: Insert Table of Figures dialog for any caption label, #3013; previously trivial: progress log: Home β€Ί Underline drop-down, #2738; previously trivial: personal templates in File β€Ί New and Save as Word Template, #2852; previously trivial: progress log: the scrollbar page tip stays on one line, #3341; previously trivial: progress log: forms protection greys out the control buttons; previously trivial: progress log: a document with many content controls saves in the time the controls take, #13; previously trivial: progress log: Word signature lines kept on save; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: progress log: closing the header returns to the body selection, #3331; previously trivial: progress log: saving to a NAS share mounted by GVFS, #3003; previously trivial: progress log: a picture dragged near the bottom of a page lands there, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: progress log: inline pictures drag to an insertion cursor and stay inline, #2233, #2234; previously trivial: progress log: a text file saved over itself closes without asking, #2376; previously trivial: progress log: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: fixed font inputs in four layout controls; previously trivial: typing no longer starts render threads on every repaint, #3027; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: progress log: banded rows shade as in Word, #3415; previously trivial: progress log: the table handles show while the caret is in the table, #2428; previously trivial: progress log: a click below a floating table reaches the text after it, #2038; previously trivial: progress log: Delete on selected table cells leaves one empty paragraph in each, #2802; previously trivial: progress log: lam-alef in exported PDFs extracts whole with fonts that draw it as two glyphs, #2310; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: Russian sorting keeps Ρ‘ with Π΅ and recognises decomposed Ρ‘/ΠΉ; previously trivial: progress log: a text box that keeps the keyboard no longer resets the input method every frame, #2986; previously trivial: progress log: recovery copies and .json files store pictures as base64, #586 follow-up; previously trivial: progress log: legacy menu keys, #1697; previously trivial: progress log: shape gradients and transparency, #1500; previously trivial: progress log: Focus fills the screen, and Full Screen View shows the pages alone, #1678; previously trivial: progress log: text above a float's anchor wraps around it, #2048; previously trivial: progress log: File β€Ί Recover follows the app theme, #3243; previously trivial: progress log: formatting toolbar with the text context menu, #2959; previously trivial: progress log: caret inside ligatures, #1974; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: long navigation headings wrap onto further lines within the pane, #3015; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: a word after a slash wraps whole, like Word, #2963; previously trivial: progress log: a header-cell picture watermark sits behind the text, #1775; previously trivial: symbols keep their font when the text font changes, #2036; previously trivial: opening a DOCX no longer copies its pictures; previously trivial: Add Font imports every selected font file, #1789); previously trivial (RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: Read Aloud speaks each sentence in its own language, #1843; previously trivial: hint-reliant CJK outlines; previously trivial: Ctrl+wheel zoom no longer scrolls the document, #1743; previously trivial: progress log: Turkish case mapping in AutoCorrect, #1923; previously trivial: a mouse drag on a table leaves the view where it is, #1738; previously trivial: progress log: Hebrew field results and smart quotes, #1670; previously trivial: Backspace removes a selected table, rows or columns, #1844; previously trivial: progress log: mixed Hebrew/Latin text keeps its order in Word, #1543; previously trivial: progress log: web typing in non-Latin text no longer repeats, #1480; previously trivial: progress log: font family caches bounded and math font lookups follow font changes, #1354; previously trivial: `-->` AutoCorrect replacement, #2223; previously trivial: numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: progress log: Columns on selected text, #2024; previously trivial: progress log: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: progress log: Indonesian proofing, #498; previously trivial: progress log: DOCX glossary documents (building blocks); previously trivial: progress log: Read Mode keeps the keyboard and shows whole, unobscured columns, #1772); previously trivial: progress log: font list entries of tall-ascent fonts no longer cut off, #2570); previously trivial: progress log: Split Cells and Convert Text to Table dialogs follow the Word check, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout fills the window width, #1876; previously trivial: progress log: Different First Page headers reachable from page 1, #2826; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: progress log: web preferences kept between visits, #2132; previously trivial: progress log: undrawable private-use list bullets show the level's default bullet, #1683; previously trivial: progress log: `w:eastAsianLayout` kept on save; previously trivial: table style editor formats every part, #714); previously trivial: VML freeforms read and kept on save; previously trivial: progress log: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: progress log: grouped `flowChartOr` shapes draw their circle and crossing spokes; previously trivial: progress log: copies keep their formatting in other programs, #1523; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) Β· **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 Β· **Last updated:** 2026-10-23 Β· **Change:** trivial (progress log: a table cell span from a `.json` file no longer overflows, #3637; previously trivial: progress log: a click in the footer while editing the header moves there; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Slovenian interface catalog, 2,614 entries; previously trivial: Insert Table of Figures dialog for any caption label, #3013; previously trivial: progress log: Home β€Ί Underline drop-down, #2738; previously trivial: personal templates in File β€Ί New and Save as Word Template, #2852; previously trivial: progress log: the scrollbar page tip stays on one line, #3341; previously trivial: progress log: forms protection greys out the control buttons; previously trivial: progress log: a document with many content controls saves in the time the controls take, #13; previously trivial: progress log: Word signature lines kept on save; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: progress log: closing the header returns to the body selection, #3331; previously trivial: progress log: saving to a NAS share mounted by GVFS, #3003; previously trivial: progress log: a picture dragged near the bottom of a page lands there, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: progress log: inline pictures drag to an insertion cursor and stay inline, #2233, #2234; previously trivial: progress log: a text file saved over itself closes without asking, #2376; previously trivial: progress log: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: fixed font inputs in four layout controls; previously trivial: typing no longer starts render threads on every repaint, #3027; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: progress log: banded rows shade as in Word, #3415; previously trivial: progress log: the table handles show while the caret is in the table, #2428; previously trivial: progress log: a click below a floating table reaches the text after it, #2038; previously trivial: progress log: Delete on selected table cells leaves one empty paragraph in each, #2802; previously trivial: progress log: lam-alef in exported PDFs extracts whole with fonts that draw it as two glyphs, #2310; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: Russian sorting keeps Ρ‘ with Π΅ and recognises decomposed Ρ‘/ΠΉ; previously trivial: progress log: a text box that keeps the keyboard no longer resets the input method every frame, #2986; previously trivial: progress log: recovery copies and .json files store pictures as base64, #586 follow-up; previously trivial: progress log: legacy menu keys, #1697; previously trivial: progress log: shape gradients and transparency, #1500; previously trivial: progress log: Focus fills the screen, and Full Screen View shows the pages alone, #1678; previously trivial: progress log: text above a float's anchor wraps around it, #2048; previously trivial: progress log: File β€Ί Recover follows the app theme, #3243; previously trivial: progress log: formatting toolbar with the text context menu, #2959; previously trivial: progress log: caret inside ligatures, #1974; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: long navigation headings wrap onto further lines within the pane, #3015; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: a word after a slash wraps whole, like Word, #2963; previously trivial: progress log: a header-cell picture watermark sits behind the text, #1775; previously trivial: symbols keep their font when the text font changes, #2036; previously trivial: opening a DOCX no longer copies its pictures; previously trivial: Add Font imports every selected font file, #1789); previously trivial (RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: Read Aloud speaks each sentence in its own language, #1843; previously trivial: hint-reliant CJK outlines; previously trivial: Ctrl+wheel zoom no longer scrolls the document, #1743; previously trivial: progress log: Turkish case mapping in AutoCorrect, #1923; previously trivial: a mouse drag on a table leaves the view where it is, #1738; previously trivial: progress log: Hebrew field results and smart quotes, #1670; previously trivial: Backspace removes a selected table, rows or columns, #1844; previously trivial: progress log: mixed Hebrew/Latin text keeps its order in Word, #1543; previously trivial: progress log: web typing in non-Latin text no longer repeats, #1480; previously trivial: progress log: font family caches bounded and math font lookups follow font changes, #1354; previously trivial: `-->` AutoCorrect replacement, #2223; previously trivial: numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: progress log: Columns on selected text, #2024; previously trivial: progress log: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: progress log: Indonesian proofing, #498; previously trivial: progress log: DOCX glossary documents (building blocks); previously trivial: progress log: Read Mode keeps the keyboard and shows whole, unobscured columns, #1772); previously trivial: progress log: font list entries of tall-ascent fonts no longer cut off, #2570); previously trivial: progress log: Split Cells and Convert Text to Table dialogs follow the Word check, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout fills the window width, #1876; previously trivial: progress log: Different First Page headers reachable from page 1, #2826; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: progress log: web preferences kept between visits, #2132; previously trivial: progress log: undrawable private-use list bullets show the level's default bullet, #1683; previously trivial: progress log: `w:eastAsianLayout` kept on save; previously trivial: table style editor formats every part, #714); previously trivial: VML freeforms read and kept on save; previously trivial: progress log: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: progress log: grouped `flowChartOr` shapes draw their circle and crossing spokes; previously trivial: progress log: copies keep their formatting in other programs, #1523; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) Β· **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Last reviewed:** 2026-10-10 Β· **Last updated:** 2026-10-18 Β· **Change:** trivial (English dialects in proofing, #1281; previously trivial: triple borders draw three lines, #1740; previously trivial: progress log: double-clicking a later page's footer no longer scrolls up, #2103; previously trivial: long navigation headings wrap onto further lines within the pane, #3015; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: a word after a slash wraps whole, like Word, #2963; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: progress log: pasted blockquotes are indented paragraphs, #1270; previously trivial: progress log: print-area strings translated in every catalog; previously trivial: progress log: comments readable in the dark theme, #2045; previously trivial: progress log: ODT sections, #1379; previously trivial: progress log: table deletes, Change Case and Unlink Fields respect Track Changes and locked content controls, #1329; previously trivial: progress log: rounded rectangle corners, #1476; previously trivial: progress log: Save As type list, #1399; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) Β· **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Printing update:** 2026-10-12 Β· direct desktop printing through craft-print; details below. @@ -136,6 +136,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm ## Progress log +- 2026-10-23: Tables: a cell span read from a `.json` document (any 32-bit number) no longer overflows when a column is inserted through the cell or the cell is merged: the span is counted from its clamped value and stays within 63 columns. It panicked in debug builds and wrapped to a small span in release (#3637). - 2026-10-22: Headers and footers: while editing the header, a single click in the footer edits the footer with the caret where clicked (and the reverse, and another page's own header or footer). The click went to the header being edited and the view scrolled back up to it, so a double-click on the footer rarely landed unless both were on screen. - 2026-10-21: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459). @@ -902,3 +903,4 @@ Footer page-table height and body wrapping are corrected. Other pagination gaps | 2026-10-17 | trivial | Progress log: saving into a folder that refuses a temporary file (#2138) | | 2026-10-19 | trivial | Progress log: personal templates in File β€Ί New, Save as Word Template (#2852) | | 2026-10-20 | trivial | Progress log: saving to a NAS share mounted by GVFS (#3003) | +| 2026-10-23 | trivial | Progress log: a table cell span from a `.json` file no longer overflows (#3637) | diff --git a/crates/doc/src/table.rs b/crates/doc/src/table.rs index 3bfbdb74f..3031f7420 100644 --- a/crates/doc/src/table.rs +++ b/crates/doc/src/table.rs @@ -193,7 +193,8 @@ impl Table { match idx { Some((i, true)) => { if let Some(c) = row.cells.get_mut(i) { - c.props.span += 1; + // From the clamped span: a `.json` file can carry any `u32` (#3637). + c.props.span = (c.span() + 1).min(MAX_COLS) as u32; } } Some((i, false)) => { @@ -223,7 +224,7 @@ impl Table { for (i, c) in row.cells.iter_mut().enumerate() { if g < x + c.span() { if c.span() > 1 { - c.props.span -= 1; + c.props.span = (c.span() - 1) as u32; } else { rm = Some(i); } @@ -247,7 +248,7 @@ impl Table { let removed: Vec = row.cells.drain(c0 + 1..=c1).collect(); if let Some(first) = row.cells.get_mut(c0) { for c in removed { - first.props.span += c.props.span.max(1); + first.props.span = (first.span() + c.span()).min(MAX_COLS) as u32; let nonempty: Vec<_> = c.blocks.into_iter().filter(|b| b.as_para().is_none_or(|p| !p.is_empty())).collect(); first.blocks.extend(nonempty); } @@ -549,6 +550,32 @@ mod tests { t.delete_col(99); } + /// #3637: a `.json` file can give a cell any span; inserting a column through it, merging it + /// or deleting a column under it doesn't overflow, and the span it gets is in 1..=MAX_COLS. + #[test] + fn a_huge_span_from_a_file_does_not_overflow() { + let huge = || { + let mut t = Table::new(1, 2, 200.0); + t.rows[0].cells[0].props.span = u32::MAX; + t + }; + // Column 1 is inside the spanning cell, so the cell widens. + let mut t = huge(); + t.insert_col(1); + assert_eq!(t.rows[0].cells[0].props.span, MAX_COLS as u32); + let mut t = huge(); + t.merge_right(0, 0, 1); + assert_eq!(t.rows[0].cells.len(), 1); + assert_eq!(t.rows[0].cells[0].props.span, MAX_COLS as u32); + let mut t = huge(); + t.rows[0].cells[1].props.span = u32::MAX; + t.merge_right(0, 0, 1); + assert_eq!(t.rows[0].cells[0].props.span, MAX_COLS as u32); + let mut t = huge(); + t.delete_col(0); + assert_eq!(t.rows[0].cells[0].props.span, MAX_COLS as u32 - 1); + } + #[test] fn merge_and_split() { let mut t = Table::new(2, 3, 300.0); diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index b2a0e6979..31df388d5 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity with Microsoft Word -> **Last reviewed:** 2026-10-11 Β· **Last updated:** 2026-10-22 Β· **Change:** trivial (the file commands that change the document are checked on a protected document, the protection of a protected document needs an explicit stop, the session author can't change through `file.setAuthor` under protection (the CLI's `--author` uses the same protection check), and the post-check compares the settings and the metadata; previously trivial: a document with many content controls saves in the time the controls take, #13; previously trivial: `majorBidi`/`minorBidi` theme fonts follow the settings' theme font language, as measured in Word; previously trivial: Word signature lines are kept on save, still signable; previously trivial: Times New Roman keeps Liberation Serif under the roman-font rule; previously trivial: shape gradients and transparency, #1500; previously trivial: a folder that keeps no Unix modes, such as a NAS share mounted by GVFS, still takes a save, #3003; previously trivial: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: table style band sizes written, a style without them unbanded as in Word, #3415; previously trivial: HTML export writes ruby (furigana, phonetic guides) and HTML import reads it back, #1681; previously trivial: RTF import decodes the East Asian multibyte code pages, #1653; previously trivial: PDF export: lam-alef drawn as two glyphs at one x extracts in reading order, #2310; previously trivial: native .json stores pictures and kept parts as base64, #586 follow-up; previously trivial: ODT headers and footers read from each master page, #863; previously trivial: RTF import compares heading and code formatting with the file's stylesheet; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: Word XML and web pages saved as `.doc` open by what they hold, #2853; previously trivial: a VML shape in a header cell is always placed on the page, user-checked in Word for Mac, #1775; previously trivial: `w:sym` characters keep their symbol font when the text's font changes and are written back as `w:sym`, #2036; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: two lines in one and horizontal in vertical (`w:eastAsianLayout`) kept on save; previously trivial: PDF outline targets follow leading page breaks; previously trivial: positional tabs `w:ptab` read and kept on save, #2367; previously trivial: DOCX: mixed right-to-left runs split and marked `w:rtl` with their language, #1543; previously trivial: `w:doNotTrackMoves` round-trips and turns move tracking off; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: DOCX glossary documents (building blocks) read, kept and written; previously trivial: cached field results keep their visible run formatting; previously trivial: cached REF results retain hyperlink destinations; previously trivial: CLI conversions can explicitly refresh fields; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: a pasted HTML `
` is an indented paragraph, not the Quote style, #1270); previously trivial: every table style part round-trips, #714; previously trivial: VML freeforms read and kept on save; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: `flowChartOr` draws its circle and cross spokes, as in Word; previously trivial: RTF: Mac OS Roman and DOS text, #1653; previously trivial: VML text boxes keep their internal margins, fit to their text and vertical alignment; hidden drawings not shown; previously trivial: PDF export draws horizontal and vertical lines inside groups, #1474; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously trivial: formatted paste from the system clipboard's HTML and RTF, #687; previously trivial: independent complex-script formatting inheritance and round-trip regression tests; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) Β· **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats +> **Last reviewed:** 2026-10-11 Β· **Last updated:** 2026-10-23 Β· **Change:** trivial (a table cell span from a `.json` file stays within 63 columns through Insert Column and Merge Cells, #3637; previously trivial: the file commands that change the document are checked on a protected document, the protection of a protected document needs an explicit stop, the session author can't change through `file.setAuthor` under protection (the CLI's `--author` uses the same protection check), and the post-check compares the settings and the metadata; previously trivial: a document with many content controls saves in the time the controls take, #13; previously trivial: `majorBidi`/`minorBidi` theme fonts follow the settings' theme font language, as measured in Word; previously trivial: Word signature lines are kept on save, still signable; previously trivial: Times New Roman keeps Liberation Serif under the roman-font rule; previously trivial: shape gradients and transparency, #1500; previously trivial: a folder that keeps no Unix modes, such as a NAS share mounted by GVFS, still takes a save, #3003; previously trivial: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: table style band sizes written, a style without them unbanded as in Word, #3415; previously trivial: HTML export writes ruby (furigana, phonetic guides) and HTML import reads it back, #1681; previously trivial: RTF import decodes the East Asian multibyte code pages, #1653; previously trivial: PDF export: lam-alef drawn as two glyphs at one x extracts in reading order, #2310; previously trivial: native .json stores pictures and kept parts as base64, #586 follow-up; previously trivial: ODT headers and footers read from each master page, #863; previously trivial: RTF import compares heading and code formatting with the file's stylesheet; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: Word XML and web pages saved as `.doc` open by what they hold, #2853; previously trivial: a VML shape in a header cell is always placed on the page, user-checked in Word for Mac, #1775; previously trivial: `w:sym` characters keep their symbol font when the text's font changes and are written back as `w:sym`, #2036; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: two lines in one and horizontal in vertical (`w:eastAsianLayout`) kept on save; previously trivial: PDF outline targets follow leading page breaks; previously trivial: positional tabs `w:ptab` read and kept on save, #2367; previously trivial: DOCX: mixed right-to-left runs split and marked `w:rtl` with their language, #1543; previously trivial: `w:doNotTrackMoves` round-trips and turns move tracking off; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: DOCX glossary documents (building blocks) read, kept and written; previously trivial: cached field results keep their visible run formatting; previously trivial: cached REF results retain hyperlink destinations; previously trivial: CLI conversions can explicitly refresh fields; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: a pasted HTML `
` is an indented paragraph, not the Quote style, #1270); previously trivial: every table style part round-trips, #714; previously trivial: VML freeforms read and kept on save; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: `flowChartOr` draws its circle and cross spokes, as in Word; previously trivial: RTF: Mac OS Roman and DOS text, #1653; previously trivial: VML text boxes keep their internal margins, fit to their text and vertical alignment; hidden drawings not shown; previously trivial: PDF export draws horizontal and vertical lines inside groups, #1474; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously trivial: formatted paste from the system clipboard's HTML and RTF, #687; previously trivial: independent complex-script formatting inheritance and round-trip regression tests; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) Β· **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats Every format Word opens or saves, what WordCraft does with it, and how it's tested. Fidelity is the share of a typical real-world file's content and formatting that survives (read: shows correctly; @@ -68,7 +68,7 @@ Weights are the share of a Word user's file traffic (estimate). Read/write: βœ… | `.tex` LaTeX | β€” | β€” | βœ… | βœ… | β€” | β€” | `crates/formats/src/tests.rs` | Beyond Word (#11), incl. math | | Pictures for Insert β€Ί Picture, drag-and-drop and paste | β€” | R | βœ… | β€” | β€” | β€” | `insert_picture_takes_a_windows_metafile` (engine), `paste_picture.rs` | PNG, JPEG, GIF, WebP, BMP, and Windows Metafile `.wmf` / Enhanced Metafile `.emf` at their own frame size (#802), drawn by `crates/metafile`. Not yet: SVG, TIFF, ICO, HEIC | | `.png` page images | β€” | (Save as picture, Mac) | β€” | βœ… | β€” | β€” | `io_ext.rs` | | -| `.json` document model | β€” | β€” | βœ… | βœ… | β€” | β€” | engine tests | For agents | +| `.json` document model | β€” | β€” | βœ… | βœ… | β€” | β€” | engine tests | For agents. A table cell's span is any number in the file; Insert Column and Merge Cells count it as at most 63 columns, as the `.docx` reader does (#3637) | ## DOCX in depth @@ -416,3 +416,4 @@ Imported text fields use the first nonempty cached result run's formatting on th | 2026-10-18 | trivial | Word 2007 WordArt (VML `v:textpath` in the body or a header logo) shown as a text box with its text, font, colour and warp, not dropped (#2875) | | 2026-10-20 | trivial | A folder that keeps no Unix modes (a NAS share mounted by GVFS) still takes a save instead of failing with "Operation not supported" (#3003) | | 2026-10-16 | trivial | Word's Table of Contents content control (`w:docPartObj`) is kept around a TOC that isn't in the heading-and-entries form (#2427) | +| 2026-10-23 | trivial | `.json`: a table cell span past 63 columns no longer overflows when a column is inserted through the cell or the cell is merged (#3637) | From 3183c03be5087811db35e32d86b156e3730c1092 Mon Sep 17 00:00:00 2001 From: Jayesh Prajapati <156836276+Jayesh-Prajapati9@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:58:41 +0530 Subject: [PATCH 19/20] #3663: "NaN" in the Font dialog's Size box no longer closes the app (#3928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #3663: "NaN" in the Font dialog's Size box no longer closes the app "NaN" parses as an f32 and clamp keeps NaN, so the preview font size was NaN and epaint's glyph assertion panicked while painting the dialog. The preview now drops non-finite sizes and shows 12 pt. * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada * Parse the Flat OPC part root tag once when inheriting namespaces (#3944) * Parse the Flat OPC part root tag once when inheriting namespaces Fixes #3727 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada * #3944: drop the wall-clock assert from the long-root-tag test Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Haiku 5.5 Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Brandon Thomas Co-authored-by: Joseph Anglada Co-authored-by: dfgodvan Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> * Write a text-box story shared by many shapes only once on docx save (#3983) Fixes #3653 Co-authored-by: Claude Opus 5.5 * Keep pending formatting revisions when Style Inspector clears formatting (#3971) Fixes #3671 Co-authored-by: Claude Haiku 5.5 * Cap the headless ui_requests queue and keep one Paste Special request (#3997) Fixes #3641 Co-authored-by: Claude Haiku 5.5 * Validate PNG extension in file.exportPng (#3837) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * changes to filter dialogue * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada * Parse the Flat OPC part root tag once when inheriting namespaces (#3944) * Parse the Flat OPC part root tag once when inheriting namespaces Fixes #3727 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-au… * Khmer: ignore w14:ligatures when shaping Khmer text (#3921) Documents that Word saves with w14:ligatures="historicalDiscretional" (every Khmer run in typical Khmer OS files) turned liga and clig off. Khmer OS fonts build their syllables there, so pre-base vowels and stacked signs came out in the wrong order (αžŸαŸŠαžΎαž”αž’αž„αŸ’αž€αŸαž drawn as αžŸαžΈαž”αž’αž„αŸ’αž€αžαŸ). Khmer text is now shaped with the font's defaults, as Word does. Co-authored-by: Claude Sonnet 5.5 * Skip quoted attribute values when scanning settings.xml tags (#3954) Fixes #3682 Co-authored-by: Claude Haiku 5.5 * Rename kept docx parts whose names are not valid OPC part names (#3947) * Rename kept docx parts whose names are not valid OPC part names Fixes #3691 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada * Parse the Flat OPC part root tag once when inheriting namespaces (#3944) * Parse the Flat OPC part root tag once when inheriting namespaces Fixes #3727 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its re… * #3637: A table cell span from a .json file no longer overflows (#3927) Insert Column, Merge Cells and Delete Column did arithmetic on the raw CellProps::span, which a .json document can set to any u32, so a span near u32::MAX overflowed (a panic in debug, a wrapped span in release). They now work from the clamped span() and cap the result at MAX_COLS. --------- Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada Co-authored-by: dfgodvan Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Michael Higgins <55243596+Higgs32584@users.noreply.github.com> Co-authored-by: In Mean <6434741+inmean@users.noreply.github.com> --- ROADMAP.md | 4 +++- crates/ui-egui/src/dialogs.rs | 34 +++++++++++++++++++++++++++++++++- docs/ui-parity.md | 3 ++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index ac92f361f..75eef1d47 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** Β· next: beta, ~15 points (60% β†’ 75% ready for real work) and ~150–230 h away -> **Last reviewed:** 2026-10-10 Β· **Last updated:** 2026-10-23 Β· **Change:** trivial (progress log: a table cell span from a `.json` file no longer overflows, #3637; previously trivial: progress log: a click in the footer while editing the header moves there; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Slovenian interface catalog, 2,614 entries; previously trivial: Insert Table of Figures dialog for any caption label, #3013; previously trivial: progress log: Home β€Ί Underline drop-down, #2738; previously trivial: personal templates in File β€Ί New and Save as Word Template, #2852; previously trivial: progress log: the scrollbar page tip stays on one line, #3341; previously trivial: progress log: forms protection greys out the control buttons; previously trivial: progress log: a document with many content controls saves in the time the controls take, #13; previously trivial: progress log: Word signature lines kept on save; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: progress log: closing the header returns to the body selection, #3331; previously trivial: progress log: saving to a NAS share mounted by GVFS, #3003; previously trivial: progress log: a picture dragged near the bottom of a page lands there, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: progress log: inline pictures drag to an insertion cursor and stay inline, #2233, #2234; previously trivial: progress log: a text file saved over itself closes without asking, #2376; previously trivial: progress log: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: fixed font inputs in four layout controls; previously trivial: typing no longer starts render threads on every repaint, #3027; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: progress log: banded rows shade as in Word, #3415; previously trivial: progress log: the table handles show while the caret is in the table, #2428; previously trivial: progress log: a click below a floating table reaches the text after it, #2038; previously trivial: progress log: Delete on selected table cells leaves one empty paragraph in each, #2802; previously trivial: progress log: lam-alef in exported PDFs extracts whole with fonts that draw it as two glyphs, #2310; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: Russian sorting keeps Ρ‘ with Π΅ and recognises decomposed Ρ‘/ΠΉ; previously trivial: progress log: a text box that keeps the keyboard no longer resets the input method every frame, #2986; previously trivial: progress log: recovery copies and .json files store pictures as base64, #586 follow-up; previously trivial: progress log: legacy menu keys, #1697; previously trivial: progress log: shape gradients and transparency, #1500; previously trivial: progress log: Focus fills the screen, and Full Screen View shows the pages alone, #1678; previously trivial: progress log: text above a float's anchor wraps around it, #2048; previously trivial: progress log: File β€Ί Recover follows the app theme, #3243; previously trivial: progress log: formatting toolbar with the text context menu, #2959; previously trivial: progress log: caret inside ligatures, #1974; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: long navigation headings wrap onto further lines within the pane, #3015; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: a word after a slash wraps whole, like Word, #2963; previously trivial: progress log: a header-cell picture watermark sits behind the text, #1775; previously trivial: symbols keep their font when the text font changes, #2036; previously trivial: opening a DOCX no longer copies its pictures; previously trivial: Add Font imports every selected font file, #1789); previously trivial (RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: Read Aloud speaks each sentence in its own language, #1843; previously trivial: hint-reliant CJK outlines; previously trivial: Ctrl+wheel zoom no longer scrolls the document, #1743; previously trivial: progress log: Turkish case mapping in AutoCorrect, #1923; previously trivial: a mouse drag on a table leaves the view where it is, #1738; previously trivial: progress log: Hebrew field results and smart quotes, #1670; previously trivial: Backspace removes a selected table, rows or columns, #1844; previously trivial: progress log: mixed Hebrew/Latin text keeps its order in Word, #1543; previously trivial: progress log: web typing in non-Latin text no longer repeats, #1480; previously trivial: progress log: font family caches bounded and math font lookups follow font changes, #1354; previously trivial: `-->` AutoCorrect replacement, #2223; previously trivial: numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: progress log: Columns on selected text, #2024; previously trivial: progress log: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: progress log: Indonesian proofing, #498; previously trivial: progress log: DOCX glossary documents (building blocks); previously trivial: progress log: Read Mode keeps the keyboard and shows whole, unobscured columns, #1772); previously trivial: progress log: font list entries of tall-ascent fonts no longer cut off, #2570); previously trivial: progress log: Split Cells and Convert Text to Table dialogs follow the Word check, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout fills the window width, #1876; previously trivial: progress log: Different First Page headers reachable from page 1, #2826; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: progress log: web preferences kept between visits, #2132; previously trivial: progress log: undrawable private-use list bullets show the level's default bullet, #1683; previously trivial: progress log: `w:eastAsianLayout` kept on save; previously trivial: table style editor formats every part, #714); previously trivial: VML freeforms read and kept on save; previously trivial: progress log: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: progress log: grouped `flowChartOr` shapes draw their circle and crossing spokes; previously trivial: progress log: copies keep their formatting in other programs, #1523; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) Β· **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 Β· **Last updated:** 2026-10-23 Β· **Change:** trivial (progress log: "NaN" in the Font dialog's Size box no longer closes the app, #3663; previously trivial: progress log: a table cell span from a `.json` file no longer overflows, #3637; previously trivial: progress log: a click in the footer while editing the header moves there; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Slovenian interface catalog, 2,614 entries; previously trivial: Insert Table of Figures dialog for any caption label, #3013; previously trivial: progress log: Home β€Ί Underline drop-down, #2738; previously trivial: personal templates in File β€Ί New and Save as Word Template, #2852; previously trivial: progress log: the scrollbar page tip stays on one line, #3341; previously trivial: progress log: forms protection greys out the control buttons; previously trivial: progress log: a document with many content controls saves in the time the controls take, #13; previously trivial: progress log: Word signature lines kept on save; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: progress log: closing the header returns to the body selection, #3331; previously trivial: progress log: saving to a NAS share mounted by GVFS, #3003; previously trivial: progress log: a picture dragged near the bottom of a page lands there, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: progress log: inline pictures drag to an insertion cursor and stay inline, #2233, #2234; previously trivial: progress log: a text file saved over itself closes without asking, #2376; previously trivial: progress log: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: fixed font inputs in four layout controls; previously trivial: typing no longer starts render threads on every repaint, #3027; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: progress log: banded rows shade as in Word, #3415; previously trivial: progress log: the table handles show while the caret is in the table, #2428; previously trivial: progress log: a click below a floating table reaches the text after it, #2038; previously trivial: progress log: Delete on selected table cells leaves one empty paragraph in each, #2802; previously trivial: progress log: lam-alef in exported PDFs extracts whole with fonts that draw it as two glyphs, #2310; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial: Russian sorting keeps Ρ‘ with Π΅ and recognises decomposed Ρ‘/ΠΉ; previously trivial: progress log: a text box that keeps the keyboard no longer resets the input method every frame, #2986; previously trivial: progress log: recovery copies and .json files store pictures as base64, #586 follow-up; previously trivial: progress log: legacy menu keys, #1697; previously trivial: progress log: shape gradients and transparency, #1500; previously trivial: progress log: Focus fills the screen, and Full Screen View shows the pages alone, #1678; previously trivial: progress log: text above a float's anchor wraps around it, #2048; previously trivial: progress log: File β€Ί Recover follows the app theme, #3243; previously trivial: progress log: formatting toolbar with the text context menu, #2959; previously trivial: progress log: caret inside ligatures, #1974; previously trivial: text typed into a content control takes the control's formatting; localized placeholder styles recognised; previously trivial: forms protection keeps its content controls: no inserting, Properties or Remove Content Control; previously trivial: long navigation headings wrap onto further lines within the pane, #3015; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: a word after a slash wraps whole, like Word, #2963; previously trivial: progress log: a header-cell picture watermark sits behind the text, #1775; previously trivial: symbols keep their font when the text font changes, #2036; previously trivial: opening a DOCX no longer copies its pictures; previously trivial: Add Font imports every selected font file, #1789); previously trivial (RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: Read Aloud speaks each sentence in its own language, #1843; previously trivial: hint-reliant CJK outlines; previously trivial: Ctrl+wheel zoom no longer scrolls the document, #1743; previously trivial: progress log: Turkish case mapping in AutoCorrect, #1923; previously trivial: a mouse drag on a table leaves the view where it is, #1738; previously trivial: progress log: Hebrew field results and smart quotes, #1670; previously trivial: Backspace removes a selected table, rows or columns, #1844; previously trivial: progress log: mixed Hebrew/Latin text keeps its order in Word, #1543; previously trivial: progress log: web typing in non-Latin text no longer repeats, #1480; previously trivial: progress log: font family caches bounded and math font lookups follow font changes, #1354; previously trivial: `-->` AutoCorrect replacement, #2223; previously trivial: numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: progress log: Columns on selected text, #2024; previously trivial: progress log: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: progress log: Indonesian proofing, #498; previously trivial: progress log: DOCX glossary documents (building blocks); previously trivial: progress log: Read Mode keeps the keyboard and shows whole, unobscured columns, #1772); previously trivial: progress log: font list entries of tall-ascent fonts no longer cut off, #2570); previously trivial: progress log: Split Cells and Convert Text to Table dialogs follow the Word check, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout fills the window width, #1876; previously trivial: progress log: Different First Page headers reachable from page 1, #2826; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: progress log: web preferences kept between visits, #2132; previously trivial: progress log: undrawable private-use list bullets show the level's default bullet, #1683; previously trivial: progress log: `w:eastAsianLayout` kept on save; previously trivial: table style editor formats every part, #714); previously trivial: VML freeforms read and kept on save; previously trivial: progress log: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: progress log: grouped `flowChartOr` shapes draw their circle and crossing spokes; previously trivial: progress log: copies keep their formatting in other programs, #1523; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) Β· **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Last reviewed:** 2026-10-10 Β· **Last updated:** 2026-10-18 Β· **Change:** trivial (English dialects in proofing, #1281; previously trivial: triple borders draw three lines, #1740; previously trivial: progress log: double-clicking a later page's footer no longer scrolls up, #2103; previously trivial: long navigation headings wrap onto further lines within the pane, #3015; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: a word after a slash wraps whole, like Word, #2963; previously trivial: RTF export writes sections (gap 19); previously trivial: ODT export writes sections (gap 19); previously trivial: progress log: pasted blockquotes are indented paragraphs, #1270; previously trivial: progress log: print-area strings translated in every catalog; previously trivial: progress log: comments readable in the dark theme, #2045; previously trivial: progress log: ODT sections, #1379; previously trivial: progress log: table deletes, Change Case and Unlink Fields respect Track Changes and locked content controls, #1329; previously trivial: progress log: rounded rectangle corners, #1476; previously trivial: progress log: Save As type list, #1399; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) Β· **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Printing update:** 2026-10-12 Β· direct desktop printing through craft-print; details below. @@ -136,6 +136,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm ## Progress log +- 2026-10-23: Font dialog: "NaN" or "inf" typed in the Size box previews the text at 12 pt. "NaN" reads as a number, and a font size that is not a number stopped the app while drawing the preview (#3663). - 2026-10-23: Tables: a cell span read from a `.json` document (any 32-bit number) no longer overflows when a column is inserted through the cell or the cell is merged: the span is counted from its clamped value and stays within 63 columns. It panicked in debug builds and wrapped to a small span in release (#3637). - 2026-10-22: Headers and footers: while editing the header, a single click in the footer edits the footer with the caret where clicked (and the reverse, and another page's own header or footer). The click went to the header being edited and the view scrolled back up to it, so a double-click on the footer rarely landed unless both were on screen. - 2026-10-21: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459). @@ -903,4 +904,5 @@ Footer page-table height and body wrapping are corrected. Other pagination gaps | 2026-10-17 | trivial | Progress log: saving into a folder that refuses a temporary file (#2138) | | 2026-10-19 | trivial | Progress log: personal templates in File β€Ί New, Save as Word Template (#2852) | | 2026-10-20 | trivial | Progress log: saving to a NAS share mounted by GVFS (#3003) | +| 2026-10-23 | trivial | Progress log: "NaN" in the Font dialog's Size box no longer closes the app (#3663) | | 2026-10-23 | trivial | Progress log: a table cell span from a `.json` file no longer overflows (#3637) | diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 3a945bf07..977b9c1da 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1657,6 +1657,12 @@ pub(crate) fn buttons(ui: &mut Ui, ok: &str) -> (bool, bool) { r } +/// The Font dialog preview's size: the Size box's points between 6 and 72, else 12. `"NaN"` and +/// `"inf"` parse as floats too, and a NaN font size trips epaint's glyph assertion (#3663). +fn preview_size(size: &str) -> f32 { + size.trim().parse::().ok().filter(|pt| pt.is_finite()).unwrap_or(12.0).clamp(6.0, 72.0) +} + /// The character properties the Font dialog's fields stand for. fn font_props(d: &Dialog) -> serde_json::Map { let Dialog::Font { @@ -1784,7 +1790,7 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool { .. } => { // Build the sample text format for the preview pane below. - let preview_size = size.trim().parse::().unwrap_or(12.0).clamp(6.0, 72.0); + let preview_size = preview_size(size); let preview_color = wordcraft_doc::Rgb::parse(color).map(crate::theme::c32).unwrap_or_else(|| ui.visuals().text_color()); let mut fmt = egui::TextFormat { font_id: egui::FontId::new(preview_size, egui::FontFamily::Proportional), @@ -4585,6 +4591,32 @@ mod tests { } } + /// #3663: "NaN" in the Font dialog's Size box parses as a float; the preview draws at 12 pt + /// instead of a NaN size, which tripped epaint's glyph assertion and closed the app. + #[test] + fn font_dialog_preview_ignores_a_size_that_is_not_a_number() { + for (typed, pt) in [("NaN", 12.0), ("nan", 12.0), ("inf", 12.0), ("-infinity", 12.0), ("10.5", 10.5), ("200", 72.0), ("", 12.0)] { + assert_eq!(preview_size(typed), pt, "{typed:?}"); + } + let mut app = app_with("Example"); + let _ = app.run("select.all", json!({})); + app.dialog = Dialog::open("font", &mut app); + let Some(Dialog::Font { size, .. }) = &mut app.dialog else { panic!("no font dialog") }; + *size = "NaN".into(); + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state( + |ui, app: &mut WordApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + app, + ); + for _ in 0..4 { + h.step(); + } + assert!(matches!(&h.state().dialog, Some(Dialog::Font { size, .. }) if size == "NaN"), "the dialog stays open"); + } + #[test] fn font_dialog_ok_keeps_what_the_user_left_alone() { // Issue #466: OK sent every field, clearing small caps, spacing and double underline diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 88685b2f7..ed093c566 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity with Microsoft Word -> **Last reviewed:** 2026-10-10 Β· **Last updated:** 2026-10-22 Β· **Change:** trivial (File β€Ί Options disables the User name field while Restrict Editing is active; previously trivial: while editing a header or footer, a click in the other one edits it there without scrolling; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Insert Table of Figures dialog: any caption label, #3013; previously trivial: Home β€Ί Underline drop-down shows its lines, #2738; previously trivial: the text context menu includes the formatting mini toolbar, #2959; previously trivial: personal templates in File β€Ί New and Save as Word Template, #2852; previously trivial: the scrollbar page tip stays on one line as the page number grows, #3341; previously trivial: File β€Ί Home and New: templates and recent documents are buttons named for screen readers, #3176; previously trivial: forms protection greys out the Developer tab's control buttons and Properties; previously trivial: AltGr doesn't open KeyTips, #2388; previously trivial: closing the header or footer puts the caret back where it was in the body, #3331; previously trivial: Format Page Numbers dialog: number format and Continue from previous section / Start at, #1113; previously trivial: inline pictures drag to a dashed insertion cursor and stay inline, #2233, #2234; previously trivial: Save Changes prompt: a text file saved over itself closes, #2376; previously trivial: the equation being edited is framed as in Word: an outline as tall as its line, a grip, an options strip that opens its menu, and its current argument shaded; previously trivial: the table handles show while the caret is in the table, #2428; previously trivial: legacy menu keys, #1697; previously trivial: Shape Fill β€Ί Gradient and the Format Shape pane, #1500; previously trivial: Focus fills the screen, and Full Screen View shows the pages alone, #1678; previously trivial: the text pointer over the page is drawn black or white on Windows, #1746; previously trivial: a click in the header/footer band from the body no longer scrolls to the body text, #2103; previously trivial: File β€Ί Recover follows the app theme, #3243; previously trivial: long navigation headings wrap onto further lines within the pane, #3015; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: dragging the vertical scrollbar names the page at the top of the window, #2551; previously trivial: Add Font imports every selected font file, #1789); previously trivial (File β€Ί Close keeps the window, #1712; previously trivial: the Spike: Ctrl+F3 and Ctrl+Shift+F3; previously trivial: Ctrl+wheel zoom no longer scrolls the document, #1743; previously trivial: a mouse drag on a table leaves the view where it is, #1738; previously trivial: numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: the tab strip never draws tabs over Comments / Editing / Share, #2026; previously trivial: Columns on selected text, #2024; previously trivial: Different First Page headers reachable from page 1, #2826; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: Read Mode keeps the keyboard, shows whole columns and keeps text clear of the page-turn bar, #1772); previously trivial: font list entries of tall-ascent fonts sit in their row, not cut off, #2570); previously trivial: Split Cells and Convert Text to Table dialogs follow Word's merged-cell limits and Initial column width, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout text fills the window width, #1876; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717)); previously trivial: comment balloons readable in the dark theme, #2045; previously trivial: Recently Used Fonts section at the top of the font list, #2068; previously trivial: web preferences kept between visits, #2132; previously trivial: dialog close control: a Windows-style box at the title bar's right end, the whole box clickable, red on hover, #1885); previously trivial (Modify Table Style formats every table style part, #714); previously trivial: a click on the ribbon chrome hands the keyboard to the page in the same frame, #1093 follow-up; previously trivial: Page Setup vertical alignment, #1320; previously trivial: the app opens on the Home tab every launch, as in Word; previously trivial: no document behind the Start screen, #1129; previously trivial: equation pictures share a few textures, #1002; previously trivial: selection bar in the left margin, #856; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) Β· **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 Β· **Last updated:** 2026-10-23 Β· **Change:** trivial (the Font dialog previews "NaN" or "inf" in the Size box at 12 pt, #3663; previously trivial: File β€Ί Options disables the User name field while Restrict Editing is active; previously trivial: while editing a header or footer, a click in the other one edits it there without scrolling; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Insert Table of Figures dialog: any caption label, #3013; previously trivial: Home β€Ί Underline drop-down shows its lines, #2738; previously trivial: the text context menu includes the formatting mini toolbar, #2959; previously trivial: personal templates in File β€Ί New and Save as Word Template, #2852; previously trivial: the scrollbar page tip stays on one line as the page number grows, #3341; previously trivial: File β€Ί Home and New: templates and recent documents are buttons named for screen readers, #3176; previously trivial: forms protection greys out the Developer tab's control buttons and Properties; previously trivial: AltGr doesn't open KeyTips, #2388; previously trivial: closing the header or footer puts the caret back where it was in the body, #3331; previously trivial: Format Page Numbers dialog: number format and Continue from previous section / Start at, #1113; previously trivial: inline pictures drag to a dashed insertion cursor and stay inline, #2233, #2234; previously trivial: Save Changes prompt: a text file saved over itself closes, #2376; previously trivial: the equation being edited is framed as in Word: an outline as tall as its line, a grip, an options strip that opens its menu, and its current argument shaded; previously trivial: the table handles show while the caret is in the table, #2428; previously trivial: legacy menu keys, #1697; previously trivial: Shape Fill β€Ί Gradient and the Format Shape pane, #1500; previously trivial: Focus fills the screen, and Full Screen View shows the pages alone, #1678; previously trivial: the text pointer over the page is drawn black or white on Windows, #1746; previously trivial: a click in the header/footer band from the body no longer scrolls to the body text, #2103; previously trivial: File β€Ί Recover follows the app theme, #3243; previously trivial: long navigation headings wrap onto further lines within the pane, #3015; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: dragging the vertical scrollbar names the page at the top of the window, #2551; previously trivial: Add Font imports every selected font file, #1789); previously trivial (File β€Ί Close keeps the window, #1712; previously trivial: the Spike: Ctrl+F3 and Ctrl+Shift+F3; previously trivial: Ctrl+wheel zoom no longer scrolls the document, #1743; previously trivial: a mouse drag on a table leaves the view where it is, #1738; previously trivial: numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: the tab strip never draws tabs over Comments / Editing / Share, #2026; previously trivial: Columns on selected text, #2024; previously trivial: Different First Page headers reachable from page 1, #2826; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: Read Mode keeps the keyboard, shows whole columns and keeps text clear of the page-turn bar, #1772); previously trivial: font list entries of tall-ascent fonts sit in their row, not cut off, #2570); previously trivial: Split Cells and Convert Text to Table dialogs follow Word's merged-cell limits and Initial column width, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout text fills the window width, #1876; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717)); previously trivial: comment balloons readable in the dark theme, #2045; previously trivial: Recently Used Fonts section at the top of the font list, #2068; previously trivial: web preferences kept between visits, #2132; previously trivial: dialog close control: a Windows-style box at the title bar's right end, the whole box clickable, red on hover, #1885); previously trivial (Modify Table Style formats every table style part, #714); previously trivial: a click on the ribbon chrome hands the keyboard to the page in the same frame, #1093 follow-up; previously trivial: Page Setup vertical alignment, #1320; previously trivial: the app opens on the Home tab every launch, as in Word; previously trivial: no document behind the Start screen, #1129; previously trivial: equation pictures share a few textures, #1002; previously trivial: selection bar in the left margin, #856; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) Β· **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 Recovery discovery no longer interrupts an explicitly created or opened document. Older copies remain in File β€Ί Recover, whose buttons use the interface palette. See [local recovery](recovery.md). @@ -287,3 +287,4 @@ labels unambiguous. Those extra-number assignments have not been compared with W | 2026-10-19 | trivial | Personal templates (#2852): File β€Ί New β€Ί Personal lists the user templates folder (`/Templates`, or File β€Ί Options β€Ί Advanced β€Ί File locations β€Ί User templates), subfolders as categories, searchable; a template opens as an untitled document attached to it; Save As β€Ί Save as Word Template starts there (`file.listTemplates`, `ui.saveAsTemplate`) | | 2026-10-15 | trivial | Double-clicking a page number in a later page's footer opens the footer there: the first click no longer moves the caret to the body text and scrolls the view up to it (#2103) | | 2026-10-20 | trivial | File β€Ί Home and New: each template (thumbnail and caption) and each recent document is one button named by its caption or file name (with the path when two recent file names are the same), so a screen reader announces it instead of unnamed text (#3176) | +| 2026-10-23 | trivial | Font dialog: "NaN" or "inf" in the Size box previews the text at 12 pt instead of stopping the app while drawing the preview (#3663) | From b2e4722bf53cb7dd01b8221e1fc674f503e5926f Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:31:27 +0200 Subject: [PATCH 20/20] Load ui.json with many unknown keys in linear time (#3968) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Load ui.json with many unknown keys in linear time Fixes #3659 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada * Parse the Flat OPC part root tag once when inheriting namespaces (#3944) * Parse the Flat OPC part root tag once when inheriting namespaces Fixes #3727 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada * #3944: drop the wall-clock assert from the long-root-tag test Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Haiku 5.5 Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Brandon Thomas Co-authored-by: Joseph Anglada Co-authored-by: dfgodvan Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> * Write a text-box story shared by many shapes only once on docx save (#3983) Fixes #3653 Co-authored-by: Claude Opus 5.5 * Keep pending formatting revisions when Style Inspector clears formatting (#3971) Fixes #3671 Co-authored-by: Claude Haiku 5.5 * Cap the headless ui_requests queue and keep one Paste Special request (#3997) Fixes #3641 Co-authored-by: Claude Haiku 5.5 * Validate PNG extension in file.exportPng (#3837) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * changes to filter dialogue * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada * Parse the Flat OPC part root tag once when inheriting namespaces (#3944) * Parse the Flat OPC part root tag once when inheriting namespaces Fixes #3727 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-au… * Khmer: ignore w14:ligatures when shaping Khmer text (#3921) Documents that Word saves with w14:ligatures="historicalDiscretional" (every Khmer run in typical Khmer OS files) turned liga and clig off. Khmer OS fonts build their syllables there, so pre-base vowels and stacked signs came out in the wrong order (αžŸαŸŠαžΎαž”αž’αž„αŸ’αž€αŸαž drawn as αžŸαžΈαž”αž’αž„αŸ’αž€αžαŸ). Khmer text is now shaped with the font's defaults, as Word does. Co-authored-by: Claude Sonnet 5.5 * Skip quoted attribute values when scanning settings.xml tags (#3954) Fixes #3682 Co-authored-by: Claude Haiku 5.5 * Rename kept docx parts whose names are not valid OPC part names (#3947) * Rename kept docx parts whose names are not valid OPC part names Fixes #3691 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada * Parse the Flat OPC part root tag once when inheriting namespaces (#3944) * Parse the Flat OPC part root tag once when inheriting namespaces Fixes #3727 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its re… * #3637: A table cell span from a .json file no longer overflows (#3927) Insert Column, Merge Cells and Delete Column did arithmetic on the raw CellProps::span, which a .json document can set to any u32, so a span near u32::MAX overflowed (a panic in debug, a wrapped span in release). They now work from the clamped span() and cap the result at MAX_COLS. * #3663: "NaN" in the Font dialog's Size box no longer closes the app (#3928) * #3663: "NaN" in the Font dialog's Size box no longer closes the app "NaN" parses as an f32 and clamp keeps NaN, so the preview font size was NaN and epaint's glyph assertion panicked while painting the dialog. The preview now drops non-finite sizes and shows 12 pt. * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * #3816: also assert the next paragraph's first line stays on page 0 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Co-authored-by: Joseph Anglada * Parse the Flat OPC part root tag once when inheriting namespaces (#3944) * Parse the Flat OPC part root tag once when inheriting namespaces Fixes #3727 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * Engine: no dead-code warnings on wasm (#3542) * fix(engine): do not treat decimal points as sentence boundaries in Sentence case In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case. Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point. Closes #3485 * proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 * fmt: rustfmt equation array centring * #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 * Engine: no dead-code warnings on wasm `cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned about five items whose only callers are platform-gated: - citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used only by the desktop (non-wasm) source import and template listing. - speech `voice_language`: used by espeak (Unix but macOS) and the macOS voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which also took in wasm). - speech `primary_tag`: used by those plus the Windows synthesizer, so `any(unix, windows, test)`. Each item now carries exactly its callers' gate rather than a blanket allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS and wasm32 (zero warnings each), clippy -D warnings, and the engine tests. Co-Authored-By: Claude Opus 5.5 * Integration: #1847 reject comment IDs above u32::MAX * Integration: #3267 leave footnote definitions out of link definitions * Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them * Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers * Integration: #2268 ODT lengths accept only the standard pc unit for picas * Integration: #2269 LaTeX lengths keep macro names case-sensitive * Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245 * Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes * Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name * Integration: #1985 only ^0nnn codes read as Windows-1252 * Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if) * Integration: #2185 hyperlink_target slices with get() and needs the whole field name * Integration: #2034 objects without a stacking order draw above the ones before them * Integration: #3434 drop review-bot notes from test doc comments * Integration: #3419 doc comments back on their own items (EquationFrame and its test above them) * Integration: #2334 test a formula string result continued in a CONTINUE record * Integration: #3429 the based-on-chain test's doc comment back above its test * Integration: #2453 drawing_payload test builds the shape fill as a Fill * Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them) * Integration: cargo fmt * Integration: #1728 close the layout test the merge resolution left open * Integration: cargo fmt * Integration: cargo fmt * Integration: cargo fmt * Integration: #2722 Dutch registry comment on its own line * Integration: #3203 English text for the keys the Romanian catalog lacks Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated. * Integration: #1076 English text for the keys the Slovenian catalog lacks Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated. * Integration: status lines list each change once Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617). * Integration: cargo xtask parity * Integration: cargo fmt * Integration: #1832 drop a useless .into() in the end-of-document citation test clippy::useless_conversion failed cargo xtask ci. * Integration: #2244 VML on/off values read case-insensitively by the shape fill reader Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill. * Integration: #2041 English text in the Polish catalog for the keys Italian gained polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated. * Integration: #2722 English text for four labels the Dutch and Danish catalogs lack every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated. * Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings #3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel. * IME: the caret follows the composition text (#3556) * Header and footer: a click in the other band edits it there (#3557) * README: the app icon above the title * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrelated issue #1344 is about the inspector missing hidden text, not about its removals going untracked, so the reference only sent a reader to the wrong place. Co-Authored-By: opencode * A version snapshot belongs to the document it was taken from `Session::set_document` resets everything about the open document β€” the document id, the document, the selection, the undo history, the password, the file name β€” but not the version snapshots. A snapshot taken of one document therefore survived `file.new`, `file.open`, `file.newFromTemplate` and `file.recover`, and `file.versions {"restore": index}` then put the previous document's content into the open one. The snapshots go with the document they were taken from: `set_document` clears them, so a restore can only ever bring back a version of the document that is open. Every caller replaces the document with another one (a new or opened file, a recovered copy, a merge result, a sheet of labels), so none of them loses anything it needs. Co-Authored-By: opencode * Docs: the inspector's tracking refusal and the version snapshot's scope Co-Authored-By: opencode * A comparison clears the version snapshots of the document it replaces `review.compare` and `review.combine` open the original with `s.doc = doc`, which bypasses `set_document` β€” so the version snapshots of the document the comparison replaces survived it, and a restore could put the previous document back over the comparison result. The snapshots go with the document they were taken from, as everywhere else. The document itself is still assigned directly: `set_document` resets the undo history, and the comparison's own undo step would go with it. Co-Authored-By: opencode * #3277: Test Inspector refusal without protection * #1327: Keep snapshots with their document through Compare * #1327: Preserve file identity after rejected document replacement * test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989) The future is polled by hand with no async runtime, so a blocking sleep is how the test models a slow dialog. πŸ€– Generated with Codebuff Co-authored-by: Codebuff * #3542: gate the label_of test off wasm32 Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Brandon Thomas * Drop U+FFFE and U+FFFF from ODT export text (#3817) Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58) Co-authored-by: Claude Haiku 5.5 * Cap the number of VML path commands parsed from a path attribute (#3951) Fixes #3683 Co-authored-by: Claude Haiku 5.5 * Return None from parse_number when the value overflows f64 (#3577) Found while testing crates/doc/src/locale_number.rs:104 Co-authored-by: Claude Opus 5.5 * Fix spurious ι›Ά after a group's trailing zeros in Chinese numerals (#3745) Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format) Co-authored-by: Claude Haiku 5.5 * picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830) Co-authored-by: Claude Sonnet 5.5 * Reject signs and non-digits in parse_date fields (#3575) Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date) Co-authored-by: Claude Opus 5.5 * Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816) * Restrict Editing: a file command that changes the document is checked too `always_allowed` waved through every `file.*` id, so on a protected document the file commands that do change it went unchecked: the privacy options and the properties wrote document settings, and the inspector's removals deleted content. The privacy options' own comment already said they were "refused on a protected document"; that wasn't true. The allow-list now names the file commands that don't change the open document, and the ones that do are checked like any other command. Reading the document stays allowed: the inspector without removals, the privacy settings, the info, and saving a version snapshot. Restoring a stored version replaces the whole document, and a snapshot's own protection is stale β€” restoring one lifted the protection and dropped its password. The live document's protection, password and formatting limit now survive a restore. Co-Authored-By: opencode * Restrict Editing: the user name can't change under protection `file.setAuthor` sets the name a protected document's named exception ranges are checked against, and the author new revisions and comments are recorded under. `always_allowed` waved through every `file.*` id, so on a protected document the name could be changed: an agent could take the identity of a named editor and unlock their exception range, and forge the authorship of the revisions it leaves behind. A protected document refuses the change now, with the rule Word uses for a content control's properties: stop protection first. The check is in the command itself β€” `file.setAuthor` is a pure command, so `check_before` never sees it β€” and in `check_before` for the day it isn't. Word lets the user name change under protection: it is a preference there. This project doesn't, because that identity gates what may be edited. Co-Authored-By: opencode * Restrict Editing: a protected document's protection needs an explicit stop The guard in `restrict` refused a protected document only when its protection had a password, so a password-less one could have its protection changed β€” and `file.protect {"mode": "none"}` stopped it outright. An agent could strip a user's protection silently and then edit the document freely. Two rules now, as Word has them (Stop Protection first): - The protection of a protected document doesn't change, whatever the mode. The formatting-only protection doesn't count: it stores the mode "none", and it limits the styles, not the editing. - Stopping the protection is explicit. The "none" mode is the shorthand for it, but on a protected document only `{"stop": true}` stops it β€” the way the pane's Stop Protection button does it β€” so a command that merely names a mode can't strip it. The password still guards the stop, and the formatting-only path (the mode "none" with the formatting restriction) still works. `cmd::tools::tests::protection_blocks_edits` stops the protection with `{"mode": "none"}`; it has to stop it explicitly now. Co-Authored-By: opencode * Restrict Editing: the post-check compares the settings and the metadata `outside_unchanged` is the check that rolls a command back when it changed anything outside the editable regions. It compared the text, the formatting, the page setup, the document defaults and the styles β€” but not the document's settings, its metadata, its custom properties, the resolved flag of its comments, its list definitions or its bibliography sources. A command that changed the metadata of a protected document was accepted and never rolled back: `file.properties {"title": "Forged title"}` on a protected document with an exception changed the title and stayed. The settings, the core properties, the custom properties, the comments' resolved flags, the list definitions and the sources are compared now, so a command that changes them is rolled back like any other change outside the editable regions. The revisions and the media store stay out of it on purpose: an edit inside an exception legitimately leaves a revision, and a picture inserted there adds to the media store. The list definitions and the sources are compared like the styles: the ones the document had must be unchanged, but a new one may be added β€” a list or a citation inserted inside an editable region adds its definition, like applying a style adds one. Saving is unaffected: it writes the metadata, but `file.save` is a pure command and isn't post-checked, and the protection itself is changed by `review.restrict`, which is always allowed. Co-Authored-By: opencode * The protection is stopped explicitly, and the docs say so The test that stopped the protection used the "none" shorthand, which a protected document now refuses; it stops it the explicit way instead, and pins that the shorthand doesn't stop it. `file.protect` says the same: it starts enforcement, and "none" doesn't stop a protected document. Co-Authored-By: opencode * Docs: the Restrict Editing gaps that are closed Co-Authored-By: opencode * A password guards the protection whatever its mode The guard that stops the protection of a protected document from changing asked whether the editing was restricted, so a formatting-only protection β€” the mode "none", which limits the styles rather than the editing β€” was exempted. A formatting-only protection can carry a password, and that password guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password": "other"}` replaced the protection and the password with ones of the caller's choosing, and the protection could then be stopped with them. A password guards the protection whatever its mode, as it did before. Co-Authored-By: opencode * The CLI's --author names the session, it doesn't change the document `--author` dispatched `file.setAuthor`, which a protected document refuses now that the name is checked as the identity its exceptions are matched against β€” so a script that named itself on a protected file failed before it ran a single command. The flag names the session, not the open document, so it sets the name directly, with the same normalisation and blank-name check the command uses. `file.setAuthor` keeps its protection check for the callers that mean to change the document's identity. Co-Authored-By: opencode * Docs: the progress docs' status lines carry today's date Co-Authored-By: opencode * The document's properties and a version restore need the protection stopped Two gaps the review found, both of them a change to a protected document that went through without the protection seeing it. The properties (`file.properties`) and the privacy options (`file.privacyOptions`) are document-wide state, and they aren't tracked: under tracked changes they changed the document with no revision to show for it, and under the other modes they were only rolled back by the post-check, after the command had run. They are refused now, before the command runs, as the content control's properties are. A version restore (`file.versions`) replaces the whole document, including the content the protection kept read-only, and it did that without the password: `file.versions` is a pure command, so the protection never saw it. Keeping the live protection and its password stopped them being dropped, but the content underneath was still replaced. A restore needs the protection stopped first, and the password with it when the document has one. The live protection still wins over the snapshot's stale one, so a restore can't lock the user out of their own document either. Co-Authored-By: opencode * Layout: keep with next takes the 0.01 pt of slack the other fit checks take place_para_in_flow moved a paragraph kept with the next one to a new page when the two needed more than the room left by any amount, while lines, rows and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a rounding error over the page bottom moved on alone. * Release 0.10.0 Co-Authored-By: Claude Opus 5.5 * The inspector's removals are refused in every protected mode `file.inspect` with removals takes content out of the document β€” the comments, the revisions, the properties, the hidden text, the headers. Under readOnly and comments it was refused, or rolled back by the post-check where the document had an exception; under trackedChanges nothing stopped it, so a document that tracks changes could have its comments and revisions taken out with no revision to show for it. The removals are refused now, in every mode a protected document can be in, next to the properties and the privacy options. An inspection without removals only reads the document, and still works. The protection test covers the modes now, not only readOnly. Co-Authored-By: opencode * Docs: the session author, not the user name "the user name can't change while the document is protected" was the wrong term twice over: it is the session author β€” the name new tracked changes and comments are recorded under, and the one a named exception is matched against β€” and it is `file.setAuthor` that refuses to change it. The CLI's `--author` names the session directly, so it works on a protected document. The parity row, the two status lines and the two progress rows now say that. Co-Authored-By: opencode * #3279: Keep protected session author fixed in CLI and Options * The Document Inspector refuses its removals while the document tracks changes The inspector's removals delete straight from the document: the hidden text, the comments, the revisions and the headers go without ever reaching the engine's tracked-delete path. On a document that tracks changes β€” and `trackedChanges` protection turns tracking on β€” the content was gone with no revision to show for it, and the revision trail was broken behind the user's back. The removals are not tracked, so they cannot be: rather than untrack them silently, the inspector refuses the whole removal form while the document tracks changes, and says why ("the document tracks changes: stop tracking before removing content"). The check comes before anything is removed, so the document is left untouched, and reading the document β€” the inspector without removals β€” keeps working. A document that does not track changes is inspected as before. Co-Authored-By: opencode * The inspector's tracking refusal doesn't cite an unrel… * #3968: drop the wall-clock assert from the settings-load test Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Haiku 5.5 Co-authored-by: Dylan M. Taylor Co-authored-by: Codebuff Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com> Co-authored-by: Divyanshu Gupta Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Co-authored-by: vangel76 Co-authored-by: aiongg Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com> Co-authored-by: bflatastic Co-authored-by: Brandon Thomas Co-authored-by: Mat Strange Co-authored-by: opencode Co-authored-by: skylar taylor-barrick Co-authored-by: Brandon Thomas Co-authored-by: Joseph Anglada Co-authored-by: dfgodvan Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Co-authored-by: Michael Higgins <55243596+Higgs32584@users.noreply.github.com> Co-authored-by: In Mean <6434741+inmean@users.noreply.github.com> Co-authored-by: Jayesh Prajapati <156836276+Jayesh-Prajapati9@users.noreply.github.com> --- crates/ui-egui/src/options.rs | 42 ++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/options.rs b/crates/ui-egui/src/options.rs index fc3a559d7..44c1837c8 100644 --- a/crates/ui-egui/src/options.rs +++ b/crates/ui-egui/src/options.rs @@ -717,7 +717,15 @@ fn language_picker(app: &mut WordApp, ui: &mut Ui) { pub fn lenient(input: &Value) -> T { let mut base = serde_json::to_value(T::default()).unwrap_or(Value::Null); if base.is_object() { - overlay::(&mut base, &mut Vec::new(), input, 0); + let mut extra = Vec::new(); + overlay::(&mut base, &mut Vec::new(), input, 0, &mut extra); + // Keys with no slot in the defaults (write-only legacy settings, or unknown ones) are + // kept only after the loop, so `base` stays the size of `T` while each key is checked. + for (path, k, v) in extra { + if let Some(parent) = object_at(&mut base, &path) { + parent.insert(k, v); + } + } } serde_json::from_value(base).unwrap_or_default() } @@ -731,18 +739,34 @@ fn object_at<'a>(root: &'a mut Value, path: &[String]) -> Option<&'a mut Map(root: &mut Value, path: &mut Vec, input: &Value, depth: usize) { +fn overlay( + root: &mut Value, + path: &mut Vec, + input: &Value, + depth: usize, + extra: &mut Vec<(Vec, String, Value)>, +) { let Some(map) = input.as_object() else { return }; for (k, v) in map { let Some(parent) = object_at(root, path) else { return }; let old = parent.get(k).cloned(); if depth < 4 && v.is_object() && old.as_ref().is_some_and(Value::is_object) { path.push(k.clone()); - overlay::(root, path, v, depth + 1); + overlay::(root, path, v, depth + 1, extra); path.pop(); continue; } parent.insert(k.clone(), v.clone()); + if old.is_none() { + let ok = serde_json::from_value::(root.clone()).is_ok(); + if let Some(parent) = object_at(root, path) { + parent.remove(k); + } + if ok { + extra.push((path.clone(), k.clone(), v.clone())); + } + continue; + } if serde_json::from_value::(root.clone()).is_err() && let Some(parent) = object_at(root, path) { @@ -806,6 +830,18 @@ mod tests { use crate::theme::Appearance; use crate::{Services, UiState, WordApp}; + #[test] + fn unknown_settings_keys_load_in_linear_time() { + let mut map = serde_json::Map::new(); + for i in 0..20_000 { + map.insert(format!("unknownKey{i}"), json!(i)); + } + map.insert("miniToolbar".into(), json!(false)); + let bytes = serde_json::to_vec(&serde_json::Value::Object(map)).unwrap(); + let state = UiState::from_json(&bytes).unwrap(); + assert!(!state.mini_toolbar); + } + fn app() -> WordApp { WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), Services::default()) }