From 74930f3929be1ff3ced9f764ffaf874bb84eaf07 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:33:57 +0800 Subject: [PATCH] M12: what scripts set on XFA page-master content is applied to that page when laid out again apply_overrides walks the body only. Page-master content is now set page by page: each page made from a page area gets its own copy of the area's content with the overrides its SOM path (Body[n]...) carries, so a field a script hides on page 2 is hidden there only after a relayout. The copies are kept for the whole layout, as the measurement cache keys on node addresses. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv --- crates/xfa/src/layout.rs | 18 +++++ crates/xfa/src/layout/pages.rs | 18 ++++- crates/xfa/src/lib.rs | 5 +- crates/xfa/src/script.rs | 131 +++++++++++++++++---------------- crates/xfa/src/tests.rs | 38 ++++++++++ 5 files changed, 141 insertions(+), 69 deletions(-) diff --git a/crates/xfa/src/layout.rs b/crates/xfa/src/layout.rs index 73c6bde90f..cce62f90cd 100644 --- a/crates/xfa/src/layout.rs +++ b/crates/xfa/src/layout.rs @@ -155,6 +155,11 @@ struct Layouter<'t> { /// The flowed subforms being laid out, outermost first: a break between their children /// closes their parts on one content area and opens new ones on the next. frames: Vec, + /// What scripts set on page-master content, applied to each page's copy by its own SOM path. + overrides: Option<&'t crate::script::Overrides>, + /// The pages' copies of master content that overrides changed, kept for the whole layout so + /// their addresses stay unique (the measurement cache keys on them). + master_copies: Vec>>, } /// How many instances of a repeating subform to lay out: what the data holds once it holds @@ -167,6 +172,17 @@ pub fn instance_count(occur: &Occur, from_data: usize) -> usize { /// Lay a template out, with the values and repeat counts of `data` (the `xfa:data` element). pub fn layout(tpl: &Template, data: Option<&DataNode>) -> Result { + layout_inner(tpl, data, None) +} + +/// [`layout`], with what scripts set (`ov`) applied to page-master content page by page: a +/// field hidden on the second page made from a page area is hidden there only. The rest of the +/// template takes `ov` before this ([`crate::script::apply_overrides`]). +pub fn layout_with_overrides(tpl: &Template, data: Option<&DataNode>, ov: &crate::script::Overrides) -> Result { + layout_inner(tpl, data, Some(ov)) +} + +fn layout_inner(tpl: &Template, data: Option<&DataNode>, overrides: Option<&crate::script::Overrides>) -> Result { let areas = match &tpl.root.page_set { Some(ps) if !ps.areas.is_empty() => ps.areas.clone(), _ => vec![PageArea { @@ -201,6 +217,8 @@ pub fn layout(tpl: &Template, data: Option<&DataNode>) -> Result measures: Cell::new(0), leaves: Vec::new(), frames: Vec::new(), + overrides, + master_copies: Vec::new(), }; let ctx = Ctx { font: Font::default(), para: Para::default(), som: String::new() }; l.new_page()?; diff --git a/crates/xfa/src/layout/pages.rs b/crates/xfa/src/layout/pages.rs index 6b0ba7820e..5bc7a13fbe 100644 --- a/crates/xfa/src/layout/pages.rs +++ b/crates/xfa/src/layout/pages.rs @@ -2,7 +2,7 @@ use super::EMBED_COPIED; use super::items::{Item, Page, UNDERLINE_DROP}; use super::{ - Block, Border, Ctx, Cursor, FlowMark, Font, Frame, Layouter, MAX_ITEMS, MAX_MEASURES, MAX_PAGES, PAGE_COUNT_MARK, PageRole, Para, Rc, Rect, + Block, Border, Ctx, Cursor, FlowMark, Font, Frame, Layouter, MAX_ITEMS, MAX_MEASURES, MAX_PAGES, Node, PAGE_COUNT_MARK, PageRole, Para, Rc, Rect, SomIndices, XfaError, text, }; @@ -107,8 +107,20 @@ impl Layouter<'_> { let ctx = Ctx { font: Font::default(), para: Para::default(), som: area.name.clone().map(|n| format!("{n}[{instance}]")).unwrap_or_default() }; let page_rect = Rect::new(0.0, 0.0, area.width, area.height); - let sibs = SomIndices::of(&area.items); - for (i, item) in area.items.iter().enumerate() { + // What scripts set on this page's master content (a button hidden on this page only) goes + // on a copy of it for this page; other pages keep the template's. + let prefix = format!("{}.", ctx.som); + let copy = + self.overrides.filter(|ov| !ctx.som.is_empty() && ov.presence.keys().chain(ov.access.keys()).any(|k| k.starts_with(&prefix))).map(|ov| { + let mut copy = area.items.clone(); + crate::script::apply_overrides_to(&mut copy, &ctx.som, ov, 1); + let copy = Rc::new(copy); + self.master_copies.push(Rc::clone(©)); + copy + }); + let items: &[Node] = copy.as_deref().map_or(&area.items, Vec::as_slice); + let sibs = SomIndices::of(items); + for (i, item) in items.iter().enumerate() { self.draw_positioned_child(item, page_rect, &ctx, 1, sibs.at(i))?; } Ok(()) diff --git a/crates/xfa/src/lib.rs b/crates/xfa/src/lib.rs index 202fa37975..3afe4f11f8 100644 --- a/crates/xfa/src/lib.rs +++ b/crates/xfa/src/lib.rs @@ -158,8 +158,9 @@ pub fn render_into(doc: &mut Document) -> Result { } let (tpl, parse_warnings) = parse(&packets.xdp)?; let data = parse_datasets(&packets.xdp); - let laid = apply_overrides(&tpl, &overrides(doc)); - let mut form = layout(&laid, data.as_ref())?; + let ov = overrides(doc); + let laid = apply_overrides(&tpl, &ov); + let mut form = layout::layout_with_overrides(&laid, data.as_ref(), &ov)?; form.warnings.splice(0..0, parse_warnings); if form.pages.is_empty() { return Err(XfaError::Malformed("the template laid out to no pages".into())); diff --git a/crates/xfa/src/script.rs b/crates/xfa/src/script.rs index 56be18bb9a..d0452533dc 100644 --- a/crates/xfa/src/script.rs +++ b/crates/xfa/src/script.rs @@ -475,81 +475,84 @@ pub fn has_scripts(tpl: &Template) -> bool { any(&tpl.root.scripts) || walk(&tpl.root.children, 1) || tpl.root.page_set.as_ref().is_some_and(|ps| ps.areas.iter().any(|a| walk(&a.items, 1))) } -/// The template with `ov` applied to its objects (instance 0 of each; scripts that hide one -/// instance of a repeating subform hide them all). -pub fn apply_overrides(tpl: &Template, ov: &Overrides) -> Template { - /// Set the presence `ov` gives each of `nodes` (children of `parent_som`) and their descendants, - /// down to [`MAX_DEPTH`], and the access it gives a field outside an exclusion group. An unnamed - /// draw, area or subform shares its parent's SOM path, so it takes no override of its own. - fn walk(nodes: &mut [Node], parent_som: &str, ov: &Overrides, depth: usize) { - if depth > MAX_DEPTH { - return; - } - let sibs = SomIndices::of(nodes); - for (i, n) in nodes.iter_mut().enumerate() { - let sib = sibs.at(i); - let name_default = match n { - Node::Field(_) => Some("field"), - Node::ExclGroup(_) => Some("group"), - _ => None, - }; - let name = n.common().name.as_deref().or(name_default); - let som = child_som(parent_som, name, sib); - // An unnamed draw, area or subform has no SOM name: `som` is its parent's, and so are - // the overrides under it. - let presence = name.and_then(|_| ov.presence.get(&som)).map(|p| Presence::parse(Some(p))); - let access = name.and_then(|_| ov.access.get(&som)).cloned(); - match n { - Node::Subform(s) => { - if let Some(p) = presence { - s.common.presence = p; - } - walk(&mut s.children, &som, ov, depth + 1); +/// Set the presence `ov` gives each of `nodes` (children of `parent_som`) and their descendants, +/// down to [`MAX_DEPTH`], and the access it gives a field outside an exclusion group. An unnamed +/// draw, area or subform shares its parent's SOM path, so it takes no override of its own. +pub(crate) fn apply_overrides_to(nodes: &mut [Node], parent_som: &str, ov: &Overrides, depth: usize) { + if depth > MAX_DEPTH { + return; + } + let sibs = SomIndices::of(nodes); + for (i, n) in nodes.iter_mut().enumerate() { + let sib = sibs.at(i); + let name_default = match n { + Node::Field(_) => Some("field"), + Node::ExclGroup(_) => Some("group"), + _ => None, + }; + let name = n.common().name.as_deref().or(name_default); + let som = child_som(parent_som, name, sib); + // An unnamed draw, area or subform has no SOM name: `som` is its parent's, and so are + // the overrides under it. + let presence = name.and_then(|_| ov.presence.get(&som)).map(|p| Presence::parse(Some(p))); + let access = name.and_then(|_| ov.access.get(&som)).cloned(); + match n { + Node::Subform(s) => { + if let Some(p) = presence { + s.common.presence = p; } - Node::Area(a) => { - if let Some(p) = presence { - a.common.presence = p; - } - walk(&mut a.children, &som, ov, depth + 1); + apply_overrides_to(&mut s.children, &som, ov, depth + 1); + } + Node::Area(a) => { + if let Some(p) = presence { + a.common.presence = p; } - Node::Field(f) => { - if let Some(p) = presence { - f.common.presence = p; - } - if let Some(a) = access { - f.access = match a.as_str() { - "readOnly" => Access::ReadOnly, - "protected" => Access::Protected, - "nonInteractive" => Access::NonInteractive, - _ => Access::Open, - }; - } + apply_overrides_to(&mut a.children, &som, ov, depth + 1); + } + Node::Field(f) => { + if let Some(p) = presence { + f.common.presence = p; } - Node::Draw(d) => { - if let Some(p) = presence { - d.common.presence = p; - } + if let Some(a) = access { + f.access = match a.as_str() { + "readOnly" => Access::ReadOnly, + "protected" => Access::Protected, + "nonInteractive" => Access::NonInteractive, + _ => Access::Open, + }; } - Node::ExclGroup(g) => { - if let Some(p) = presence { - g.common.presence = p; - } - let fields: Vec = g.fields.iter().map(|f| Node::Field(Box::new(f.clone()))).collect(); - let fsibs = SomIndices::of(&fields); - for (fi, f) in g.fields.iter_mut().enumerate() { - let fsom = child_som(&som, f.common.name.as_deref().or(Some("field")), fsibs.at(fi)); - if let Some(p) = ov.presence.get(&fsom) { - f.common.presence = Presence::parse(Some(p)); - } + } + Node::Draw(d) => { + if let Some(p) = presence { + d.common.presence = p; + } + } + Node::ExclGroup(g) => { + if let Some(p) = presence { + g.common.presence = p; + } + let fields: Vec = g.fields.iter().map(|f| Node::Field(Box::new(f.clone()))).collect(); + let fsibs = SomIndices::of(&fields); + for (fi, f) in g.fields.iter_mut().enumerate() { + let fsom = child_som(&som, f.common.name.as_deref().or(Some("field")), fsibs.at(fi)); + if let Some(p) = ov.presence.get(&fsom) { + f.common.presence = Presence::parse(Some(p)); } } } } } +} + +/// The template with `ov` applied to its objects (instance 0 of each; scripts that hide one +/// instance of a repeating subform hide them all). Page-master content is left as it is: a page +/// area makes many pages, each with its own SOM path, so `ov` is applied to it page by page as +/// the pages are laid out ([`crate::layout::layout_with_overrides`]). +pub fn apply_overrides(tpl: &Template, ov: &Overrides) -> Template { let mut t = tpl.clone(); if !ov.is_empty() { let mut root = vec![Node::Subform(Box::new(t.root.clone()))]; - walk(&mut root, "", ov, 0); + apply_overrides_to(&mut root, "", ov, 0); if let Some(Node::Subform(r)) = root.pop() { t.root = *r; } @@ -617,7 +620,7 @@ pub fn rerender(doc: &mut Document, tpl: &Template) -> Result let data = parse_datasets(&packets.xdp); let ov = overrides(doc); let laid = apply_overrides(tpl, &ov); - let form = crate::layout(&laid, data.as_ref())?; + let form = crate::layout::layout_with_overrides(&laid, data.as_ref(), &ov)?; if form.pages.is_empty() { return Err(XfaError::Malformed("the template laid out to no pages".into())); } diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 16528fae3a..3ed06a0a6d 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -4186,3 +4186,41 @@ fn clear_password_values_clears_static_xfa_password_fields() { let packets = read_packets(&doc).unwrap().unwrap(); assert!(!packets.xdp.contains("hunter2"), "{}", packets.xdp); } + +/// What a script sets on page-master content (a field hidden on the second page made from a +/// page area) is applied to that page's copy only, when the form is laid out again: the other +/// pages keep the field. +#[test] +fn a_script_hiding_master_content_on_one_page_hides_it_there_only() { + let section = |n: u32| { + format!( + r#""# + ) + }; + let xdp = format!( + r##" + + + +"##, + section(1), + section(2), + section(3) + ); + let (tpl, _) = crate::parse(&xdp).unwrap(); + let mut ov = Overrides::default(); + ov.presence.insert("Body[1].pageNo[0]".into(), "hidden".into()); + let laid = apply_overrides(&tpl, &ov); + let form = crate::layout::layout_with_overrides(&laid, None, &ov).unwrap(); + let has: Vec = form.pages.iter().map(|p| widgets(p).iter().any(|w| w.som.contains("pageNo"))).collect(); + assert_eq!(has, [true, false, true], "hidden on the second page only"); +}