diff --git a/crates/docx/src/write/mod.rs b/crates/docx/src/write/mod.rs index 83a54b079..b2a1aaf45 100644 --- a/crates/docx/src/write/mod.rs +++ b/crates/docx/src/write/mod.rs @@ -67,6 +67,8 @@ pub(crate) struct Writer<'d> { para_ids: HashMap, /// A note reference mark to put at the start of the next paragraph. pending_mark: Option<&'static str>, + /// The note being written (is footnote, part id): its reference to itself is the mark above. + current_note: Option<(bool, u32)>, /// Media keys actually referenced by a written drawing. used_media: std::collections::BTreeSet, } @@ -88,6 +90,7 @@ pub fn write(doc: &Document) -> Result, DocxError> { hf: Vec::new(), para_ids: HashMap::new(), pending_mark: None, + current_note: None, used_media: Default::default(), }; wr.assign_media(); @@ -172,7 +175,7 @@ pub fn write(doc: &Document) -> Result, DocxError> { let nid = k.to_string(); w.open(tag, &[("w:id", &nid)]); let blocks: Blocks = doc.parts.get(&pid).map(|p| p.blocks.clone()).unwrap_or_default(); - wr.note_blocks(&mut w, &blocks, &mut prels, foot); + wr.note_blocks(&mut w, &blocks, &mut prels, foot, pid); w.close(tag); } w.close(root); @@ -440,9 +443,11 @@ impl Writer<'_> { } /// Note stories: the first paragraph starts with the note's own reference mark. - fn note_blocks(&mut self, w: &mut W, blocks: &Blocks, rels: &mut PartRels, foot: bool) { + fn note_blocks(&mut self, w: &mut W, blocks: &Blocks, rels: &mut PartRels, foot: bool, part: u32) { self.pending_mark = Some(if foot { "w:footnoteRef" } else { "w:endnoteRef" }); + self.current_note = Some((foot, part)); self.blocks(w, blocks, rels, false, 0); + self.current_note = None; self.pending_mark = None; } } diff --git a/crates/docx/src/write/story.rs b/crates/docx/src/write/story.rs index 1bad0e9f9..c2036c8f8 100644 --- a/crates/docx/src/write/story.rs +++ b/crates/docx/src/write/story.rs @@ -279,6 +279,10 @@ impl Writer<'_> { InlineObject::Field { instr, result, locked } => self.field(w, instr, result, *locked, props), InlineObject::NoteRef { kind, id, custom } => { let foot = *kind == NoteKind::Footnote; + if self.current_note == Some((foot, *id)) { + // The note's own number: already written as the w:footnoteRef / w:endnoteRef mark. + return; + } let nid = self.note_id(*id, foot); let mut p = props.clone(); if p.style.is_none() && p.vert_align.is_none() { diff --git a/crates/docx/tests/roundtrip.rs b/crates/docx/tests/roundtrip.rs index 0ac1a1291..ad84f1a6c 100644 --- a/crates/docx/tests/roundtrip.rs +++ b/crates/docx/tests/roundtrip.rs @@ -448,6 +448,33 @@ fn notes_round_trip() { assert_eq!(r.settings.endnote_format, NumFormat::UpperRoman); } +/// `references.footnote` starts the note's own text with a reference to the note, so the editor shows +/// its number. In the file that is the `w:footnoteRef` mark; a `w:footnoteReference` there would make +/// the note cite itself, which LibreOffice refuses to open. +#[test] +fn note_never_references_itself() { + let mut d = Document::new(); + let f = d.add_part(PartKind::Footnote, Vec::new()); + let mut note = Paragraph::with_text(" The note.", CharProps::default()); + note.insert_object(0, InlineObject::NoteRef { kind: NoteKind::Footnote, id: f, custom: String::new() }, &CharProps::default()).unwrap(); + d.parts.get_mut(&f).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: String::new() }, &CharProps::default()).unwrap(); + d.body = vec![para_block(p)]; + + let bytes = wordcraft_docx::write(&d).expect("write"); + let mut zip = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap(); + let mut xml = String::new(); + std::io::Read::read_to_string(&mut zip.by_name("word/footnotes.xml").unwrap(), &mut xml).unwrap(); + assert!(!xml.contains("").count(), 1, "{xml}"); + + let r = wordcraft_docx::read(&bytes).expect("read"); + let got = paras(&r)[0]; + let Some(InlineObject::NoteRef { id, .. }) = got.objects.first() else { panic!("{:?}", got.objects) }; + assert_eq!(part_text(&r, Some(*id)).trim(), "The note."); +} + #[test] fn tracked_changes_round_trip() { let mut d = Document::new();