From f4dfd34373a594d444fd37a80ccd871f41ca6cff Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 00:34:28 +0000 Subject: [PATCH] M-ODT gap 19: lists without an xml:id kept as read, given one Older files' lists have no xml:id: kept as read all the same, given one on save so a list going on with it can name it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC --- crates/formats/src/odt/body.rs | 5 ++--- crates/formats/src/odt/tests_more/lossless.rs | 16 ++++++++++++++++ crates/formats/src/odt/writer.rs | 13 ++++++++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/crates/formats/src/odt/body.rs b/crates/formats/src/odt/body.rs index 756e1d708a..fdff228aa7 100644 --- a/crates/formats/src/odt/body.rs +++ b/crates/formats/src/odt/body.rs @@ -357,8 +357,8 @@ impl<'a> Body<'a> { /// [`Body::feed_at`] for a top-level list (ยง5.3.1): kept whole, as read, when each of its /// paragraphs is (a paragraph's own markup can't say how its list numbers and draws its /// labels: a label followed by a space, a picture bullet, an unnumbered header). Not one - /// continuing another by its `xml:id`, which WordCraft may write anew, nor one without - /// its own, which a list continuing it can't name. `None` when it took the event. + /// continuing another by its `xml:id`, which WordCraft may write anew (one without its own + /// is given one, for a list continuing it to name). `None` when it took the event. fn whole_list(&mut self, ev: Ev, at: (usize, usize)) -> Option { match &ev { Ev::Start(q, a) @@ -367,7 +367,6 @@ impl<'a> Body<'a> { && self.lists.is_empty() && self.para.is_none() && get(a, "text:continue-list").is_none() - && get(a, "xml:id").is_some_and(|i| !i.is_empty()) && !self.raw.is_empty() && self.in_text && self.skip == 0 diff --git a/crates/formats/src/odt/tests_more/lossless.rs b/crates/formats/src/odt/tests_more/lossless.rs index 8a1c068c82..b88a18aebd 100644 --- a/crates/formats/src/odt/tests_more/lossless.rs +++ b/crates/formats/src/odt/tests_more/lossless.rs @@ -725,3 +725,19 @@ fn a_section_ending_in_a_list_closes_before_the_list_goes_on() { } check(&e, false); } + +#[test] +fn a_list_without_an_id_is_kept_and_given_one() { + // Older files' lists have no `xml:id`: kept as read, one is given, and the list going on with + // it (by `text:continue-numbering`, read as the same list) names it when written from the model. + let content = LISTS.content.replace(r#""#, r#""#).replace( + r#""#, + r#""#, + ); + let leaked: &'static str = Box::leak(content.into_boxed_str()); + let d = import(&package(&Sample { name: "older lists", content: leaked, styles: "", meta: "" })).unwrap(); + let c = content_of(&export(&d).unwrap()); + assert!(c.contains(r#""#) && c.contains(r#"text:label-followed-by="space""#), "{c}"); + let going_on = c.split("Between the lists.").nth(1).unwrap_or(""); + assert!(going_on.contains(r#"text:continue-list="wclstk1""#) || going_on.contains(r#"text:continue-numbering="true""#), "{c}"); +} diff --git a/crates/formats/src/odt/writer.rs b/crates/formats/src/odt/writer.rs index c31446859d..4244652519 100644 --- a/crates/formats/src/odt/writer.rs +++ b/crates/formats/src/odt/writer.rs @@ -226,6 +226,8 @@ pub(super) struct Writer { /// The file's tracked changes markup written as read marks, written as read among the /// regions (each once). kept_changes: std::collections::BTreeSet, + /// Lists kept as read given an `xml:id` (see `kept_run`). + kept_lists: usize, /// The file's own package paths, kept as read: names WordCraft's own pictures don't take. pub(super) reserved: std::sync::Arc>, pub(super) kept_used: super::kept::Needed, @@ -775,6 +777,15 @@ impl Writer { if !self.kept_markup(&ix.0, &mut markup) { return None; } + // A list without its own `xml:id` (older files' lists) is given one, for a list going on + // with it to name. + if p.list.is_some() + && list_xml_id(&ix.0).is_none() + && let Some(at) = markup.find(" q.wrap.as_deref().map(|w| (Vec::new(), w.1)), _ => None, @@ -785,7 +796,7 @@ impl Writer { self.master = None; // A list kept so: a list continuing it, written from the model, names it. if let Some(l) = p.list - && let Some(id) = list_xml_id(&ix.0) + && let Some(id) = markup.find("