From e35c430628ae1203da82177d8c785b1da4254fbf Mon Sep 17 00:00:00 2001 From: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:30:58 +1000 Subject: [PATCH] Preserve stored IDML page liquid rules Co-Authored-By: Codex --- crates/idml/src/export.rs | 1 + crates/idml/src/import.rs | 2 +- crates/idml/src/names.rs | 25 ++++++ crates/idml/src/tests.rs | 156 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 1 deletion(-) diff --git a/crates/idml/src/export.rs b/crates/idml/src/export.rs index 0b9435c71..d699268e4 100644 --- a/crates/idml/src/export.rs +++ b/crates/idml/src/export.rs @@ -1663,6 +1663,7 @@ impl<'a> Ex<'a> { .attr("Self", uid(pg.id.0)) .attr("Name", name) .attr("AppliedMaster", applied.map(|s| uid(s.0)).unwrap_or_else(|| "n".into())) + .attr("LayoutRule", names::liquid_rule_out(pg.liquid)) .attr("OverrideList", "") .attr("GeometricBounds", format!("0 0 {} {}", num(pg.height), num(pg.width))) .attr("ItemTransform", format!("1 0 0 1 {} {}", num(pg.x - ox), num(-oy))) diff --git a/crates/idml/src/import.rs b/crates/idml/src/import.rs index 51dc66d84..04e3bbec2 100644 --- a/crates/idml/src/import.rs +++ b/crates/idml/src/import.rs @@ -2015,7 +2015,7 @@ impl<'r> Importer<'r> { overridden: vec![], guides: Vec::::new(), show_parent_items: show, - liquid: Default::default(), + liquid: names::liquid_rule_in(p.get("LayoutRule").unwrap_or("Off")), transition: None, view_rotation: 0, show_parent_overlay: false, diff --git a/crates/idml/src/names.rs b/crates/idml/src/names.rs index ed0daaf40..d83a55884 100644 --- a/crates/idml/src/names.rs +++ b/crates/idml/src/names.rs @@ -74,6 +74,31 @@ pub fn fitting_in(s: &str) -> designcraft_doc::Fitting { } } +pub fn liquid_rule_out(rule: designcraft_doc::LiquidRule) -> &'static str { + use designcraft_doc::LiquidRule as R; + match rule { + R::Off => "Off", + R::Scale => "Scale", + R::ReCenter => "Recenter", + R::GuideBased => "GuideBased", + R::ObjectBased => "ObjectBased", + R::ControlledByParent => "UseMaster", + } +} + +pub fn liquid_rule_in(s: &str) -> designcraft_doc::LiquidRule { + use designcraft_doc::LiquidRule as R; + match s { + "Off" => R::Off, + "Scale" => R::Scale, + "Recenter" => R::ReCenter, + "GuideBased" => R::GuideBased, + "ObjectBased" => R::ObjectBased, + "UseMaster" => R::ControlledByParent, + _ => R::Off, + } +} + pub fn escape_id(s: &str) -> String { s.replace('%', "%25").replace(':', "%3a") } diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 57801e34f..a5da73992 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -2169,3 +2169,159 @@ fn frame_preference_property_color_resolves_for_frames_and_object_styles() { assert_eq!(colors, vec!["Near", "Near", "Far", designcraft_color::swatch::NONE]); assert_eq!(d.styles.object_style("Child").unwrap().text_frame.as_ref().unwrap().column_rule_color, "Near"); } + +mod liquid_page_rules { + use super::*; + use designcraft_doc::LiquidRule as R; + use std::sync::Arc; + + // Literal XML expectations, independent of names.rs and serde spellings. + const CASES: [(&str, R); 6] = [ + ("Scale", R::Scale), + ("Off", R::Off), + ("Recenter", R::ReCenter), + ("GuideBased", R::GuideBased), + ("ObjectBased", R::ObjectBased), + ("UseMaster", R::ControlledByParent), + ]; + + fn fixture(rule: Option<&str>, parent_rule: Option<&str>) -> Vec { + let attribute = |value: Option<&str>| value.map(|v| format!(r#" LayoutRule="{v}""#)).unwrap_or_default(); + let (rule, parent_rule) = (attribute(rule), attribute(parent_rule)); + let map = r#" + + + + +"#; + let parent = format!( + r#" + + + + +"# + ); + // Based-on belongs to the whole derived master: both pages apply the same master. + let derived = format!( + r#" + + + + +"# + ); + let spread = format!( + r#" + + + + + + +"# + ); + zip_files(&[ + ("designmap.xml", map), + ("Resources/Preferences.xml", PREFS), + ("MasterSpreads/A.xml", &parent), + ("MasterSpreads/B.xml", &derived), + ("Spreads/S.xml", &spread), + ]) + } + + #[test] + fn import_literal_xml() { + let baseline = import_idml(&fixture(None, None)).unwrap(); + assert_eq!((baseline.spreads.len(), baseline.parents.len()), (1, 2)); + assert!(baseline.spreads.iter().chain(&baseline.parents).all(|sp| sp.pages.len() == 2)); + assert!(baseline.spreads.iter().chain(&baseline.parents).flat_map(|sp| &sp.pages).all(|pg| pg.liquid == R::Off)); + let cases = CASES.into_iter().map(|(literal, rule)| (Some(literal), rule)).chain([ + (None, R::Off), + (Some("ReCenter"), R::Off), + (Some("controlledByParent"), R::Off), + (Some("PreserveExisting"), R::Off), + ]); + for (literal, rule) in cases { + let actual = import_idml(&fixture(literal, Some("Recenter"))).unwrap(); + assert_eq!(actual.spreads[0].pages[1].liquid, rule, "ordinary Page: {literal:?}"); + assert_eq!(actual.parents[1].pages[0].liquid, rule, "master Page: {literal:?}"); + // Same XML without rule attributes supplies the complete allowed-delta oracle. + let mut expected = baseline.clone(); + Arc::make_mut(&mut expected.spreads[0]).pages[1].liquid = rule; + Arc::make_mut(&mut expected.parents[1]).pages[0].liquid = rule; + Arc::make_mut(&mut expected.parents[0]).pages[1].liquid = R::ReCenter; + // Import assigns a fresh creation timestamp; all other document fields are compared. + expected.created = actual.created; + assert_eq!(actual, expected, "{literal:?}: only the three explicit rule fields may change apart from the import timestamp"); + assert_eq!(actual.spreads[0].pages[0].liquid, R::Off, "omitted does not infer inheritance"); + assert_eq!(actual.spreads[0].pages[0].parent, Some(actual.parents[0].id)); + assert_eq!(actual.parents[1].parent.as_ref().unwrap().based_on, Some(actual.parents[0].id)); + assert!(actual.parents.iter().flat_map(|sp| &sp.pages).all(|pg| pg.parent.is_none())); + if rule == R::ControlledByParent { + // The right document page resolves to A's right page, not B's controlled page. + assert_eq!(actual.parent_page_for(1), Some((0, 1))); + assert_eq!(actual.parents[0].pages[1].liquid, R::ReCenter); + } + } + } + + #[test] + fn export_and_round_trip() { + use std::io::Read; + + let baseline = import_idml(&fixture(None, None)).unwrap(); + for (literal, rule) in CASES { + // Set native enums directly; the literal importer under test does not build this input. + let mut source = baseline.clone(); + let pages = &mut Arc::make_mut(&mut source.spreads[0]).pages; + pages[0].parent = None; + pages[0].liquid = R::ControlledByParent; + pages[1].liquid = rule; + Arc::make_mut(&mut source.parents[0]).pages[1].liquid = R::ReCenter; + Arc::make_mut(&mut source.parents[1]).pages[0].liquid = rule; + source.check().unwrap(); + let bytes = export_idml(&source); + let mut archive = zip::ZipArchive::new(std::io::Cursor::new(&bytes)).unwrap(); + for (directory, tag, spread, rules) in [ + ("Spreads", "Spread", &source.spreads[0], ["UseMaster", literal]), + ("MasterSpreads", "MasterSpread", &source.parents[0], ["Off", "Recenter"]), + ("MasterSpreads", "MasterSpread", &source.parents[1], [literal, "Off"]), + ] { + let path = format!("{directory}/{tag}_u{:x}.xml", spread.id.0); + let mut part = Vec::new(); + archive.by_name(&path).unwrap().read_to_end(&mut part).unwrap(); + let root = xml::parse(&part).unwrap(); + let body = root.find(tag).unwrap(); + let pages: Vec<_> = body.find_all("Page").collect(); + assert_eq!(pages.len(), spread.pages.len(), "{path}"); + for (page, expected) in spread.pages.iter().zip(rules) { + let id = format!("u{:x}", page.id.0); + let matches: Vec<_> = pages.iter().filter(|p| p.get("Self") == Some(id.as_str())).collect(); + assert_eq!(matches.len(), 1, "{path}: {id}"); + assert_eq!(matches[0].get("LayoutRule"), Some(expected), "{path}: {id}"); + } + } + let actual = import_idml(&bytes).unwrap(); + assert_eq!((actual.spreads.len(), actual.parents.len()), (1, 2)); + assert_eq!(actual.spreads[0].pages[0].parent, None); + assert_eq!(actual.spreads[0].pages[1].parent, Some(actual.parents[0].id)); + assert_eq!(actual.parent_page_for(1), Some((0, 1))); + assert!(actual.parents.iter().flat_map(|sp| &sp.pages).all(|pg| pg.parent.is_none())); + for (index, prefix, name, based_on) in [(0, "A", "Base", None), (1, "B", "Derived", Some(actual.parents[0].id))] { + let info = actual.parents[index].parent.as_ref().unwrap(); + assert_eq!((&info.prefix[..], &info.name[..], info.based_on), (prefix, name, based_on)); + } + for (got, want) in actual.spreads.iter().chain(&actual.parents).zip(source.spreads.iter().chain(&source.parents)) { + assert_eq!(got.pages.len(), want.pages.len()); + for (got, want) in got.pages.iter().zip(&want.pages) { + // IDs are allocated afresh; parent associations were checked explicitly above. + let mut normalized = got.clone(); + normalized.id = want.id; + normalized.parent = want.parent; + assert_eq!(&normalized, want, "{literal}: all other Page fields, including the stored rule"); + } + } + } + } +}