diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index 5156dbfa9..f17ec0ac5 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -890,12 +890,16 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C let pf = &story.paras[pi]; let (mut pp, base_chars) = doc.styles.resolve_para(pf); // A break character that ends the previous paragraph sends this one on, like a start option. - let after_break = pi - .checked_sub(1) - .and_then(|p| para_ranges.get(p)) - .and_then(|r| story.text.get(r.clone())) - .and_then(|t| t.chars().next_back()) - .and_then(break_start); + // Deleted paragraphs in between don't count, nor does a deleted or hidden break. + let after_break = (0..pi).rev().find(|p| !deleted.get(*p).copied().unwrap_or(false)).and_then(|p| { + let r = para_ranges.get(p)?; + let (i, c) = story.text.get(r.clone())?.char_indices().next_back()?; + let start = break_start(c)?; + let (_, chars) = doc.styles.resolve_para(story.paras.get(p)?); + let props = doc.styles.resolve_char(&chars, story.char_format_at(r.start + i + c.len_utf8())); + let gone = props.change == designcraft_doc::ChangeMark::Deleted || doc.conditions_hide(&props.conditions); + (!gone).then_some(start) + }); if let Some(t) = story.para_table(pi) { if cur.last_baseline.is_some() { cur.start(after_break.unwrap_or(pp.start_paragraph), &cols, frames, doc); diff --git a/crates/idml/src/import.rs b/crates/idml/src/import.rs index df7159967..852df3189 100644 --- a/crates/idml/src/import.rs +++ b/crates/idml/src/import.rs @@ -2040,7 +2040,9 @@ impl<'r> Importer<'r> { "ParagraphStyleRange" => { let style = c.get("AppliedParagraphStyle").map(|r| self.para_style_ref(r)).unwrap_or_else(|| pf.style.clone()); let para = self.para_attrs(c); - let chars = self.char_attrs(c); + let mut chars = self.char_attrs(c); + // A paragraph range inside a tracked change is part of that change. + chars.change = chars.change.or(cf.over.change); let npf = ParaFormat { style, para, chars: CharAttrs::default(), table: None }; if b.fresh && let Some(last) = b.paras.last_mut() diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 2f0092337..404def0f5 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -3290,3 +3290,86 @@ fn same_para_style_spacing_from_a_based_on_style_element_closes_the_gap() { assert!((baseline(1) - baseline(0) - 13.0).abs() < 1e-6, "{} {}", baseline(0), baseline(1)); assert!((baseline(3) - baseline(2) - 13.0 - 4.5 - 18.0).abs() < 1e-6, "{} {}", baseline(2), baseline(3)); } + +/// Composed lines (baseline, left, right) of the only story. +fn story_lines(d: &Document) -> Vec<(f64, f64, f64)> { + let sid = *d.stories.keys().next().unwrap(); + let cs = designcraft_compose::compose_story(d, sid, &Default::default()); + cs.frames.iter().flat_map(|f| f.lines.iter().map(|l| (l.baseline, l.x0, l.end_x))).collect() +} + +const PSR: &str = r#""#; + +/// Lays out like the same story without the deleted paragraph. +fn assert_deleted_para_takes_no_room(deleted: &str, deleted_text: &str) { + let tracked = tracked_story(&format!( + r#"{PSR}{CSR}First live paragraph.
+{deleted} +{PSR}{CSR}Second live paragraph."# + )); + let plain = tracked_story(&format!( + r#"{PSR}{CSR}First live paragraph.
+{PSR}{CSR}Second live paragraph."# + )); + let d = import_idml_with(&fixture_with_story(&tracked), &|_| None).unwrap(); + let story = d.stories.values().next().unwrap(); + let marks: Vec<_> = story.runs().map(|(r, f)| (story.text[r].to_string(), f.over.change)).collect(); + let want = vec![ + ("First live paragraph.\n".to_string(), None), + (deleted_text.to_string(), Some(designcraft_doc::ChangeMark::Deleted)), + ("Second live paragraph.".to_string(), None), + ]; + assert_eq!(marks, want); + let expected = story_lines(&import_idml_with(&fixture_with_story(&plain), &|_| None).unwrap()); + assert_eq!(expected.len(), 2); + assert_eq!(story_lines(&d), expected); +} + +#[test] +fn tracked_deleted_paragraph_range_stays_deleted() { + assert_deleted_para_takes_no_room( + &format!( + r#"{PSR}{CSR}Deleted text
"# + ), + "Deleted text\n", + ); +} + +#[test] +fn tracked_deleted_frame_break_does_not_break() { + let fb = "\u{E003}"; + let nf = r#""#; + // The change around the paragraph range. + assert_deleted_para_takes_no_room( + &format!( + r#"{PSR}{CSR}Gone{nf}
"# + ), + &format!("Gone{fb}\n"), + ); + // The change inside the character range. + assert_deleted_para_takes_no_room( + &format!( + r#"{PSR}{nf}Gone
"# + ), + &format!("Gone{fb}\n"), + ); +} + +#[test] +fn live_frame_break_before_deleted_paragraph_still_breaks() { + let nf = r#""#; + let first = + format!(r#"{PSR}{CSR}First live paragraph.{nf}
"#); + let second = format!(r#"{PSR}{CSR}Second live paragraph."#); + let deleted = format!( + r#"{PSR}{CSR}Gone
"# + ); + let import = |body: String| import_idml_with(&fixture_with_story(&tracked_story(&body)), &|_| None).unwrap(); + let reference = import(format!("{first}\n{second}")); + let sid = *reference.stories.keys().next().unwrap(); + let per_frame: Vec = + designcraft_compose::compose_story(&reference, sid, &Default::default()).frames.iter().map(|f| f.lines.len()).collect(); + assert!(per_frame.first() == Some(&1) && per_frame.get(1).is_some_and(|n| *n > 0), "the break sends the second paragraph on"); + let expected = story_lines(&reference); + assert_eq!(story_lines(&import(format!("{first}\n{deleted}\n{second}"))), expected); +}