diff --git a/crates/xfa/src/fixtures.rs b/crates/xfa/src/fixtures.rs index 4c8f29df5..5b497dd9d 100644 --- a/crates/xfa/src/fixtures.rs +++ b/crates/xfa/src/fixtures.rs @@ -96,6 +96,8 @@ pub fn static_shell(data: &str) -> Vec { assemble(objs) } +/// A PDF file holding `objs` as objects 1, 2, … in order, with a cross-reference table and a +/// trailer whose root is object 1. fn assemble(objs: Vec>) -> Vec { let mut out = b"%PDF-1.7\n".to_vec(); let mut offsets = Vec::new(); @@ -114,6 +116,30 @@ fn assemble(objs: Vec>) -> Vec { out } +/// A hidden subform `details` a script may show, holding a named field and three unnamed +/// objects: a caption, an old caption the form keeps hidden, and a hidden subform. Unnamed +/// objects have no SOM name of their own, so showing `details` must leave the hidden ones hidden. +pub fn unnamed_hidden_template() -> String { + r##" + + + +"## + .to_string() +} + /// A scripted form (JavaScript events): `qty` (initialised to 2, validated ≤ 100) × `price` /// (5) = `total` (calculated); a check box `more` that shows the hidden `details` subform; /// a table whose `row` (item, amount) repeats, with `addRow`, `removeRow` and `hello` buttons; diff --git a/crates/xfa/src/script.rs b/crates/xfa/src/script.rs index 64d394898..f709a2754 100644 --- a/crates/xfa/src/script.rs +++ b/crates/xfa/src/script.rs @@ -188,8 +188,9 @@ impl Walker<'_> { true } - /// Note the scripted events of the object at `som`: its event scripts, its calculation and - /// its validation (with the message a failing one shows). + /// Record the scripts of the object at `som` as events: each non-empty event script, then its + /// calculate and validate scripts (the validate one with `message`). Once [`MAX_EVENTS`] events + /// are recorded, nothing more is and the form is marked truncated. fn push_events(&mut self, som: &str, scripts: &[Script], calculate: Option<&Script>, validate: Option<&Script>, message: Option<&str>) { if self.events.len() >= MAX_EVENTS { self.truncated = true; @@ -231,18 +232,20 @@ impl Walker<'_> { } } - /// The presence of the object at `som`: a script's override, else the template's `p`. - fn presence(&self, som: &str, p: Presence) -> String { - self.ov.presence.get(som).cloned().unwrap_or_else(|| presence_name(p).to_string()) + /// The presence of the object named `name` at `som`: a script's, or else the template's. An + /// unnamed object shares its parent's SOM path, so a presence set there is not its own. + fn presence(&self, name: Option<&str>, som: &str, p: Presence) -> String { + name.and_then(|_| self.ov.presence.get(som)).cloned().unwrap_or_else(|| presence_name(p).to_string()) } - /// The access of the object at `som`: a script's override, else the template's `a`. + /// The access of the object at `som`: a script's, or else the template's `a`. fn access(&self, som: &str, a: Access) -> String { self.ov.access.get(som).cloned().unwrap_or_else(|| access_name(a).to_string()) } - /// The live node of field `f`, instance `index` under `parent_som`, valued from the data or - /// its template default (`radio`: the value its exclusion group chose), with its events noted. + /// The live node of field `f`, child `index` of `parent_som`, with its scripts recorded. In an + /// exclusion group (`radio` is the group's selected value) its value is its on value when picked, + /// else empty; elsewhere it is its data value (as saved) or its template default. fn field(&mut self, f: &Field, parent_som: &str, index: usize, radio: Option<&str>) -> FormNode { let som = child_som(parent_som, f.common.name.as_deref().or(Some("field")), index); let data_value = self.data.and_then(|d| d.value_at(&data_path(self.tpl, &som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty()); @@ -262,7 +265,7 @@ impl Walker<'_> { kind: Some(NodeKind::Field), value, numeric: f.numeric, - presence: self.presence(&som, f.common.presence), + presence: self.presence(Some("field"), &som, f.common.presence), access: self.access(&som, f.access), repeatable: false, occur_min: 1, @@ -283,8 +286,9 @@ impl Walker<'_> { } } - /// The live nodes of template `nodes` under `parent_som`: as many instances of a subform as - /// the data or its occurrence asks for, areas, fields, exclusion groups and draws. + /// The live nodes of `nodes`, the children of `parent_som`, with their scripts recorded: one per + /// instance of a repeating subform (what the data holds, else its initial count). Nothing past + /// [`MAX_DEPTH`] is built, and building stops once the object budget is spent. fn children(&mut self, nodes: &[Node], parent_som: &str, depth: usize) -> Vec { let mut out = Vec::new(); if depth > MAX_DEPTH { @@ -318,7 +322,7 @@ impl Walker<'_> { kind: Some(NodeKind::Subform), value: String::new(), numeric: false, - presence: self.presence(&som, sf.common.presence), + presence: self.presence(sf.common.name.as_deref(), &som, sf.common.presence), access: "open".into(), repeatable: sf.occur.max != Some(1), occur_min: sf.occur.min, @@ -336,7 +340,7 @@ impl Walker<'_> { name: a.common.name.clone().unwrap_or_else(|| "#area".into()), som: som.clone(), kind: Some(NodeKind::Area), - presence: self.presence(&som, a.common.presence), + presence: self.presence(a.common.name.as_deref(), &som, a.common.presence), access: "open".into(), occur_min: 1, occur_max: Some(1), @@ -353,7 +357,7 @@ impl Walker<'_> { som: som.clone(), kind: Some(NodeKind::Draw), value: d.value.plain().unwrap_or_default(), - presence: self.presence(&som, d.common.presence), + presence: self.presence(d.common.name.as_deref(), &som, d.common.presence), access: "open".into(), occur_min: 1, occur_max: Some(1), @@ -380,7 +384,7 @@ impl Walker<'_> { som: som.clone(), kind: Some(NodeKind::ExclGroup), value: selected.unwrap_or_default(), - presence: self.presence(&som, g.common.presence), + presence: self.presence(Some("group"), &som, g.common.presence), access: "open".into(), occur_min: 1, occur_max: Some(1), @@ -455,6 +459,9 @@ pub fn has_scripts(tpl: &Template) -> bool { /// 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; @@ -467,9 +474,12 @@ pub fn apply_overrides(tpl: &Template, ov: &Overrides) -> Template { Node::ExclGroup(_) => Some("group"), _ => None, }; - let som = child_som(parent_som, n.common().name.as_deref().or(name_default), sib); - let presence = ov.presence.get(&som).map(|p| Presence::parse(Some(p))); - let access = ov.access.get(&som).cloned(); + 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 { diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 1babcc62f..d66f6f7cf 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -7,7 +7,9 @@ use crate::data::{ DatasetsWrite, FieldData, FieldDatum, build_data, iso_to_pattern, parse_datasets, pattern_to_iso, read_values, som_to_path, write_datasets, }; use crate::data::{add_data_instances, remove_data_instance, write_data_value}; -use crate::fixtures::{TINY_GIF, fields_template, scripted_template, shell, static_shell, template, template_with_data, tiny_png}; +use crate::fixtures::{ + TINY_GIF, fields_template, scripted_template, shell, static_shell, template, template_with_data, tiny_png, unnamed_hidden_template, +}; use crate::layout::Item; use crate::layout::WidgetKind; use crate::model::*; @@ -575,6 +577,9 @@ fn xfa_bytes(doc: &Document) -> Vec { s.decoded_within(1 << 20).unwrap() } +/// Writing field values into the datasets keeps every data node no field binds to as written, +/// changes the bound values in place and adds a missing one beside them. Writing the same values +/// again changes nothing, and a field bound to structured content is reported, not flattened. #[test] fn writing_values_keeps_the_data_no_field_binds_to() { // Regression: the data element was rebuilt from the fields alone, dropping everything else. @@ -615,6 +620,9 @@ fn writing_values_keeps_the_data_no_field_binds_to() { assert!(String::from_utf8(xfa_bytes(&doc)).unwrap().contains("

