From 33f00e8dfe393653b86cf556de5ed3230e178d8a Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sun, 18 Oct 2026 09:43:04 +0200 Subject: [PATCH] Keep custom footnote/endnote marks as text inside the note on DOCX save (#2647) Found while testing crates/docx/src/write/story.rs:512-530 (InlineObject::NoteRef, the `current_note == Some(..)` branch) Co-authored-by: Claude Haiku 5.5 --- crates/docx/src/write/story.rs | 7 ++++++- crates/docx/tests/roundtrip.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/crates/docx/src/write/story.rs b/crates/docx/src/write/story.rs index 07f0795db..d41f0e418 100644 --- a/crates/docx/src/write/story.rs +++ b/crates/docx/src/write/story.rs @@ -520,7 +520,12 @@ impl Writer<'_> { self.rev_open(w, props); w.open("w:r", &[]); self.rpr(w, &p); - w.empty(mark, &[]); + // A custom mark is plain text; the placeholder would turn it into a number. + if custom.is_empty() { + w.empty(mark, &[]); + } else { + w.leaf("w:t", &[("xml:space", "preserve")], custom); + } w.close("w:r"); self.rev_close(w, props); } diff --git a/crates/docx/tests/roundtrip.rs b/crates/docx/tests/roundtrip.rs index 6d3a9f872..b8b8e330f 100644 --- a/crates/docx/tests/roundtrip.rs +++ b/crates/docx/tests/roundtrip.rs @@ -707,6 +707,32 @@ fn note_never_references_itself() { assert_eq!(part_text(&r, Some(*id)).trim(), "The note."); } +/// A note with a custom mark keeps it in the note's own text: a plain run with the mark, not the +/// `w:footnoteRef` / `w:endnoteRef` placeholder that Word replaces by the automatic number. +#[test] +fn custom_note_mark_stays_in_the_note() { + let mut d = Document::new(); + let f = d.add_part(PartKind::Footnote, Vec::new()); + let e = d.add_part(PartKind::Endnote, Vec::new()); + for (id, kind) in [(f, NoteKind::Footnote), (e, NoteKind::Endnote)] { + let mut note = Paragraph::with_text(" The note.", CharProps::default()); + note.insert_object(0, InlineObject::NoteRef { kind, id, custom: "*".into() }, &CharProps::default()).unwrap(); + d.parts.get_mut(&id).unwrap().blocks = vec![para_block(note)]; + } + let mut p = Paragraph::with_text("Text", CharProps::default()); + p.insert_object(4, InlineObject::NoteRef { kind: NoteKind::Footnote, id: f, custom: "*".into() }, &CharProps::default()).unwrap(); + let end = p.len(); + p.insert_object(end, InlineObject::NoteRef { kind: NoteKind::Endnote, id: e, custom: "*".into() }, &CharProps::default()).unwrap(); + d.body = vec![para_block(p)]; + + let bytes = wordcraft_docx::write(&d).expect("write"); + for (file, mark) in [("word/footnotes.xml", ""), ("word/endnotes.xml", "")] { + let xml = part_xml(&bytes, file); + assert!(!xml.contains(mark), "{file}: the custom mark became an automatic number: {xml}"); + assert!(xml.contains(r#"*"#), "{file}: {xml}"); + } +} + /// A note read from a Word file starts with its own number (a reference to itself, from the /// `w:footnoteRef` mark). Saving writes that mark back exactly once, with its character style, and /// never as a `w:footnoteReference`; reading the saved file gives the same note again.