Copy: bound the anchored-object walk through cells and notes

Copying a story gives the objects anchored in its table cells and
notes fresh ids down to 64 levels, as the table copy does. A copy whose
stories below that level hold anchored objects, tables or notes is
refused with the nested-too-deeply error instead of recursing without
limit.
This commit is contained in:
dotCooCoo committed 2026-11-12 12:57:17 -08:00
1 parent b30d39fdec
commit 07d264fd23
1 file changed
+66 -4
+66 -4
View File
@@ -1281,21 +1281,41 @@ fn retarget_xrefs(copy: &mut Story, orig: &Story, map: &HashMap<u64, u64>, depth
}
}
/// Fresh ids for the objects anchored in story copy `st`, its table cells, footnotes and endnotes, at every depth
/// (#1522), each `depth` levels deep. False when one of them nests groups deeper than [`MAX_COPY_DEPTH`].
/// Fresh ids for the objects anchored in story copy `st`, its table cells, footnotes and endnotes down to
/// [`MAX_COPY_DEPTH`] levels (#1522), each `depth` levels deep. False when one of them nests groups deeper than
/// [`MAX_COPY_DEPTH`], or when the stories below that level hold anchored objects, tables or notes.
#[must_use = "false means the copy is incomplete and must not be inserted"]
fn renumber_anchored(dst: &mut Document, src: &Document, st: &mut Story, stories: &mut HashMap<StoryId, StoryId>, depth: usize) -> bool {
renumber_anchored_in(dst, src, st, stories, depth, 0)
}
/// [`renumber_anchored`] for a story `level` table cells and notes below the story copy.
#[must_use = "false means the copy is incomplete and must not be inserted"]
fn renumber_anchored_in(
dst: &mut Document,
src: &Document,
st: &mut Story,
stories: &mut HashMap<StoryId, StoryId>,
depth: usize,
level: usize,
) -> bool {
let mut complete = true;
for o in &mut st.objects {
complete &= renumber_in(dst, src, &mut Arc::make_mut(o).item, stories, depth, false);
}
if level >= MAX_COPY_DEPTH {
let leaf = |s: &Story| s.objects.is_empty() && s.tables.is_empty() && s.notes.is_empty() && s.endnotes.is_empty();
return complete
&& st.tables.values().all(|t| t.cells.iter().all(|c| leaf(&c.text)))
&& st.notes.iter().chain(&st.endnotes).all(|n| leaf(&n.text));
}
for t in st.tables.values_mut() {
for c in &mut Arc::make_mut(t).cells {
complete &= renumber_anchored(dst, src, &mut c.text, stories, depth);
complete &= renumber_anchored_in(dst, src, &mut c.text, stories, depth, level + 1);
}
}
for n in st.notes.iter_mut().chain(st.endnotes.iter_mut()) {
complete &= renumber_anchored(dst, src, &mut Arc::make_mut(n).text, stories, depth);
complete &= renumber_anchored_in(dst, src, &mut Arc::make_mut(n).text, stories, depth, level + 1);
}
complete
}
@@ -5744,6 +5764,48 @@ mod copy_ids_tests {
assert_eq!(s.doc().unwrap().doc, before, "nothing is pasted");
}
#[test]
fn objects_anchored_in_cells_nested_too_deeply_are_not_copied() {
use std::sync::Arc;
// The first cell of the story's table holds `n` tables, each in the first cell of the one before, and the cell
// of the innermost one holds an anchored object.
fn nested(n: usize) -> crate::Session {
let mut s = crate::Session::new();
let (f, sid) = marked_frame(&mut s);
let leaf = s.execute("frame.create", &json!({"rect": [0, 0, 10, 10]})).unwrap()["id"].as_u64().unwrap();
let d = Arc::make_mut(&mut s.doc_mut().unwrap().doc);
let item = d.item(designcraft_doc::ItemId(leaf)).cloned().unwrap();
Arc::make_mut(&mut d.spreads[0]).items.retain(|i| i.id.0 != leaf);
let mut chain: Option<designcraft_doc::Table> = None;
for _ in 0..n {
let mut t = designcraft_doc::Table::new(d.alloc(), 1, 1, 0, 0, 40.0);
match chain.take() {
Some(inner) => {
t.cells[0].text.insert_table(0, inner);
}
None => {
t.cells[0].text.insert_object(0, designcraft_doc::AnchoredObject::new(item.clone(), Default::default()));
}
}
chain = Some(t);
}
let st = d.story_mut(sid).unwrap();
let text = &mut Arc::make_mut(st.tables.values_mut().next().unwrap()).cells[0].text;
text.insert_table(text.len(), chain.unwrap());
s.execute("selection.set", &json!({"ids": [f]})).unwrap();
s
}
let mut s = nested(super::MAX_COPY_DEPTH - 1);
s.execute("edit.duplicate", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc.anchored_items().len(), 2, "the anchored object is copied");
let mut s = nested(super::MAX_COPY_DEPTH);
let before = s.doc().unwrap().doc.clone();
let e = s.execute("edit.duplicate", &json!({})).unwrap_err().to_string();
assert!(e.contains("too deeply to copy"), "{e}");
assert_eq!(s.doc().unwrap().doc, before, "nothing is copied");
}
#[test]
fn pasted_cross_references_follow_the_pasted_anchors_of_other_stories() {
let mut s = crate::Session::new();