M-ODT gap 19: lists without an xml:id kept as read, given one

Older files' lists have no xml🆔 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
This commit is contained in:
Claude
2026-11-04 00:34:28 +00:00
parent 0ce44996ec
commit f4dfd34373
3 changed files with 30 additions and 4 deletions
+2 -3
View File
@@ -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<Ev> {
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
@@ -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#"<text:list xml:id="list1" text:style-name="L1">"#, r#"<text:list text:style-name="L1">"#).replace(
r#"<text:list xml:id="list2" text:continue-list="list1" text:style-name="L1">"#,
r#"<text:list text:continue-numbering="true" text:style-name="L1">"#,
);
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#"<text:list xml:id="wclstk1" text:style-name="L1">"#) && 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}");
}
+12 -1
View File
@@ -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<String>,
/// 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<std::collections::BTreeSet<String>>,
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("<text:list").map(|i| i + "<text:list".len())
{
self.kept_lists += 1;
markup.insert_str(at, &format!(" xml:id=\"{}lstk{}\"", self.prefix, self.kept_lists));
}
let last = match blocks.get(i + ix.1.max(1) - 1) {
Some(FBlock::Para(q)) => 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("<text:list").and_then(|i| markup.get(i..)).and_then(list_xml_id)
{
if l.list != 0 {
self.list_xml.entry(l.list).or_insert_with(|| id.clone());