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 = "";
+/// 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();