From 71d33cd02feaac042d15dcb6d0f1bd29d257026c Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 10 Oct 2026 08:35:15 +0000 Subject: [PATCH] IDML: text inside a paragraph-level cross-reference source keeps its formatting --- crates/idml/src/import.rs | 27 +++++++++++++++++---------- crates/idml/src/tests.rs | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/crates/idml/src/import.rs b/crates/idml/src/import.rs index fb2378446..c18085cd4 100644 --- a/crates/idml/src/import.rs +++ b/crates/idml/src/import.rs @@ -1658,6 +1658,15 @@ impl<'r> Importer<'r> { t } + /// The format of the text in a `CharacterStyleRange`: its style, and its local attributes over + /// the enclosing paragraph range's. + fn range_format(&mut self, c: &El, pchars: &CharAttrs) -> CharFormat { + let style = c.get("AppliedCharacterStyle").map(|r| self.char_style_ref(r)).unwrap_or_else(|| st::NO_CHAR_STYLE.into()); + let mut over = pchars.clone(); + over.merge(&self.char_attrs(c)); + CharFormat { style, over } + } + fn walk_story(&mut self, e: &El, b: &mut StoryBuilder, pf: &ParaFormat, pchars: &CharAttrs, cf: &CharFormat, brk: Option<&str>) { for n in &e.children { match n { @@ -1672,13 +1681,13 @@ impl<'r> Importer<'r> { { *last = npf.clone(); } - self.walk_story(c, b, &npf, &chars, cf, None); + // Text and markers outside any character range (a destination or a + // cross-reference source at paragraph level) take the paragraph's format. + let pcf = CharFormat { style: st::NO_CHAR_STYLE.into(), over: chars.clone() }; + self.walk_story(c, b, &npf, &chars, &pcf, None); } "CharacterStyleRange" => { - let style = c.get("AppliedCharacterStyle").map(|r| self.char_style_ref(r)).unwrap_or_else(|| st::NO_CHAR_STYLE.into()); - let mut over = pchars.clone(); - over.merge(&self.char_attrs(c)); - let ncf = CharFormat { style, over }; + let ncf = self.range_format(c, pchars); let bt = c.get("ParagraphBreakType").filter(|t| *t != "Anywhere"); b.last = ncf.clone(); self.walk_story(c, b, pf, pchars, &ncf, bt); @@ -1742,11 +1751,9 @@ impl<'r> Importer<'r> { "CrossReferenceSource" => { // The generated text is regenerated from the format. b.xrefs.push((c.get("Self").unwrap_or("").to_string(), c.get("AppliedFormat").unwrap_or("").to_string())); - let inner = c.find("CharacterStyleRange"); - let f = match inner.and_then(|r| r.get("AppliedCharacterStyle")) { - Some(r) => CharFormat { style: self.char_style_ref(r), over: pchars.clone() }, - None => cf.clone(), - }; + // A source at paragraph level wraps a range; inside a range it holds the + // text directly and takes that range's format. + let f = c.find("CharacterStyleRange").map_or_else(|| cf.clone(), |r| self.range_format(r, pchars)); b.push(&designcraft_doc::XREF_MARK.to_string(), &f); } "Footnote" => { diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 163fd0b26..fad724a09 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -652,6 +652,44 @@ fn round_trips_cross_references() { assert_eq!(back.xref_formats.len(), d.xref_formats.len()); } +/// A cross-reference source sits either inside a character range (holding the text directly) or at +/// paragraph level around a range. Both give the generated text the paragraph's and the range's +/// attributes, so both set the same text at the same width. +#[test] +fn cross_reference_source_nesting_keeps_formatting() { + let story = |body: &str| { + format!( + r#" + +{body}"# + ) + }; + const CSR: &str = r#""#; + let inside = story(&format!( + r#"{CSR}See section 11., then a few more words to wrap."# + )); + let around = story(&format!( + r#"{CSR}See section {CSR}11.{CSR}, then a few more words to wrap."# + )); + let compose = |xml: &str| { + let d = import_idml(&fixture_with_story(xml)).unwrap(); + d.check().unwrap(); + let s = d.stories.values().find(|s| !s.xrefs.is_empty()).expect("story with a cross-reference").clone(); + let cs = designcraft_compose::compose_story(&d, s.id, &Default::default()); + let glyphs: Vec<_> = cs.frames.iter().flat_map(|f| &f.lines).flat_map(|l| l.glyphs.iter().map(move |g| (g.byte, g.x, l.baseline))).collect(); + (s, glyphs) + }; + let (a, ga) = compose(&inside); + let (b, gb) = compose(&around); + assert!(ga.len() > a.text.chars().count() / 2, "the story composes"); + assert_eq!(a.text, b.text); + assert_eq!(a.chars, b.chars); + assert_eq!(a.chars.len(), 1, "the generated text shares its neighbours' format"); + assert_eq!(a.chars[0].format.over.tracking, Some(200.0)); + assert_eq!(a.chars[0].format.over.size, Some(11.0)); + assert_eq!(ga, gb); +} + #[test] fn round_trips_index_references() { use designcraft_doc::index::IndexRange;