diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 41d1c8dcf..f1260084f 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -4279,6 +4279,8 @@ fn xfa_choice_lists_signature_and_picture_fields_open_fill_script_and_save() { assert!(taken[0].contains("langs") && !taken[0].contains("country"), "{}", taken[0]); } +/// A form's data exports, imports and takes pictures through the session, and what it holds +/// comes back the same. #[test] fn xfa_data_exports_imports_and_pictures_round_trip_through_the_session() { use pdfcraft_xfa::som_to_path; @@ -4360,6 +4362,8 @@ fn xfa_data_exports_imports_and_pictures_round_trip_through_the_session() { assert_eq!(d2.page_images(0).len(), 2); } +/// Importing data into a static form replaces every value, emptying the fields the file +/// doesn't name, and pictures chosen for it reach its data. #[test] fn xfa_data_import_on_a_static_form_replaces_every_value_and_pictures_reach_its_data() { let original = Arc::new(pdfcraft_xfa::fixtures::static_shell("Ada1")); @@ -4380,6 +4384,8 @@ fn xfa_data_import_on_a_static_form_replaces_every_value_and_pictures_reach_its_ assert!(xml.contains("Bob") && !xml.contains("1"), "{xml}"); } +/// Importing data into a dynamic form adds and removes the rows of its repeating subform to +/// match, and the calculations over them run again. #[test] fn xfa_data_import_adds_and_removes_rows_of_a_dynamic_form() { let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::scripted_template())); @@ -4412,6 +4418,8 @@ fn xfa_data_import_adds_and_removes_rows_of_a_dynamic_form() { assert!(String::from_utf8_lossy(&out).contains("only")); } +/// Hundreds of calculations, in JavaScript and FormCalc, run on opening in one engine within +/// the opening's budget, and a change recalculates them within the event's. #[test] fn hundreds_of_xfa_calculations_run_in_one_engine_within_the_open_budget() { let (sections, rows) = (10, 20); @@ -4442,6 +4450,9 @@ fn hundreds_of_xfa_calculations_run_in_one_engine_within_the_open_budget() { eprintln!("recalculated after a change in {:?}", started.elapsed()); } +/// The scripts of one event share the form as the engine holds it: a failed script's +/// half-done writes reach nobody, and a script reads the saved value a choice list was given +/// by its shown text. #[test] fn xfa_scripts_of_one_event_see_the_form_as_the_engine_holds_it() { // In document order: `a` holds 1; `b`'s calculate writes `a` then fails; `c`'s calculate @@ -4483,6 +4494,9 @@ fn xfa_date_arithmetic_reads_the_date_as_its_display_picture_shows_it() { assert_eq!(value(&s, "to"), ["20261022"], "{:?}", out.errors); } +/// A scripted form end to end: initialize and calculate on open, recalculation after a change, +/// a failing validation's message, buttons that add and remove rows and show messages, a check +/// box that shows a subform (undoable), FormCalc, and a save and reopen that keep it all. #[test] fn xfa_scripts_initialize_calculate_validate_toggle_and_add_rows() { let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::scripted_template())); diff --git a/crates/engine/src/xfa.rs b/crates/engine/src/xfa.rs index 43e00a578..34c2de15e 100644 --- a/crates/engine/src/xfa.rs +++ b/crates/engine/src/xfa.rs @@ -129,6 +129,7 @@ pub(crate) struct XfaRun<'a> { pub ran_away: bool, } +/// The live form node `n`, with its descendants, as the script engine's snapshot of it. fn to_js(n: &FormNode) -> XfaNode { XfaNode { name: n.name.clone(), diff --git a/crates/js/src/formcalc.rs b/crates/js/src/formcalc.rs index 729cb1a88..74f8fef46 100644 --- a/crates/js/src/formcalc.rs +++ b/crates/js/src/formcalc.rs @@ -1097,6 +1097,8 @@ impl Interp<'_> { self.charge(1) } + /// Property `name` of form object `i` as FormCalc reads it: its value, attributes, + /// relatives and children by name. fn node_member(&mut self, i: usize, name: &str) -> R { let Some(n) = self.node(i) else { return Ok(Val::Null) }; Ok(match name { diff --git a/crates/js/src/tests.rs b/crates/js/src/tests.rs index b04f3c06c..e2c51c437 100644 --- a/crates/js/src/tests.rs +++ b/crates/js/src/tests.rs @@ -380,12 +380,15 @@ mod xfa_model { } } + /// Run the JavaScript `script` for `activity` on the test form, from the node at `target`. fn go(script: &str, target: &str, activity: &str) -> XfaOutcome { let ev = XfaEvent { activity: activity.into(), target: target.into(), ..Default::default() }; let doc = XfaDoc { file_name: "f.pdf".into(), page: 1, page_count: 3 }; run_xfa(script, &ev, &doc, &form(), Limits::default()) } + /// A calculate script finds its siblings by name and yields its last expression, and a + /// value it sets comes back as an effect. #[test] fn calculate_scripts_see_siblings_by_name_and_yield_their_last_expression() { let o = go("qty.rawValue * price.rawValue", "form1[0].page1[0].total[0]", "calculate"); @@ -427,6 +430,7 @@ mod xfa_model { assert_eq!(o.result.as_deref(), Some("2026-10-21")); } + /// Long values a script sets, and long results, are cut where they are stored, with a note. #[test] fn long_values_and_results_are_cut_where_they_are_stored() { // The value kept for later reads is the one the effect saves. @@ -654,6 +658,7 @@ mod formcalc { assert_eq!(o.result.as_deref(), Some("2026-10-21")); } + /// FormCalc arithmetic, string conversion and comparisons follow FormCalc's own rules. #[test] fn arithmetic_strings_and_comparisons_follow_formcalc_rules() { assert_eq!(calc("1 + 2 * 3"), "7"); diff --git a/crates/js/src/xfa.rs b/crates/js/src/xfa.rs index a1be639ca..5a693db9f 100644 --- a/crates/js/src/xfa.rs +++ b/crates/js/src/xfa.rs @@ -363,10 +363,13 @@ impl XHost { type Shared = RefCell; +/// The form host of the script running in `ctx`. fn host(ctx: &Context) -> JsResult<&Shared> { ctx.get_data::().ok_or_else(|| error("the XFA script host is not set up")) } +/// The snapshot `n` and its descendants flattened into `out` under `parent` (to [`MAX_DEPTH`] +/// and [`MAX_NODES`]): the index of `n`'s node, `None` past the limits. fn flatten(n: &XfaNode, parent: Option, out: &mut Vec, depth: usize) -> Option { if depth > MAX_DEPTH || out.len() >= MAX_NODES { return None; @@ -826,6 +829,8 @@ pub(crate) fn resolve_som(h: &XHost, from: usize, expr: &str) -> Vec { cur } +/// The live descendants of node `from` named `name` (`from..name` in a SOM path), in document +/// order, to [`MAX_DEPTH`]. pub(crate) fn descendants_named(h: &XHost, from: usize, name: &str, depth: usize) -> Vec { let mut out = Vec::new(); if depth > MAX_DEPTH { @@ -846,6 +851,8 @@ pub(crate) fn descendants_named(h: &XHost, from: usize, name: &str, depth: usize out } +/// A property read on a form node's Proxy: its value (and `formattedValue` as its picture shows +/// it), attributes, relatives, methods, instance managers and children by name. fn node_get(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { let idx = node_index(&arg(args, 0), ctx)?; let key = arg(args, 1); diff --git a/crates/xfa/src/layout.rs b/crates/xfa/src/layout.rs index 75ffae923..6a38a9743 100644 --- a/crates/xfa/src/layout.rs +++ b/crates/xfa/src/layout.rs @@ -419,6 +419,8 @@ pub fn formatted_value(picture: &str, raw: &str) -> Option { (shown != raw).then_some(shown) } +/// The native PDF action a button's click script amounts to (a reset, print, save or link), +/// else the script itself to run; `None` without a click script. fn button_action(scripts: &[Script]) -> Option { let s = scripts.iter().find(|s| s.activity == "click")?; let t = &s.text; diff --git a/crates/xfa/src/parse.rs b/crates/xfa/src/parse.rs index c753e5aed..92e9e5c28 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 template model: its widget, caption, value, items, pictures, +/// access and scripts. 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 f75235e20..64d394898 100644 --- a/crates/xfa/src/script.rs +++ b/crates/xfa/src/script.rs @@ -188,6 +188,8 @@ 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). 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; @@ -229,14 +231,18 @@ 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 access of the object at `som`: a script's override, 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. 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()); @@ -277,6 +283,8 @@ 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. fn children(&mut self, nodes: &[Node], parent_som: &str, depth: usize) -> Vec { let mut out = Vec::new(); if depth > MAX_DEPTH { diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 963056f10..22ddb5ed2 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -310,6 +310,7 @@ fn jpeg_headers_give_the_size() { const DATA: &str = "
Singh12001-02-03N2019StudiedDelhi2021WorkedToronto2023MovedOttawa
"; +/// The datasets packet parses into a tree whose nodes the fields' data paths find. #[test] fn datasets_parse_into_a_tree_and_som_paths_find_their_nodes() { let d = parse_datasets(&template_with_data(1, DATA)).expect("datasets"); @@ -323,6 +324,8 @@ fn datasets_parse_into_a_tree_and_som_paths_find_their_nodes() { assert!(parse_datasets("").is_none()); } +/// ISO dates convert to and from the date patterns fields show; text that isn't a date is left +/// alone or refused. #[test] fn dates_convert_between_iso_and_acrobat_patterns() { assert_eq!(iso_to_pattern("2001-02-03", "yyyy-mm-dd"), "2001-02-03"); @@ -362,6 +365,7 @@ fn display_pictures_are_parsed_apart_from_edit_pictures() { assert_eq!(days.display_picture, None); } +/// The datasets fill the fields they bind to and add the rows the data holds. #[test] fn data_fills_the_fields_and_adds_rows() { let form = layout_xml(&template_with_data(1, DATA)).unwrap();