From dd6d7feadb1024b519035bd9c099e1849e541f3d Mon Sep 17 00:00:00 2001 From: Eric Kaden <608341+EricKaden@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:18:11 -0400 Subject: [PATCH] M12: a date is tried in the field's first pattern again only when its format refused it --- crates/engine/src/lib.rs | 13 ++++++++----- crates/engine/src/tests.rs | 18 ++++++++++++++++++ crates/forms/src/lib.rs | 6 +++++- crates/forms/src/tests.rs | 2 +- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index bde8f0960..8ff666b86 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -2534,13 +2534,16 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -> Some(js) => pdfcraft_forms::set_value_with(doc, name, value, js), None => pdfcraft_forms::set_value(doc, name, value), }; - if let Err(refused) = set(doc, value) { - // A date the field's pattern doesn't read may be typed by another of an XFA date - // field's patterns: it is kept in the first. - match xfa::typed_date(doc, cx.xfa.as_deref(), name, value) { + match set(doc, value) { + Ok(()) => {} + // A date the field's format doesn't read may be typed by another of an XFA date + // field's patterns: it is kept in the first. Only a format refusal is tried + // again: it comes before any of the field's scripts run, so they run once. + Err(refused @ pdfcraft_forms::FormError::Format(_)) => match xfa::typed_date(doc, cx.xfa.as_deref(), name, value) { Some(typed) => set(doc, &typed)?, None => return Err(refused.into()), - } + }, + Err(refused) => return Err(refused.into()), } } Edit::ApplyScriptChanges { changes } => match cx.js.as_mut() { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index ed9e22c6b..11963bdd8 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -7305,3 +7305,21 @@ fn a_date_typed_by_another_of_an_xfa_fields_edit_patterns_is_kept_in_its_first() let data = pdfcraft_xfa::parse_datasets(&pdfcraft_xfa::read_packets(&cos).unwrap().unwrap().xdp).expect("a datasets packet"); assert_eq!(data.text_at(&pdfcraft_xfa::som_to_path("form[0].leave[0]")), Some("2026-11-08")); } + +/// A date the field's own scripts refuse is not tried again in another of an XFA date field's +/// patterns: only a date its format refuses is, which happens before any script runs, so the +/// scripts run once. This validate script counts its runs in `name` and accepts on its second. +#[test] +fn a_date_its_validate_script_refuses_is_not_tried_again() { + let mut s = Session::new(); + let id = s + .open("dates.pdf", None, Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::date_alternatives_template())), None) + .expect("opens"); + let script = Some("var n = Number(getField('name').value || 0) + 1; getField('name').value = String(n); event.rc = n >= 2;".to_string()); + s.apply(id, Edit::SetFieldScript { name: "leave".into(), event: "validate".into(), script }).unwrap(); + let value = |s: &Session, n: &str| s.get(id).unwrap().form.iter().find(|f| f.name == n).unwrap().value.clone(); + // Its format reads 18OCT26, and so does its DDMMMYY pattern. + let refused = s.apply(id, Edit::SetFieldValue { name: "leave".into(), value: FieldValue::Text("18OCT26".into()) }); + assert!(refused.is_err(), "the validate script refused it, once"); + assert!(value(&s, "name").is_empty(), "nothing the refused run changed is kept: {:?}", value(&s, "name")); +} diff --git a/crates/forms/src/lib.rs b/crates/forms/src/lib.rs index 7bd87a2ef..d4dc56748 100644 --- a/crates/forms/src/lib.rs +++ b/crates/forms/src/lib.rs @@ -48,6 +48,10 @@ pub enum FormError { ReadOnly(String), #[error("{0}")] Invalid(String), + /// The value doesn't fit the field's format (its keystroke check refused it), which is + /// checked before any of the field's scripts run. + #[error("{0}")] + Format(String), #[error("{0}")] Cos(#[from] pdfcraft_cos::CosError), } @@ -1044,7 +1048,7 @@ fn write_value(doc: &mut Document, f: &Field, value: &FieldValue, scripts: &mut return invalid(format!("{:?} takes at most {max} characters", f.name)); } // Keystroke (on commit) and Validate, as Acrobat runs them before accepting a value. - let t = &af::keystroke(&f.actions.format, &f.name, t).map_err(FormError::Invalid)?; + let t = &af::keystroke(&f.actions.format, &f.name, t).map_err(FormError::Format)?; af::validate(&f.actions.validate, t).map_err(FormError::Invalid)?; let t = &scripting::accept(doc, f, t, scripts)?; doc.update_dict(f.obj, |d| { diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index b2b8573eb..df8d8c35f 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -1270,7 +1270,7 @@ fn formats_validation_and_calculations_run_like_acrobat() { // Keystroke and validation errors use Acrobat's messages and change nothing. assert_eq!( set_value(&mut doc, "Qty", &FieldValue::Text("lots".into())), - Err(FormError::Invalid("The value entered does not match the format of the field [ Qty ]".into())) + Err(FormError::Format("The value entered does not match the format of the field [ Qty ]".into())) ); assert_eq!( set_value(&mut doc, "Qty", &FieldValue::Text("120".into())),