From 34ec59fee1a5ea0f348266e3de2abb4d8db6945c Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 24 Oct 2026 21:04:52 +0000 Subject: [PATCH] Table of contents: Replace and Remove take the heading paragraph above the field's paragraph with the table (#4039) --- crates/engine/src/cmd/references.rs | 18 ++++++++++++++---- crates/engine/src/tests.rs | 4 ++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/cmd/references.rs b/crates/engine/src/cmd/references.rs index 645325e99..6205fcde6 100644 --- a/crates/engine/src/cmd/references.rs +++ b/crates/engine/src/cmd/references.rs @@ -338,21 +338,31 @@ fn toc_range(s: &Session) -> Option<(usize, usize)> { } /// Removes the TOC blocks `start..=end` and returns where the TOC was. A field paragraph that ends -/// a section stays, emptied, so its section break isn't lost (#3889). +/// a section stays, emptied, so its section break isn't lost (#3889). A heading paragraph of its +/// own just above the field's paragraph (Word's tables start the field after their heading) goes +/// with the table. fn remove_toc(s: &mut Session, start: usize, end: usize) -> Result { for i in (start + 1..=end).rev() { s.doc.remove_block(StoryRef::Body, &Path::top(i))?; } - match s.doc.para_mut(StoryRef::Body, &Path::top(start)) { + let at = match s.doc.para_mut(StoryRef::Body, &Path::top(start)) { Ok(p) if p.section.is_some() => { *p = Paragraph { section: p.section.take(), ..Paragraph::new() }; - Ok(start + 1) + start + 1 } _ => { s.doc.remove_block(StoryRef::Body, &Path::top(start))?; - Ok(start) + start } + }; + let titled = start.checked_sub(1).and_then(|i| s.doc.body.get(i)).and_then(|b| b.as_para()).is_some_and(|p| { + p.props.style.as_deref() == Some("TOCHeading") && p.section.is_none() && toc_field(p).is_none() + }); + if titled { + s.doc.remove_block(StoryRef::Body, &Path::top(start - 1))?; + return Ok(at - 1); } + Ok(at) } /// The caret is in the table of contents (its heading or an entry), where Word's context menu diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 99988fef7..03dd3e7a4 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -3421,10 +3421,14 @@ fn toc_in_columns_with_localized_styles_keeps_its_layout_through_update_table() // The field's paragraph ends the one-column section: replacing or removing the TOC keeps it. let columns = |s: &Session| s.doc.sections().iter().map(|(_, x)| x.columns.count).collect::>(); assert_eq!(columns(&s), [1, 2, 1]); + // The heading goes with the table: one "Contents" after Replace, none after Remove. + let titles = |s: &Session| s.doc.body.iter().filter_map(|b| b.as_para()).filter(|p| p.plain_text() == "Contents").count(); run(&mut s, "references.toc", json!({"levels": 2})); assert_eq!(columns(&s), [1, 2, 1], "{}", text(&s)); + assert_eq!(titles(&s), 1, "{}", text(&s)); run(&mut s, "references.removeToc", json!({})); assert_eq!(columns(&s), [1, 2, 1], "{}", text(&s)); + assert_eq!(titles(&s), 0, "{}", text(&s)); } /// #2182: with two TOCs only the one Update Table would refresh is offered Update Field; the other