1use 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#[derive(Debug, thiserror::Error)]
24pub enum ScriptError {
25 #[error("FormCalc error in node '{node}': {message}")]
26 Execution {
28 node: String,
30 message: String,
32 },
33 #[error("Validation failed for node '{node}': {message}")]
34 ValidationFailed {
36 node: String,
38 message: String,
40 },
41}
42
43#[derive(Debug, Default)]
45pub struct ScriptResult {
46 pub updated_fields: Vec<FormNodeId>,
48 pub validation_errors: Vec<(FormNodeId, String)>,
50}
51
52pub fn run_calculations(form: &mut FormTree) -> Result<ScriptResult, ScriptError> {
58 let mut result = ScriptResult::default();
59 let mut interpreter = Interpreter::new();
60
61 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 let value = match eval_script(&mut interpreter, &script) {
77 Ok(v) => v,
78 Err(_) => continue,
79 };
80
81 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
96pub 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
125pub 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
136fn 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
145fn value_to_string(val: &Value) -> String {
147 match val {
148 Value::Number(n) => {
149 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
161fn 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
170pub 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 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 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 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 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 }
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); }
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()), 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()), 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 make_field_with_calc(&mut tree, "Sum", "0", Some("5 * 3"));
407 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()), column_widths: vec![],
427 col_span: 1,
428 });
429
430 let result = prepare_form(&mut tree).unwrap();
431
432 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 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}