mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-26 03:17:28 -06:00
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:
co-authored by
Claude Opus 5.5
parent
3f166a94f5
commit
5a94aa77d9
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user