diff --git a/crates/formats/src/model/from_doc.rs b/crates/formats/src/model/from_doc.rs index ef90a5bd4e..d5da5a3801 100644 --- a/crates/formats/src/model/from_doc.rs +++ b/crates/formats/src/model/from_doc.rs @@ -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 } diff --git a/crates/formats/src/odt/tests_more/comments.rs b/crates/formats/src/odt/tests_more/comments.rs index 0890f99d23..0e3a43a7d8 100644 --- a/crates/formats/src/odt/tests_more/comments.rs +++ b/crates/formats/src/odt/tests_more/comments.rs @@ -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#"ARootx"#, + r#"BReply"#, + r#"CReply to reply"#, + ); + 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"); +}