Merge pull request #5396 from storytold/fix/sec-followup-5303

Threads: a frame left alone by a dropped repeat is flowed again
This commit is contained in:
BFlat
2026-11-03 23:13:19 +07:00
committed by GitHub
2 changed files with 41 additions and 8 deletions
+18 -8
View File
@@ -97,15 +97,25 @@ pub(crate) fn reflow(before: &Document, doc: &mut Document) {
// copy its text into the story each time, and write the grown story back for the next thread
// to copy again. Only its first place counts.
let mut seen = std::collections::HashSet::new();
let threads: Vec<Vec<NodeId>> =
std::mem::take(&mut doc.text_threads).into_iter().map(|t| t.into_iter().filter(|id| seen.insert(*id)).collect()).collect();
// A thread that lost a frame that way is flowed again: its frames' layout was made for the
// thread as listed (a frame left alone must lose its threaded continuation).
let threads: Vec<(Vec<NodeId>, bool)> = std::mem::take(&mut doc.text_threads)
.into_iter()
.map(|t| {
let listed = t.len();
let t: Vec<NodeId> = t.into_iter().filter(|id| seen.insert(*id)).collect();
let lost = t.len() != listed;
(t, lost)
})
.collect();
let mut kept = vec![];
for thread in threads {
let changed = thread.iter().any(|id| match (before.node(*id), doc.node(*id)) {
(Some(a), Some(b)) => !std::ptr::eq(a, b),
(None, None) => false,
_ => true,
});
for (thread, lost) in threads {
let changed = lost
|| thread.iter().any(|id| match (before.node(*id), doc.node(*id)) {
(Some(a), Some(b)) => !std::ptr::eq(a, b),
(None, None) => false,
_ => true,
});
// When no frame changed, the thread keeps its area-type frames and their text is not copied.
if !changed {
let live: Vec<NodeId> = thread.into_iter().filter(|id| text_of(doc, *id).is_some()).collect();
+23
View File
@@ -58,6 +58,29 @@ fn frames_listed_twice_flow_the_story_once() {
assert_eq!(format!("{}{}", plain(fa), plain(fb)), TEXT.repeat(4));
}
/// A frame left alone when a repeated listing is dropped (`[[a, b], [b, c]]` → `[[a, b], [c]]`)
/// is flowed again as a story of its own, even when no frame changed: it no longer continues a
/// paragraph from the frame before it.
#[test]
fn a_frame_left_alone_by_a_dropped_repeat_stops_continuing() {
let (mut s, a, b) = setup();
s.execute("text.thread.create", &json!({})).unwrap();
let c =
s.execute("text.create", &json!({"x": 400, "y": 10, "text": "Tail", "area": {"width": 150, "height": 60}})).unwrap()["id"].as_u64().unwrap();
let mut d = (*s.doc().unwrap().doc).clone();
let (fa, fb, fc) = (NodeId(a), NodeId(b), NodeId(c));
d.text_threads = vec![vec![fa, fb], vec![fb, fc]];
if let Some(NodeKind::Text(t)) = d.node_mut(fc).map(|n| &mut n.kind) {
t.first_para_continues = true;
}
let before = d.clone();
reflow(&before, &mut d);
assert_eq!(d.text_threads, vec![vec![fa, fb]]);
let NodeKind::Text(t) = &d.node(fc).unwrap().kind else { panic!("not text") };
assert!(!t.first_para_continues, "an unthreaded frame continues nothing");
assert_eq!(t.plain_text(), "Tail");
}
#[test]
fn edits_copy_only_the_stories_of_threads_whose_frames_changed() {
let (mut s, a, b) = setup();