Gap 19: ODT export keeps replies to replies

Export wrote only the direct replies of a comment, so a reply to a reply
was lost on save. Every reply in a comment's thread is written now, each
answering the comment that starts the thread, as LibreOffice's threads
are flat. A reply chain that loops (hostile) is written once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
This commit is contained in:
Claude committed 2026-10-31 23:53:03 +00:00
1 parent 084d26a3c8
commit bc50c6f7f9
2 files changed
+29 -3

No files matched your search

+12 -3
View File
@@ -139,12 +139,21 @@ fn rich_inlines(doc: &Document, o: &InlineObject, f: &Fmt, opts: FlowOpts) -> Op
let endnote = *kind == wordcraft_doc::para::NoteKind::Endnote;
vec![Inline::Note { endnote, custom: custom.clone(), blocks }]
}
// The comment, then its replies, which have no place of their own.
// The comment, then the replies in its thread (replies to replies too, each answering the
// comment that starts the thread), which have no place of their own.
InlineObject::CommentStart { id } if opts.comments => {
let Some(c) = doc.comments.get(id) else { return Some(Vec::new()) };
let mut out = vec![Inline::Comment { id: *id, comment: Box::new(flow_comment(doc, c, None, opts)) }];
for (rid, r) in doc.comments.iter().filter(|(_, r)| r.parent == Some(*id)) {
out.push(Inline::Comment { id: *rid, comment: Box::new(flow_comment(doc, r, Some(*id), opts)) });
let mut thread = std::collections::BTreeSet::from([*id]);
let mut k = 0;
while let Some(Inline::Comment { id: at, .. }) = out.get(k) {
let at = *at;
k += 1;
for (rid, r) in doc.comments.iter().filter(|(_, r)| r.parent == Some(at)) {
if thread.insert(*rid) {
out.push(Inline::Comment { id: *rid, comment: Box::new(flow_comment(doc, r, Some(*id), opts)) });
}
}
}
out
}
@@ -168,3 +168,20 @@ fn unmatched_and_hostile_comments_do_not_panic() {
}
}
}
#[test]
fn a_reply_to_a_reply_is_kept() {
let xml = concat!(
r#"<text:p><office:annotation office:name="a"><dc:creator>A</dc:creator><text:p>Root</text:p></office:annotation>x<office:annotation-end office:name="a"/>"#,
r#"<office:annotation office:name="b" loext:parent-name="a"><dc:creator>B</dc:creator><text:p>Reply</text:p></office:annotation>"#,
r#"<office:annotation loext:parent-name="b"><dc:creator>C</dc:creator><text:p>Reply to reply</text:p></office:annotation></text:p>"#,
);
let d = import(&body(xml)).unwrap();
assert_eq!(d.comments.len(), 3);
// LibreOffice's threads are flat: every reply answers the comment that starts them.
let back = import(&export(&d).unwrap()).unwrap();
let got = comments(&back);
assert_eq!(got.len(), 3, "{got:?}");
assert_eq!(got[2].4.as_deref(), Some("Root"));
assert_eq!(got[2].5, "Reply to reply");
}