From c0a513eed8a940b7b2cd0e7d734e9b8916f34913 Mon Sep 17 00:00:00 2001 From: Eric Kaden <608341+EricKaden@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:32:16 -0400 Subject: [PATCH] M12: scripts read a date in its data picture as the ISO date A date the field's data picture writes as 20261018 reaches scripts as 2026-10-18, the rawValue another viewer gives them, so a script that checks String(rawValue).length == 10 works (DA Form 31's 10b TO). Doc comments on the functions this branch touches. --- crates/engine/src/tests.rs | 5 +++++ crates/xfa/src/data.rs | 4 ++++ crates/xfa/src/layout.rs | 4 ++++ crates/xfa/src/parse.rs | 2 ++ crates/xfa/src/script.rs | 19 +++++++++++++++---- crates/xfa/src/tests.rs | 24 ++++++++++++++++++++++++ parity/acrobat-features.toml | 4 ++-- 7 files changed, 56 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 9365e744c..27a6a3b4f 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -4155,6 +4155,8 @@ fn dynamic_xfa_forms_open_with_the_values_and_rows_of_their_data() { assert!(!doc.dirty); } +/// A static XFA form's fields take their values from the datasets on opening, and edits are +/// written back there. #[test] fn static_xfa_forms_take_their_values_from_the_datasets_and_write_them_back() { let original = Arc::new(pdfcraft_xfa::fixtures::static_shell("Ada1")); @@ -4206,6 +4208,8 @@ fn xfa_dates_are_written_and_read_back_in_their_data_picture() { assert_eq!(value("when"), vec!["10/18/2026".to_string()]); } +/// Same-named date fields are each written in their own data picture, and a data picture +/// without a day keeps the ISO date. #[test] fn each_xfa_date_field_is_written_in_its_own_data_picture_and_a_partial_one_keeps_iso() { let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::sibling_date_template(""))); @@ -4231,6 +4235,7 @@ fn each_xfa_date_field_is_written_in_its_own_data_picture_and_a_partial_one_keep } } +/// Data no field binds to (other namespaces, history) survives an edit and a save. #[test] fn xfa_data_no_field_binds_to_survives_an_edit_and_save() { // Regression: editing a field replaced the whole data element with the fields' values. diff --git a/crates/xfa/src/data.rs b/crates/xfa/src/data.rs index 678b1d975..79ca6e166 100644 --- a/crates/xfa/src/data.rs +++ b/crates/xfa/src/data.rs @@ -553,6 +553,8 @@ fn path_of(tpl: Option<&Template>, som: &str) -> DataPath { } } +/// The values the datasets should hold for `fields`, by data path: dates in each field's data +/// picture (or ISO), check and radio states, choice values as saved. fn wanted(doc: &Document, fields: &[FieldDatum], tpl: Option<&Template>) -> Vec { let mut out = Vec::new(); for f in fields { @@ -1636,6 +1638,8 @@ pub fn read_values_with(doc: &Document, tpl: Option<&Template>, fields: &[FieldD with_template(doc, tpl, |t| values_at(doc, t, fields)) } +/// The values the document's datasets hold for `fields`, read in each field's data picture and +/// shown in its own pattern; only fields the data mentions. fn values_at(doc: &Document, tpl: Option<&Template>, fields: &[FieldDatum]) -> Vec<(String, FieldData)> { let Ok(Some(p)) = crate::read_packets(doc) else { return Vec::new() }; let Some(data) = parse_datasets(&p.xdp) else { return Vec::new() }; diff --git a/crates/xfa/src/layout.rs b/crates/xfa/src/layout.rs index b7c2605a6..478936188 100644 --- a/crates/xfa/src/layout.rs +++ b/crates/xfa/src/layout.rs @@ -399,6 +399,8 @@ fn date_pattern(picture: Option<&str>) -> String { picture.and_then(date_letters).unwrap_or_else(|| "yyyy-mm-dd".into()) } +/// The PDF action a button's click script becomes: a native one for a single-statement idiom +/// (reset, print, save, a link), else the script itself. fn button_action(scripts: &[Script]) -> Option { let s = scripts.iter().find(|s| s.activity == "click")?; let t = &s.text; @@ -1450,6 +1452,8 @@ impl Layouter<'_> { if *n == 1 { base } else { format!("{base}_{n}") } } + /// Lay out field `f` in `rect`: its caption, its widget of the right kind, and its value + /// from the data. fn draw_field( &mut self, f: &Field, diff --git a/crates/xfa/src/parse.rs b/crates/xfa/src/parse.rs index ac02e5119..abe3f1a42 100644 --- a/crates/xfa/src/parse.rs +++ b/crates/xfa/src/parse.rs @@ -407,6 +407,8 @@ fn parse_items(n: XmlNode, warnings: &mut Vec) -> (Vec, Vec` element as the model's [`Field`]: its widget kind, caption, value, items, +/// pictures (edit and data), scripts and access. fn parse_field(n: XmlNode, warnings: &mut Vec) -> Field { let ui = child(n, "ui"); let ui_el = ui.and_then(|u| u.children().find(|c| c.is_element() && c.tag_name().name() != "extras" && c.tag_name().name() != "picture")); diff --git a/crates/xfa/src/script.rs b/crates/xfa/src/script.rs index 9d158ac6c..f74293a67 100644 --- a/crates/xfa/src/script.rs +++ b/crates/xfa/src/script.rs @@ -235,6 +235,8 @@ impl Walker<'_> { 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`: its value from the data + /// (as scripts read it) or its default, a radio button's from its group's choice `radio`. 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()); @@ -244,7 +246,7 @@ impl Walker<'_> { let on = f.items.first().cloned().unwrap_or_default(); if selected == on { on } else { String::new() } } - (None, Some(v)) => f.saved_value(&v), + (None, Some(v)) => f.saved_value(&Walker::as_scripts_read(f, &v)), (None, None) => f.value.plain().map(|v| f.saved_value(v.trim())).unwrap_or_default(), }; self.push_events(&som, &f.scripts, f.calculate.as_ref(), f.validate.as_ref(), f.validate_message.as_deref()); @@ -264,12 +266,19 @@ impl Walker<'_> { } } - /// What [`Self::field`] gives a field outside an exclusion group: its data value (as saved) - /// or its template default. + /// A value the data holds as scripts read it: a date the field's data picture writes in + /// another form (`20261018`) is the ISO date (`2026-10-18`) another viewer gives scripts as + /// `rawValue`. Other values, and dates that don't match the picture, are as they are. + fn as_scripts_read(f: &Field, v: &str) -> String { + crate::data::data_to_iso(v, crate::data::data_date_letters(f).as_deref()) + } + + /// What [`Self::field`] gives a field outside an exclusion group: its data value (as saved, + /// as scripts read it) or its template default. fn plain_field_value(tpl: &Template, data: Option<&DataNode>, f: &Field, som: &str) -> String { let data_value = data.and_then(|d| d.value_at(&data_path(tpl, som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty()); match data_value { - Some(v) => f.saved_value(&v), + Some(v) => f.saved_value(&Walker::as_scripts_read(f, &v)), None => f.value.plain().map(|v| f.saved_value(v.trim())).unwrap_or_default(), } } @@ -602,6 +611,8 @@ pub fn refresh_value(live: &mut FormNode, tpl: &Template, data: Option<&DataNode if path.is_empty() { return None; } + /// Down the one branch of `n` that `path` leads to from segment `pos`, giving each field + /// bound there its value from `data`; `None` when something there needs a rebuild. fn walk( n: &mut FormNode, tpl: &Template, diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 0a192479a..b6c861a53 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -325,6 +325,7 @@ fn datasets_parse_into_a_tree_and_som_paths_find_their_nodes() { assert!(parse_datasets("").is_none()); } +/// Dates convert between ISO and date patterns both ways, and what isn't a date is kept. #[test] fn dates_convert_between_iso_and_acrobat_patterns() { assert_eq!(iso_to_pattern("2001-02-03", "yyyy-mm-dd"), "2001-02-03"); @@ -350,6 +351,7 @@ fn a_date_is_read_from_the_data_in_its_data_picture() { assert_eq!(by("when").value.as_deref(), Some("10/18/2026"), "no data picture: the data holds ISO"); } +/// A SOM path names the template field its segment numbers count to, repeating rows included. #[test] fn som_paths_name_the_template_field_their_numbers_count_to() { let xml = r#"