M6: footnotes no longer cite themselves in saved .docx files (#53)

references.footnote starts the note's text with a NoteRef to the note itself,
so the editor shows its number. The writer already puts the w:footnoteRef
mark at the start of every note, then also wrote that NoteRef as a
w:footnoteReference, so each note referenced itself. LibreOffice refuses to
open such a file ("source file could not be loaded").

Skip a note's reference to itself while writing that note; the mark stands
in for it. Notes read from Word files never contain one, so they are
unaffected.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AiBizzApps
2026-10-09 08:48:51 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 3f166a94f5
commit 5a94aa77d9
3 changed files with 38 additions and 2 deletions
+7 -2
View File
@@ -67,6 +67,8 @@ pub(crate) struct Writer<'d> {
para_ids: HashMap<usize, String>,
/// 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<String>,
}
@@ -88,6 +90,7 @@ pub fn write(doc: &Document) -> Result<Vec<u8>, 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<Vec<u8>, 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;
}
}
+4
View File
@@ -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() {
+27
View File
@@ -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("<w:footnoteReference"), "a footnote cites itself: {xml}");
assert_eq!(xml.matches("<w:footnoteRef/>").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();