Skip to main content

xfa_layout_engine/
scripting.rs

1//! Scripting integration โ€” run FormCalc calculate/validate scripts on form fields.
2//!
3//! Implements XFA Spec 3.3 ยง14.3.2 event model for calculate and validate events.
4//! Before layout, the engine executes calculate scripts on fields to compute
5//! derived values, then optionally runs validate scripts to check constraints.
6//!
7//! NOTE: This module handles simple calculate/validate scripts with a flat
8//! interpreter.  The more advanced dynamic scripting (initialize events,
9//! SOM-based field resolution, presence toggling) lives in
10//! `pdf-xfa/src/dynamic.rs` which uses the full FormTree SOM resolver.
11
12use std::sync::{atomic::AtomicBool, Arc};
13
14use crate::form::{FormNodeId, FormNodeType, FormTree};
15use xfa_js_sandboxed::{ExecCtx, FieldValues, XfaJsRuntime};
16
17use formcalc_interpreter::interpreter::Interpreter;
18use formcalc_interpreter::lexer::tokenize;
19use formcalc_interpreter::parser;
20use formcalc_interpreter::value::Value;
21
22/// Errors from script execution.
23#[derive(Debug, thiserror::Error)]
24pub enum ScriptError {
25    #[error("FormCalc error in node '{node}': {message}")]
26    /// Script execution error.
27    Execution {
28        /// Node name.
29        node: String,
30        /// Error message.
31        message: String,
32    },
33    #[error("Validation failed for node '{node}': {message}")]
34    /// Validation failed error.
35    ValidationFailed {
36        /// Node name.
37        node: String,
38        /// Error message.
39        message: String,
40    },
41}
42
43/// Result of running all scripts on a form tree.
44#[derive(Debug, Default)]
45pub struct ScriptResult {
46    /// Fields whose values were updated by calculate scripts.
47    pub updated_fields: Vec<FormNodeId>,
48    /// Validation failures (node id and error message).
49    pub validation_errors: Vec<(FormNodeId, String)>,
50}
51
52/// Execute all calculate scripts in the form tree, updating field values.
53///
54/// Walks the tree depth-first. For each Field node with a `calculate` script,
55/// evaluates the script and sets the field's value to the result.
56/// Returns a summary of which fields were updated.
57pub fn run_calculations(form: &mut FormTree) -> Result<ScriptResult, ScriptError> {
58    let mut result = ScriptResult::default();
59    let mut interpreter = Interpreter::new();
60
61    // Collect all nodes with calculate scripts first (to avoid borrow issues)
62    let calc_nodes: Vec<(FormNodeId, String, String)> = form
63        .nodes
64        .iter()
65        .enumerate()
66        .filter_map(|(i, node)| {
67            node.calculate
68                .as_ref()
69                .map(|script| (FormNodeId(i), node.name.clone(), script.clone()))
70        })
71        .collect();
72
73    for (id, _name, script) in calc_nodes {
74        // Gracefully skip scripts that fail (e.g. unrecognized JavaScript syntax,
75        // unsupported FormCalc constructs). Matches Adobe's best-effort behavior.
76        let value = match eval_script(&mut interpreter, &script) {
77            Ok(v) => v,
78            Err(_) => continue,
79        };
80
81        // Convert the FormCalc result to a string and set the field value
82        let value_str = value_to_string(&value);
83
84        let node = form.get_mut(id);
85        if let FormNodeType::Field { ref mut value } = node.node_type {
86            if *value != value_str {
87                *value = value_str;
88                result.updated_fields.push(id);
89            }
90        }
91    }
92
93    Ok(result)
94}
95
96/// Execute all validate scripts in the form tree, collecting failures.
97///
98/// For each Field node with a `validate` script, evaluates the script.
99/// A validation passes if the result is truthy (non-zero number, non-empty string).
100pub fn run_validations(form: &FormTree) -> Result<ScriptResult, ScriptError> {
101    let mut result = ScriptResult::default();
102    let mut interpreter = Interpreter::new();
103
104    for (i, node) in form.nodes.iter().enumerate() {
105        if let Some(ref script) = node.validate {
106            let val =
107                eval_script(&mut interpreter, script).map_err(|e| ScriptError::Execution {
108                    node: node.name.clone(),
109                    message: e,
110                })?;
111
112            if !is_truthy(&val) {
113                let msg = format!(
114                    "Validation script returned falsy value: {}",
115                    value_to_string(&val)
116                );
117                result.validation_errors.push((FormNodeId(i), msg));
118            }
119        }
120    }
121
122    Ok(result)
123}
124
125/// Run calculate scripts, then layout. Convenience wrapper for the common flow.
126///
127/// Returns the script result so callers can inspect which fields changed
128/// and whether validations passed.
129pub fn prepare_form(form: &mut FormTree) -> Result<ScriptResult, ScriptError> {
130    let mut calc_result = run_calculations(form)?;
131    let val_result = run_validations(form)?;
132    calc_result.validation_errors = val_result.validation_errors;
133    Ok(calc_result)
134}
135
136/// Evaluate a FormCalc script string and return the result value.
137fn eval_script(interpreter: &mut Interpreter, script: &str) -> Result<Value, String> {
138    let tokens = tokenize(script).map_err(|e| format!("Tokenize error: {e}"))?;
139    let ast = parser::parse(tokens).map_err(|e| format!("Parse error: {e}"))?;
140    interpreter
141        .exec(&ast)
142        .map_err(|e| format!("Runtime error: {e}"))
143}
144
145/// Convert a FormCalc Value to a display string.
146fn value_to_string(val: &Value) -> String {
147    match val {
148        Value::Number(n) => {
149            // Format integers without decimal point
150            if *n == n.floor() && n.is_finite() {
151                format!("{}", *n as i64)
152            } else {
153                format!("{n}")
154            }
155        }
156        Value::String(s) => s.clone(),
157        Value::Null => String::new(),
158    }
159}
160
161/// Check if a FormCalc value is truthy (for validation results).
162fn is_truthy(val: &Value) -> bool {
163    match val {
164        Value::Number(n) => *n != 0.0,
165        Value::String(s) => !s.is_empty(),
166        Value::Null => false,
167    }
168}
169
170/// Execute JavaScript calculate scripts on specific form fields.
171///
172/// This is the `application/x-javascript` dispatch path. Callers provide an
173/// explicit list of `(target_field_id, js_script_body)` pairs โ€” the language
174/// detection happens at a higher layer (the template parser / XFA merger)
175/// which knows the `contentType` of each `<script>` element.
176///
177/// For each pair the function:
178/// 1. Snapshots all field raw-values into a [`FieldValues`] store.
179/// 2. Executes the JS script body via the sandboxed [`XfaJsRuntime`].
180/// 3. Flushes any `rawValue` mutations back into the `FormTree`.
181///
182/// Errors from individual scripts are swallowed (best-effort, matching Adobe
183/// behaviour). Use the returned [`ScriptResult`] to inspect which fields were
184/// updated.
185pub fn run_js_calculations(
186    form: &mut FormTree,
187    scripts: &[(FormNodeId, &str)],
188    runtime: &mut XfaJsRuntime,
189    cancel: Arc<AtomicBool>,
190) -> Result<ScriptResult, ScriptError> {
191    let mut result = ScriptResult::default();
192
193    for (target_id, script) in scripts {
194        // Snapshot all field values.
195        let mut field_values = FieldValues::new();
196        for node in &form.nodes {
197            if let FormNodeType::Field { value } = &node.node_type {
198                if !node.name.is_empty() {
199                    field_values.set(&node.name, value.as_str());
200                }
201            }
202        }
203
204        let ctx = ExecCtx {
205            fields: &mut field_values,
206            cancel: Arc::clone(&cancel),
207            event_new_text: None,
208        };
209
210        if let Ok(js_val) = runtime.execute_calculate(script, ctx) {
211            // If the script returned a non-null/undefined value AND the
212            // target field exists, use it as the new rawValue (same
213            // semantics as FormCalc calculate).
214            let raw = js_val.to_raw_string();
215            let mut wrote_via_return = false;
216            if !raw.is_empty() {
217                let node = &mut form.nodes[target_id.0];
218                if let FormNodeType::Field { value } = &mut node.node_type {
219                    if *value != raw {
220                        *value = raw;
221                        result.updated_fields.push(*target_id);
222                        wrote_via_return = true;
223                    }
224                }
225            }
226
227            // Also apply explicit xfa.form.<field>.rawValue writes.
228            for (i, node) in form.nodes.iter_mut().enumerate() {
229                if let FormNodeType::Field { value } = &mut node.node_type {
230                    if let Some(new_val) = field_values.get(&node.name) {
231                        if new_val != value.as_str() {
232                            let id = FormNodeId(i);
233                            // Don't double-count if we already wrote this via return.
234                            if !(wrote_via_return && id == *target_id) {
235                                *value = new_val.to_string();
236                                if !result.updated_fields.contains(&id) {
237                                    result.updated_fields.push(id);
238                                }
239                            }
240                        }
241                    }
242                }
243            }
244        }
245        // Errors are swallowed; the script is skipped (best-effort).
246    }
247
248    Ok(result)
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::form::{FormNode, Occur};
255    use crate::text::FontMetrics;
256    use crate::types::{BoxModel, LayoutStrategy};
257
258    fn make_field_with_calc(
259        tree: &mut FormTree,
260        name: &str,
261        initial_value: &str,
262        calculate: Option<&str>,
263    ) -> FormNodeId {
264        tree.add_node(FormNode {
265            name: name.to_string(),
266            node_type: FormNodeType::Field {
267                value: initial_value.to_string(),
268            },
269            box_model: BoxModel {
270                width: Some(100.0),
271                height: Some(20.0),
272                max_width: f64::MAX,
273                max_height: f64::MAX,
274                ..Default::default()
275            },
276            layout: LayoutStrategy::Positioned,
277            children: vec![],
278            occur: Occur::once(),
279            font: FontMetrics::default(),
280            calculate: calculate.map(|s| s.to_string()),
281            validate: None,
282            column_widths: vec![],
283            col_span: 1,
284        })
285    }
286
287    #[test]
288    fn calculate_script_updates_field_value() {
289        let mut tree = FormTree::new();
290        make_field_with_calc(&mut tree, "Total", "0", Some("10 + 20"));
291
292        let result = run_calculations(&mut tree).unwrap();
293
294        assert_eq!(result.updated_fields.len(), 1);
295        if let FormNodeType::Field { value } = &tree.get(result.updated_fields[0]).node_type {
296            assert_eq!(value, "30");
297        } else {
298            panic!("Expected Field node");
299        }
300    }
301
302    #[test]
303    fn calculate_script_string_result() {
304        let mut tree = FormTree::new();
305        make_field_with_calc(
306            &mut tree,
307            "Greeting",
308            "",
309            Some("Concat(\"Hello\", \" \", \"World\")"),
310        );
311
312        let result = run_calculations(&mut tree).unwrap();
313
314        assert_eq!(result.updated_fields.len(), 1);
315        if let FormNodeType::Field { value } = &tree.get(result.updated_fields[0]).node_type {
316            assert_eq!(value, "Hello World");
317        }
318    }
319
320    #[test]
321    fn no_update_when_value_unchanged() {
322        let mut tree = FormTree::new();
323        make_field_with_calc(&mut tree, "Same", "42", Some("42"));
324
325        let result = run_calculations(&mut tree).unwrap();
326
327        assert_eq!(result.updated_fields.len(), 0); // Value didn't change
328    }
329
330    #[test]
331    fn fields_without_scripts_are_untouched() {
332        let mut tree = FormTree::new();
333        make_field_with_calc(&mut tree, "Static", "original", None);
334
335        let result = run_calculations(&mut tree).unwrap();
336
337        assert_eq!(result.updated_fields.len(), 0);
338        if let FormNodeType::Field { value } = &tree.get(FormNodeId(0)).node_type {
339            assert_eq!(value, "original");
340        }
341    }
342
343    #[test]
344    fn validation_passes_for_truthy() {
345        let mut tree = FormTree::new();
346        let id = tree.add_node(FormNode {
347            name: "Amount".to_string(),
348            node_type: FormNodeType::Field {
349                value: "100".to_string(),
350            },
351            box_model: BoxModel {
352                width: Some(100.0),
353                height: Some(20.0),
354                max_width: f64::MAX,
355                max_height: f64::MAX,
356                ..Default::default()
357            },
358            layout: LayoutStrategy::Positioned,
359            children: vec![],
360            occur: Occur::once(),
361            font: FontMetrics::default(),
362            calculate: None,
363            validate: Some("1".to_string()), // truthy
364            column_widths: vec![],
365            col_span: 1,
366        });
367        let _ = id;
368
369        let result = run_validations(&tree).unwrap();
370        assert!(result.validation_errors.is_empty());
371    }
372
373    #[test]
374    fn validation_fails_for_falsy() {
375        let mut tree = FormTree::new();
376        tree.add_node(FormNode {
377            name: "Required".to_string(),
378            node_type: FormNodeType::Field {
379                value: "".to_string(),
380            },
381            box_model: BoxModel {
382                width: Some(100.0),
383                height: Some(20.0),
384                max_width: f64::MAX,
385                max_height: f64::MAX,
386                ..Default::default()
387            },
388            layout: LayoutStrategy::Positioned,
389            children: vec![],
390            occur: Occur::once(),
391            font: FontMetrics::default(),
392            calculate: None,
393            validate: Some("0".to_string()), // falsy
394            column_widths: vec![],
395            col_span: 1,
396        });
397
398        let result = run_validations(&tree).unwrap();
399        assert_eq!(result.validation_errors.len(), 1);
400    }
401
402    #[test]
403    fn prepare_form_runs_both() {
404        let mut tree = FormTree::new();
405        // Field with calculate script
406        make_field_with_calc(&mut tree, "Sum", "0", Some("5 * 3"));
407        // Field with validation
408        tree.add_node(FormNode {
409            name: "Check".to_string(),
410            node_type: FormNodeType::Field {
411                value: "ok".to_string(),
412            },
413            box_model: BoxModel {
414                width: Some(100.0),
415                height: Some(20.0),
416                max_width: f64::MAX,
417                max_height: f64::MAX,
418                ..Default::default()
419            },
420            layout: LayoutStrategy::Positioned,
421            children: vec![],
422            occur: Occur::once(),
423            font: FontMetrics::default(),
424            calculate: None,
425            validate: Some("0".to_string()), // will fail
426            column_widths: vec![],
427            col_span: 1,
428        });
429
430        let result = prepare_form(&mut tree).unwrap();
431
432        // Calculate ran
433        assert_eq!(result.updated_fields.len(), 1);
434        if let FormNodeType::Field { value } = &tree.get(FormNodeId(0)).node_type {
435            assert_eq!(value, "15");
436        }
437        // Validation ran
438        assert_eq!(result.validation_errors.len(), 1);
439    }
440
441    #[test]
442    fn complex_calculation() {
443        let mut tree = FormTree::new();
444        make_field_with_calc(&mut tree, "Tax", "0", Some("Round(100 * 0.21, 2)"));
445
446        let result = run_calculations(&mut tree).unwrap();
447        assert_eq!(result.updated_fields.len(), 1);
448        if let FormNodeType::Field { value } = &tree.get(result.updated_fields[0]).node_type {
449            assert_eq!(value, "21");
450        }
451    }
452}