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 crate::form::{FormNodeId, FormNodeType, FormTree};
13
14use formcalc_interpreter::interpreter::Interpreter;
15use formcalc_interpreter::lexer::tokenize;
16use formcalc_interpreter::parser;
17use formcalc_interpreter::value::Value;
18
19/// Errors from script execution.
20#[derive(Debug, thiserror::Error)]
21pub enum ScriptError {
22    #[error("FormCalc error in node '{node}': {message}")]
23    Execution { node: String, message: String },
24    #[error("Validation failed for node '{node}': {message}")]
25    ValidationFailed { node: String, message: String },
26}
27
28/// Result of running all scripts on a form tree.
29#[derive(Debug, Default)]
30pub struct ScriptResult {
31    /// Fields whose values were updated by calculate scripts.
32    pub updated_fields: Vec<FormNodeId>,
33    /// Validation failures (node id and error message).
34    pub validation_errors: Vec<(FormNodeId, String)>,
35}
36
37/// Execute all calculate scripts in the form tree, updating field values.
38///
39/// Walks the tree depth-first. For each Field node with a `calculate` script,
40/// evaluates the script and sets the field's value to the result.
41/// Returns a summary of which fields were updated.
42pub fn run_calculations(form: &mut FormTree) -> Result<ScriptResult, ScriptError> {
43    let mut result = ScriptResult::default();
44    let mut interpreter = Interpreter::new();
45
46    // Collect all nodes with calculate scripts first (to avoid borrow issues)
47    let calc_nodes: Vec<(FormNodeId, String, String)> = form
48        .nodes
49        .iter()
50        .enumerate()
51        .filter_map(|(i, node)| {
52            node.calculate
53                .as_ref()
54                .map(|script| (FormNodeId(i), node.name.clone(), script.clone()))
55        })
56        .collect();
57
58    for (id, _name, script) in calc_nodes {
59        // Gracefully skip scripts that fail (e.g. unrecognized JavaScript syntax,
60        // unsupported FormCalc constructs). Matches Adobe's best-effort behavior.
61        let value = match eval_script(&mut interpreter, &script) {
62            Ok(v) => v,
63            Err(_) => continue,
64        };
65
66        // Convert the FormCalc result to a string and set the field value
67        let value_str = value_to_string(&value);
68
69        let node = form.get_mut(id);
70        if let FormNodeType::Field { ref mut value } = node.node_type {
71            if *value != value_str {
72                *value = value_str;
73                result.updated_fields.push(id);
74            }
75        }
76    }
77
78    Ok(result)
79}
80
81/// Execute all validate scripts in the form tree, collecting failures.
82///
83/// For each Field node with a `validate` script, evaluates the script.
84/// A validation passes if the result is truthy (non-zero number, non-empty string).
85pub fn run_validations(form: &FormTree) -> Result<ScriptResult, ScriptError> {
86    let mut result = ScriptResult::default();
87    let mut interpreter = Interpreter::new();
88
89    for (i, node) in form.nodes.iter().enumerate() {
90        if let Some(ref script) = node.validate {
91            let val =
92                eval_script(&mut interpreter, script).map_err(|e| ScriptError::Execution {
93                    node: node.name.clone(),
94                    message: e,
95                })?;
96
97            if !is_truthy(&val) {
98                let msg = format!(
99                    "Validation script returned falsy value: {}",
100                    value_to_string(&val)
101                );
102                result.validation_errors.push((FormNodeId(i), msg));
103            }
104        }
105    }
106
107    Ok(result)
108}
109
110/// Run calculate scripts, then layout. Convenience wrapper for the common flow.
111///
112/// Returns the script result so callers can inspect which fields changed
113/// and whether validations passed.
114pub fn prepare_form(form: &mut FormTree) -> Result<ScriptResult, ScriptError> {
115    let mut calc_result = run_calculations(form)?;
116    let val_result = run_validations(form)?;
117    calc_result.validation_errors = val_result.validation_errors;
118    Ok(calc_result)
119}
120
121/// Evaluate a FormCalc script string and return the result value.
122fn eval_script(interpreter: &mut Interpreter, script: &str) -> Result<Value, String> {
123    let tokens = tokenize(script).map_err(|e| format!("Tokenize error: {e}"))?;
124    let ast = parser::parse(tokens).map_err(|e| format!("Parse error: {e}"))?;
125    interpreter
126        .exec(&ast)
127        .map_err(|e| format!("Runtime error: {e}"))
128}
129
130/// Convert a FormCalc Value to a display string.
131fn value_to_string(val: &Value) -> String {
132    match val {
133        Value::Number(n) => {
134            // Format integers without decimal point
135            if *n == n.floor() && n.is_finite() {
136                format!("{}", *n as i64)
137            } else {
138                format!("{n}")
139            }
140        }
141        Value::String(s) => s.clone(),
142        Value::Null => String::new(),
143    }
144}
145
146/// Check if a FormCalc value is truthy (for validation results).
147fn is_truthy(val: &Value) -> bool {
148    match val {
149        Value::Number(n) => *n != 0.0,
150        Value::String(s) => !s.is_empty(),
151        Value::Null => false,
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::form::{FormNode, Occur};
159    use crate::text::FontMetrics;
160    use crate::types::{BoxModel, LayoutStrategy};
161
162    fn make_field_with_calc(
163        tree: &mut FormTree,
164        name: &str,
165        initial_value: &str,
166        calculate: Option<&str>,
167    ) -> FormNodeId {
168        tree.add_node(FormNode {
169            name: name.to_string(),
170            node_type: FormNodeType::Field {
171                value: initial_value.to_string(),
172            },
173            box_model: BoxModel {
174                width: Some(100.0),
175                height: Some(20.0),
176                max_width: f64::MAX,
177                max_height: f64::MAX,
178                ..Default::default()
179            },
180            layout: LayoutStrategy::Positioned,
181            children: vec![],
182            occur: Occur::once(),
183            font: FontMetrics::default(),
184            calculate: calculate.map(|s| s.to_string()),
185            validate: None,
186            column_widths: vec![],
187            col_span: 1,
188        })
189    }
190
191    #[test]
192    fn calculate_script_updates_field_value() {
193        let mut tree = FormTree::new();
194        make_field_with_calc(&mut tree, "Total", "0", Some("10 + 20"));
195
196        let result = run_calculations(&mut tree).unwrap();
197
198        assert_eq!(result.updated_fields.len(), 1);
199        if let FormNodeType::Field { value } = &tree.get(result.updated_fields[0]).node_type {
200            assert_eq!(value, "30");
201        } else {
202            panic!("Expected Field node");
203        }
204    }
205
206    #[test]
207    fn calculate_script_string_result() {
208        let mut tree = FormTree::new();
209        make_field_with_calc(
210            &mut tree,
211            "Greeting",
212            "",
213            Some("Concat(\"Hello\", \" \", \"World\")"),
214        );
215
216        let result = run_calculations(&mut tree).unwrap();
217
218        assert_eq!(result.updated_fields.len(), 1);
219        if let FormNodeType::Field { value } = &tree.get(result.updated_fields[0]).node_type {
220            assert_eq!(value, "Hello World");
221        }
222    }
223
224    #[test]
225    fn no_update_when_value_unchanged() {
226        let mut tree = FormTree::new();
227        make_field_with_calc(&mut tree, "Same", "42", Some("42"));
228
229        let result = run_calculations(&mut tree).unwrap();
230
231        assert_eq!(result.updated_fields.len(), 0); // Value didn't change
232    }
233
234    #[test]
235    fn fields_without_scripts_are_untouched() {
236        let mut tree = FormTree::new();
237        make_field_with_calc(&mut tree, "Static", "original", None);
238
239        let result = run_calculations(&mut tree).unwrap();
240
241        assert_eq!(result.updated_fields.len(), 0);
242        if let FormNodeType::Field { value } = &tree.get(FormNodeId(0)).node_type {
243            assert_eq!(value, "original");
244        }
245    }
246
247    #[test]
248    fn validation_passes_for_truthy() {
249        let mut tree = FormTree::new();
250        let id = tree.add_node(FormNode {
251            name: "Amount".to_string(),
252            node_type: FormNodeType::Field {
253                value: "100".to_string(),
254            },
255            box_model: BoxModel {
256                width: Some(100.0),
257                height: Some(20.0),
258                max_width: f64::MAX,
259                max_height: f64::MAX,
260                ..Default::default()
261            },
262            layout: LayoutStrategy::Positioned,
263            children: vec![],
264            occur: Occur::once(),
265            font: FontMetrics::default(),
266            calculate: None,
267            validate: Some("1".to_string()), // truthy
268            column_widths: vec![],
269            col_span: 1,
270        });
271        let _ = id;
272
273        let result = run_validations(&tree).unwrap();
274        assert!(result.validation_errors.is_empty());
275    }
276
277    #[test]
278    fn validation_fails_for_falsy() {
279        let mut tree = FormTree::new();
280        tree.add_node(FormNode {
281            name: "Required".to_string(),
282            node_type: FormNodeType::Field {
283                value: "".to_string(),
284            },
285            box_model: BoxModel {
286                width: Some(100.0),
287                height: Some(20.0),
288                max_width: f64::MAX,
289                max_height: f64::MAX,
290                ..Default::default()
291            },
292            layout: LayoutStrategy::Positioned,
293            children: vec![],
294            occur: Occur::once(),
295            font: FontMetrics::default(),
296            calculate: None,
297            validate: Some("0".to_string()), // falsy
298            column_widths: vec![],
299            col_span: 1,
300        });
301
302        let result = run_validations(&tree).unwrap();
303        assert_eq!(result.validation_errors.len(), 1);
304    }
305
306    #[test]
307    fn prepare_form_runs_both() {
308        let mut tree = FormTree::new();
309        // Field with calculate script
310        make_field_with_calc(&mut tree, "Sum", "0", Some("5 * 3"));
311        // Field with validation
312        tree.add_node(FormNode {
313            name: "Check".to_string(),
314            node_type: FormNodeType::Field {
315                value: "ok".to_string(),
316            },
317            box_model: BoxModel {
318                width: Some(100.0),
319                height: Some(20.0),
320                max_width: f64::MAX,
321                max_height: f64::MAX,
322                ..Default::default()
323            },
324            layout: LayoutStrategy::Positioned,
325            children: vec![],
326            occur: Occur::once(),
327            font: FontMetrics::default(),
328            calculate: None,
329            validate: Some("0".to_string()), // will fail
330            column_widths: vec![],
331            col_span: 1,
332        });
333
334        let result = prepare_form(&mut tree).unwrap();
335
336        // Calculate ran
337        assert_eq!(result.updated_fields.len(), 1);
338        if let FormNodeType::Field { value } = &tree.get(FormNodeId(0)).node_type {
339            assert_eq!(value, "15");
340        }
341        // Validation ran
342        assert_eq!(result.validation_errors.len(), 1);
343    }
344
345    #[test]
346    fn complex_calculation() {
347        let mut tree = FormTree::new();
348        make_field_with_calc(&mut tree, "Tax", "0", Some("Round(100 * 0.21, 2)"));
349
350        let result = run_calculations(&mut tree).unwrap();
351        assert_eq!(result.updated_fields.len(), 1);
352        if let FormNodeType::Field { value } = &tree.get(result.updated_fields[0]).node_type {
353            assert_eq!(value, "21");
354        }
355    }
356}