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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
This commit is contained in:
mtlcl410andClaude Opus 5.5 committed 2026-10-31 13:57:42 +08:00
1 parent 71a42a20d7
commit e35d028d4a
4 files changed
+62 -1

No files matched your search

+23
View File
@@ -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#"<field name="grand" w="2in" h="0.3in" access="readOnly">"#,
r#"<field name="grand" w="2in" h="0.3in" access="readOnly"><event activity="initialize"><script contentType="application/x-javascript">xfa.resolveNode("form.page1.table.row[1].item").rawValue = "X"; xfa.host.resetData("form.page1.table.row[1].item");</script></event>"#,
);
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");
}
+27
View File
@@ -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<String> = {
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())?;
+1 -1
View File
@@ -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)]
+11
View File
@@ -575,6 +575,17 @@ pub fn fields_by_som(doc: &Document) -> HashMap<String, String> {
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<String> {
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<Report, XfaError> {