Merge #304: IDML: text in a paragraph-level cross-reference source keeps its formatting

This commit is contained in:
aiongg
2026-10-10 08:49:00 +00:00
2 changed files with 55 additions and 10 deletions
+17 -10
View File
@@ -1823,6 +1823,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 {
@@ -1837,13 +1846,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);
@@ -1907,11 +1916,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" => {
+38
View File
@@ -1039,6 +1039,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#"<?xml version="1.0" encoding="UTF-8"?>
<idPkg:Story xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging" DOMVersion="16.0">
<Story Self="s1"><ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/$ID/NormalParagraphStyle" PointSize="11">{body}</ParagraphStyleRange></Story></idPkg:Story>"#
)
};
const CSR: &str = r#"<CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]" Tracking="200">"#;
let inside = story(&format!(
r#"{CSR}<Content>See section </Content><CrossReferenceSource Self="x1" AppliedFormat="n"><Content>11.</Content></CrossReferenceSource><Content>, then a few more words to wrap.</Content></CharacterStyleRange>"#
));
let around = story(&format!(
r#"{CSR}<Content>See section </Content></CharacterStyleRange><CrossReferenceSource Self="x1" AppliedFormat="n">{CSR}<Content>11.</Content></CharacterStyleRange></CrossReferenceSource>{CSR}<Content>, then a few more words to wrap.</Content></CharacterStyleRange>"#
));
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;