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