rich

")); } +/// A UTF-16 packet in either byte order, with a byte order mark, has its values read exactly and +/// is written back as UTF-16 with its mark. A packet in neither UTF-8 nor UTF-16 is left alone, +/// and that is reported. #[test] fn utf16_packets_are_read_and_written_back_as_utf16() { let mut doc = Document::open(Arc::new(static_shell("Zoë 日本ünbound"))).unwrap(); @@ -644,6 +652,9 @@ fn utf16_packets_are_read_and_written_back_as_utf16() { assert_eq!(xfa_bytes(&doc), latin1); } +/// Hundreds of fields with absurd SOM indices finish within seconds, both writing the datasets and +/// building fresh data, create at most the node budget and report what they could not write. A +/// moderate index still works, creating the empty instances before it. #[test] fn huge_som_indices_are_capped_not_built() { // Regression: each index created its missing siblings one count at a time (quadratic), with @@ -683,6 +694,9 @@ fn huge_som_indices_are_capped_not_built() { assert!(d.get(&som_to_path("form1[0].row[2]")).is_some(), "the instances in between exist, empty"); } +/// The live form tree carries each object's SOM path, value, presence and access, one instance of +/// a repeating row without data, and the template's event, calculate and validate scripts (FormCalc +/// when no content type is given). Data adds rows and values, and an override changes presence. #[test] fn the_form_tree_carries_values_presence_instances_and_the_events() { let xml = scripted_template(); @@ -717,6 +731,9 @@ fn the_form_tree_carries_values_presence_instances_and_the_events() { assert_eq!(page.children.iter().find(|c| c.name == "details").unwrap().kind, Some(NodeKind::Subform)); } +/// Presence and access overrides are kept on the AcroForm and applied to the template: `note` has +/// no widget while `details` is hidden and gets one once an override shows it and the form is laid +/// out again. Saving and reopening keeps the overrides and one set of fields; storing none removes the key. #[test] fn overrides_live_in_the_acroform_and_hide_objects_in_the_layout() { let mut doc = Document::open(Arc::new(shell(&scripted_template()))).unwrap(); @@ -752,6 +769,32 @@ fn overrides_live_in_the_acroform_and_hide_objects_in_the_layout() { assert!(overrides(&doc).is_empty()); } +/// Showing a hidden subform through an override leaves its unnamed hidden draw and subform hidden, +/// in the layout and in the live form scripts see: they share the subform's SOM path, so its +/// override is not theirs. +#[test] +fn showing_a_subform_leaves_its_unnamed_hidden_objects_hidden() { + let (tpl, _) = parse(&unnamed_hidden_template()).unwrap(); + let mut ov = Overrides::default(); + ov.presence.insert("form[0].page1[0].details[0]".into(), "visible".into()); + let form = layout(&apply_overrides(&tpl, &ov), None).unwrap(); + let shown = texts(&form.pages[0]); + assert!(shown.iter().any(|t| t == "Current caption"), "{shown:?}"); + assert!(!shown.iter().any(|t| t == "Old caption" || t == "Old section"), "{shown:?}"); + assert_eq!(form.fields, 1); + // Scripts see the same: the hidden draw stays hidden in the live form. + let root = form_tree(&tpl, None, &ov).root; + let details = &root.children[0].children[0]; + assert_eq!(details.presence, "visible"); + let presence = |text: &str| details.children.iter().find(|c| c.value == text).map(|c| c.presence.clone()); + assert_eq!(presence("Current caption").as_deref(), Some("visible")); + assert_eq!(presence("Old caption").as_deref(), Some("hidden")); + assert_eq!(details.children.iter().find(|c| c.kind == Some(NodeKind::Subform)).map(|c| c.presence.as_str()), Some("hidden")); +} + +/// Adding and removing data instances edits the datasets packet, creating it when missing: asking +/// for no more rows than exist, or removing one that is not there, changes nothing, and removing a +/// row moves the later ones up. The layout follows the data, and an absurd count is capped, not built. #[test] fn data_instances_are_added_and_removed_in_the_packet() { let mut doc = Document::open(Arc::new(shell(&scripted_template()))).unwrap();