From e35d028d4a64be0b2d521433494da2133b62ac0d Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Fri, 30 Oct 2026 21:46:49 +0800 Subject: [PATCH] M12: an XFA reset reaches fields a script added while the form opened, before they have widgets resetData resolved its targets through the generated fields, so a field of a row an opening script added, with no widget until the form is laid out again, kept the value a later script gave it, and the final layout saved it. The reset now also walks the live form and puts the template's default back in the data of every matching field with no widget yet (review). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv --- crates/engine/src/tests.rs | 23 +++++++++++++++++++++++ crates/engine/src/xfa.rs | 27 +++++++++++++++++++++++++++ crates/xfa/src/lib.rs | 2 +- crates/xfa/src/script.rs | 11 +++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index ed53ad2a50..2dd652263d 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -9507,3 +9507,26 @@ fn repeated_backdrop_artwork_survives_background_undo_redo_and_both_save_modes() s.apply(rewritten, Edit::RemoveMarks { kind: MarkKind::Background }).unwrap(); black(&s, rewritten); } + +/// A reset in an opening script reaches a field of a row an earlier opening script added, before +/// the form is laid out again with the row's widgets: its data goes back to its default and stays +/// so when the form is saved. +#[test] +fn an_opening_reset_reaches_a_row_added_while_opening() { + let tpl = pdfcraft_xfa::fixtures::scripted_template().replace( + "if (qty.rawValue === null) qty.rawValue = 2;", + r#"if (qty.rawValue === null) qty.rawValue = 2; table._row.addInstance(1);"#, + ) + .replace( + r#""#, + r#""#, + ); + let mut s = Session::new().with_clock(|| 1_700_000_000); + let id = s.open("rows.pdf", None, Arc::new(pdfcraft_xfa::fixtures::shell(&tpl)), None).unwrap(); + let out = s.take_js_output(id); + assert!(out.errors.is_empty(), "{:?}", out.errors); + let cos = pdfcraft_cos::Document::open(s.save_bytes(id).unwrap()).unwrap(); + let data = pdfcraft_xfa::data_of(&cos).unwrap(); + assert_eq!(data.count(&pdfcraft_xfa::som_to_path("form[0].page1[0].table[0]"), "row"), 2, "the row was added"); + assert_eq!(data.text_at(&pdfcraft_xfa::som_to_path("form[0].page1[0].table[0].row[1].item[0]")).unwrap_or(""), "", "reset in the data"); +} diff --git a/crates/engine/src/xfa.rs b/crates/engine/src/xfa.rs index fd1249df2d..d0b674bc04 100644 --- a/crates/engine/src/xfa.rs +++ b/crates/engine/src/xfa.rs @@ -590,7 +590,34 @@ impl<'a, 'b> Runner<'a, 'b> { } else { Some(by_som.iter().filter(|(som, _)| names.iter().any(|n| som_matches(som, n))).map(|(_, f)| f.clone()).collect()) }; + // Fields of the live form with no widget yet (a row an opening script added, before the + // form is laid out again): their data goes back to the template's default too, or the + // value would stay and be laid out and saved. + let unshown: Vec = { + let live = self.live(); + let mut out = Vec::new(); + let mut stack: Vec<&pdfcraft_xfa::FormNode> = vec![&live.root]; + while let Some(n) = stack.pop() { + if n.kind == Some(pdfcraft_xfa::NodeKind::Field) + && !by_som.contains_key(&n.som) + && (names.is_empty() || names.iter().any(|name| som_matches(&n.som, name))) + { + out.push(n.som.clone()); + } + stack.extend(n.children.iter()); + } + out + }; + for som in unshown { + let Some(text) = pdfcraft_xfa::reset_default(self.tpl, &som) else { continue }; + let path = pdfcraft_xfa::data_path(self.tpl, &som); + if self.data.as_ref().and_then(|d| d.text_at(&path)).unwrap_or("") != text { + self.data_op(pdfcraft_xfa::DataOp::Value { path, text }); + self.changes.values += 1; + } + } if listed.as_ref().is_some_and(Vec::is_empty) { + self.invalidate(); return Ok(()); } pdfcraft_forms::reset(self.doc, listed.as_deref()).map_err(|e| e.to_string())?; diff --git a/crates/xfa/src/lib.rs b/crates/xfa/src/lib.rs index 7e63218c90..811e2f4de3 100644 --- a/crates/xfa/src/lib.rs +++ b/crates/xfa/src/lib.rs @@ -103,7 +103,7 @@ pub use pdf::{CLICK_KEY, LAYOUT_KEY, NODE_KEY, SOM_KEY, Stripped, XfaShape, exis pub use picture::typed_date; pub use script::{ FormNode, LiveForm, MAX_FORM_NODES, NodeKind, OVERRIDES_KEY, Overrides, ScriptEvent, apply_overrides, data_of, field_grows_with_text, - fields_by_som, form_tree, has_scripts, overrides, refresh_value, rerender, set_overrides, template_of, + fields_by_som, form_tree, has_scripts, overrides, refresh_value, rerender, reset_default, set_overrides, template_of, }; #[derive(Debug, thiserror::Error)] diff --git a/crates/xfa/src/script.rs b/crates/xfa/src/script.rs index 6946aa9b1c..f0906a603a 100644 --- a/crates/xfa/src/script.rs +++ b/crates/xfa/src/script.rs @@ -575,6 +575,17 @@ pub fn fields_by_som(doc: &Document) -> HashMap { out } +/// What a reset gives the field at `som` in the data: its template default, or nothing when it has +/// none. `None` for an object a reset restores no value of: not a field of the template, or a +/// button, signature or picture field. +pub fn reset_default(tpl: &Template, som: &str) -> Option { + let f = crate::data::template_field(tpl, som)?; + if matches!(f.ui, crate::model::Ui::Button | crate::model::Ui::Signature | crate::model::Ui::ImageEdit) { + return None; + } + Some(f.value.plain().unwrap_or_default()) +} + /// Lay the form out again from its template, data and overrides, replacing the pages and the /// generated fields (a script added a row or hid a subform). pub fn rerender(doc: &mut Document, tpl: &Template) -> Result {