From bfaebb76ee47d6abce29e1bfbf142a01cab090c3 Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 12 Oct 2026 17:18:26 +0000 Subject: [PATCH] Integration: an explicit SameParaStyleSpacing Ignore survives a native save (#473) same_para_style_spacing uses deserialize_explicit_value, so Some(None) round-trips through serde. Test: SameParaStyleSpacing as a Properties element on the based-on Normal style closes the gap between consecutive headings; SetIgnore keeps the usual spacing. --- crates/doc/src/attrs.rs | 11 +++++++++- crates/idml/src/tests.rs | 44 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/doc/src/attrs.rs b/crates/doc/src/attrs.rs index 02cd839c0..65a477f45 100644 --- a/crates/doc/src/attrs.rs +++ b/crates/doc/src/attrs.rs @@ -1123,7 +1123,7 @@ attr_set! { space_before: f64 = 0.0, space_after: f64 = 0.0, /// Space between consecutive paragraphs of the same style; replaces space after/before there (None = ignore). - same_para_style_spacing: Option = None, + same_para_style_spacing: Option = None => "deserialize_explicit_value", /// Drop cap: the paragraph's first `drop_cap_chars` characters (code points, never /// splitting a base from its combining marks) set `drop_cap_lines` lines tall, in the /// character style `drop_cap_style` ("" or `[None]`: the text's own formatting). @@ -1625,6 +1625,15 @@ mod tests { assert_eq!(p.word_space_max, 1.33); } + #[test] + fn same_para_style_spacing_ignore_survives_serde() { + for v in [None, Some(None), Some(Some(0.0))] { + let a = ParaAttrs { same_para_style_spacing: v, ..Default::default() }; + let back: ParaAttrs = serde_json::from_str(&serde_json::to_string(&a).unwrap()).unwrap(); + assert_eq!(back.same_para_style_spacing, v); + } + } + #[test] fn numbering_styles() { assert_eq!(NumberStyle::UpperRoman.format(14), "XIV"); diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index a7aaacd6b..2f0092337 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -3246,3 +3246,47 @@ fn same_para_style_spacing_imports_and_round_trips() { // The explicit Ignore survives the round trip too. assert_eq!(b.paras[1].para.same_para_style_spacing, Some(None)); } + +/// SameParaStyleSpacing set as a `Properties` element on the based-on style (`Normal`) reaches +/// the headings: consecutive headings sit one leading apart. SetIgnore keeps the usual spacing. +#[test] +fn same_para_style_spacing_from_a_based_on_style_element_closes_the_gap() { + let styles = r#" + + + + + + + Source Serif 413 + + + $ID/[No paragraph style]0 + + + ParagraphStyle/$ID/NormalParagraphStyle + + + ParagraphStyle/$ID/NormalParagraphStyleSetIgnore + + +"#; + let para = |style: &str, text: &str| { + format!( + r#"{text}
"# + ) + }; + let story = format!( + r#"{}{}{}{}"#, + para("Heading", "One"), + para("Heading", "Two"), + para("Loose", "Three"), + para("Loose", "Four") + ); + let d = import_idml(&fixture_with_table_styles(styles, &story)).unwrap(); + let sid = d.stories.iter().find(|(_, s)| s.text.contains("One")).map(|(id, _)| *id).unwrap(); + let cs = designcraft_compose::compose_story(&d, sid, &Default::default()); + let baseline = |para: usize| cs.frames[0].lines.iter().find(|l| l.para == para).unwrap().baseline; + 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)); +}