Merge idml-deleted-text (update): deleted text inside nested paragraphs stays deleted; a deleted break doesn't break (#581)

Conflict: crates/idml/src/tests.rs: both sides' tests (#473's SameParaStyleSpacing tests, then the branch's deleted-text tests)
This commit is contained in:
aiongg
2026-10-12 17:25:31 +00:00
3 changed files with 96 additions and 7 deletions
+10 -6
View File
@@ -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);
+3 -1
View File
@@ -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()
+83
View File
@@ -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#"<ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/$ID/NormalParagraphStyle">"#;
/// 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}<Content>First live paragraph.</Content><Br/></CharacterStyleRange></ParagraphStyleRange>
{deleted}
{PSR}{CSR}<Content>Second live paragraph.</Content></CharacterStyleRange></ParagraphStyleRange>"#
));
let plain = tracked_story(&format!(
r#"{PSR}{CSR}<Content>First live paragraph.</Content><Br/></CharacterStyleRange></ParagraphStyleRange>
{PSR}{CSR}<Content>Second live paragraph.</Content></CharacterStyleRange></ParagraphStyleRange>"#
));
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#"<Change Self="c1" ChangeType="DeletedText">{PSR}{CSR}<Content>Deleted text</Content><Br/></CharacterStyleRange></ParagraphStyleRange></Change>"#
),
"Deleted text\n",
);
}
#[test]
fn tracked_deleted_frame_break_does_not_break() {
let fb = "\u{E003}";
let nf = r#"<CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]" ParagraphBreakType="NextFrame">"#;
// The change around the paragraph range.
assert_deleted_para_takes_no_room(
&format!(
r#"<Change Self="c1" ChangeType="DeletedText">{PSR}{CSR}<Content>Gone</Content></CharacterStyleRange>{nf}<Br/></CharacterStyleRange></ParagraphStyleRange></Change>"#
),
&format!("Gone{fb}\n"),
);
// The change inside the character range.
assert_deleted_para_takes_no_room(
&format!(
r#"{PSR}{nf}<Change Self="c1" ChangeType="DeletedText"><Content>Gone</Content><Br/></Change></CharacterStyleRange></ParagraphStyleRange>"#
),
&format!("Gone{fb}\n"),
);
}
#[test]
fn live_frame_break_before_deleted_paragraph_still_breaks() {
let nf = r#"<CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]" ParagraphBreakType="NextFrame">"#;
let first =
format!(r#"{PSR}{CSR}<Content>First live paragraph.</Content></CharacterStyleRange>{nf}<Br/></CharacterStyleRange></ParagraphStyleRange>"#);
let second = format!(r#"{PSR}{CSR}<Content>Second live paragraph.</Content></CharacterStyleRange></ParagraphStyleRange>"#);
let deleted = format!(
r#"<Change Self="c1" ChangeType="DeletedText">{PSR}{CSR}<Content>Gone</Content><Br/></CharacterStyleRange></ParagraphStyleRange></Change>"#
);
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<usize> =
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);
}