1use std::sync::{atomic::AtomicBool, Arc};
19
20use crate::form::{FormNodeId, FormNodeType, FormTree};
21use xfa_js_sandboxed::{ExecCtx, FieldValues, XfaJsRuntime};
22
23use formcalc_interpreter::interpreter::Interpreter;
24use formcalc_interpreter::lexer::tokenize;
25use formcalc_interpreter::parser;
26use formcalc_interpreter::value::Value;
27
28#[derive(Debug, thiserror::Error)]
30pub enum ScriptError {
31 #[error("FormCalc error in node '{node}': {message}")]
32 Execution {
34 node: String,
36 message: String,
38 },
39 #[error("Validation failed for node '{node}': {message}")]
40 ValidationFailed {
42 node: String,
44 message: String,
46 },
47}
48
49#[derive(Debug, Default)]
51pub struct ScriptResult {
52 pub updated_fields: Vec<FormNodeId>,
54 pub validation_errors: Vec<(FormNodeId, String)>,
56}
57
58pub fn run_calculations(form: &mut FormTree) -> Result<ScriptResult, ScriptError> {
64 let mut result = ScriptResult::default();
65 let mut interpreter = Interpreter::new();
66
67 let calc_nodes: Vec<(FormNodeId, String, String)> = form
69 .nodes
70 .iter()
71 .enumerate()
72 .filter_map(|(i, node)| {
73 node.calculate
74 .as_ref()
75 .map(|script| (FormNodeId(i), node.name.clone(), script.clone()))
76 })
77 .collect();
78
79 for (id, _name, script) in calc_nodes {
80 let value = match eval_script(&mut interpreter, &script) {
83 Ok(v) => v,
84 Err(_) => continue,
85 };
86
87 let value_str = value_to_string(&value);
89
90 let node = form.get_mut(id);
91 if let FormNodeType::Field { ref mut value } = node.node_type {
92 if *value != value_str {
93 *value = value_str;
94 result.updated_fields.push(id);
95 }
96 }
97 }
98
99 Ok(result)
100}
101
102pub fn run_validations(form: &FormTree) -> Result<ScriptResult, ScriptError> {
107 let mut result = ScriptResult::default();
108 let mut interpreter = Interpreter::new();
109
110 for (i, node) in form.nodes.iter().enumerate() {
111 if let Some(ref script) = node.validate {
112 let val =
113 eval_script(&mut interpreter, script).map_err(|e| ScriptError::Execution {
114 node: node.name.clone(),
115 message: e,
116 })?;
117
118 if !is_truthy(&val) {
119 let msg = format!(
120 "Validation script returned falsy value: {}",
121 value_to_string(&val)
122 );
123 result.validation_errors.push((FormNodeId(i), msg));
124 }
125 }
126 }
127
128 Ok(result)
129}
130
131pub fn prepare_form(form: &mut FormTree) -> Result<ScriptResult, ScriptError> {
136 let mut calc_result = run_calculations(form)?;
137 let val_result = run_validations(form)?;
138 calc_result.validation_errors = val_result.validation_errors;
139 Ok(calc_result)
140}
141
142fn eval_script(interpreter: &mut Interpreter, script: &str) -> Result<Value, String> {
144 let tokens = tokenize(script).map_err(|e| format!("Tokenize error: {e}"))?;
145 let ast = parser::parse(tokens).map_err(|e| format!("Parse error: {e}"))?;
146 interpreter
147 .exec(&ast)
148 .map_err(|e| format!("Runtime error: {e}"))
149}
150
151fn value_to_string(val: &Value) -> String {
153 match val {
154 Value::Number(n) => {
155 if *n == n.floor() && n.is_finite() {
157 format!("{}", *n as i64)
158 } else {
159 format!("{n}")
160 }
161 }
162 Value::String(s) => s.clone(),
163 Value::Null => String::new(),
164 }
165}
166
167fn is_truthy(val: &Value) -> bool {
169 match val {
170 Value::Number(n) => *n != 0.0,
171 Value::String(s) => !s.is_empty(),
172 Value::Null => false,
173 }
174}
175
176pub fn run_js_calculations(
192 form: &mut FormTree,
193 scripts: &[(FormNodeId, &str)],
194 runtime: &mut XfaJsRuntime,
195 cancel: Arc<AtomicBool>,
196) -> Result<ScriptResult, ScriptError> {
197 let mut result = ScriptResult::default();
198
199 for (target_id, script) in scripts {
200 let mut field_values = FieldValues::new();
202 for node in &form.nodes {
203 if let FormNodeType::Field { value } = &node.node_type {
204 if !node.name.is_empty() {
205 field_values.set(&node.name, value.as_str());
206 }
207 }
208 }
209
210 let ctx = ExecCtx {
211 fields: &mut field_values,
212 cancel: Arc::clone(&cancel),
213 event_new_text: None,
214 };
215
216 if let Ok(js_val) = runtime.execute_calculate(script, ctx) {
217 let raw = js_val.to_raw_string();
221 let mut wrote_via_return = false;
222 if !raw.is_empty() {
223 let node = &mut form.nodes[target_id.0];
224 if let FormNodeType::Field { value } = &mut node.node_type {
225 if *value != raw {
226 *value = raw;
227 result.updated_fields.push(*target_id);
228 wrote_via_return = true;
229 }
230 }
231 }
232
233 for (i, node) in form.nodes.iter_mut().enumerate() {
235 if let FormNodeType::Field { value } = &mut node.node_type {
236 if let Some(new_val) = field_values.get(&node.name) {
237 if new_val != value.as_str() {
238 let id = FormNodeId(i);
239 if !(wrote_via_return && id == *target_id) {
241 *value = new_val.to_string();
242 if !result.updated_fields.contains(&id) {
243 result.updated_fields.push(id);
244 }
245 }
246 }
247 }
248 }
249 }
250 }
251 }
253
254 Ok(result)
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use crate::form::{FormNode, Occur};
261 use crate::text::FontMetrics;
262 use crate::types::{BoxModel, LayoutStrategy};
263
264 fn make_field_with_calc(
265 tree: &mut FormTree,
266 name: &str,
267 initial_value: &str,
268 calculate: Option<&str>,
269 ) -> FormNodeId {
270 tree.add_node(FormNode {
271 name: name.to_string(),
272 node_type: FormNodeType::Field {
273 value: initial_value.to_string(),
274 },
275 box_model: BoxModel {
276 width: Some(100.0),
277 height: Some(20.0),
278 max_width: f64::MAX,
279 max_height: f64::MAX,
280 ..Default::default()
281 },
282 layout: LayoutStrategy::Positioned,
283 children: vec![],
284 occur: Occur::once(),
285 font: FontMetrics::default(),
286 calculate: calculate.map(|s| s.to_string()),
287 validate: None,
288 column_widths: vec![],
289 col_span: 1,
290 })
291 }
292
293 #[test]
294 fn calculate_script_updates_field_value() {
295 let mut tree = FormTree::new();
296 make_field_with_calc(&mut tree, "Total", "0", Some("10 + 20"));
297
298 let result = run_calculations(&mut tree).unwrap();
299
300 assert_eq!(result.updated_fields.len(), 1);
301 if let FormNodeType::Field { value } = &tree.get(result.updated_fields[0]).node_type {
302 assert_eq!(value, "30");
303 } else {
304 panic!("Expected Field node");
305 }
306 }
307
308 #[test]
309 fn calculate_script_string_result() {
310 let mut tree = FormTree::new();
311 make_field_with_calc(
312 &mut tree,
313 "Greeting",
314 "",
315 Some("Concat(\"Hello\", \" \", \"World\")"),
316 );
317
318 let result = run_calculations(&mut tree).unwrap();
319
320 assert_eq!(result.updated_fields.len(), 1);
321 if let FormNodeType::Field { value } = &tree.get(result.updated_fields[0]).node_type {
322 assert_eq!(value, "Hello World");
323 }
324 }
325
326 #[test]
327 fn no_update_when_value_unchanged() {
328 let mut tree = FormTree::new();
329 make_field_with_calc(&mut tree, "Same", "42", Some("42"));
330
331 let result = run_calculations(&mut tree).unwrap();
332
333 assert_eq!(result.updated_fields.len(), 0); }
335
336 #[test]
337 fn fields_without_scripts_are_untouched() {
338 let mut tree = FormTree::new();
339 make_field_with_calc(&mut tree, "Static", "original", None);
340
341 let result = run_calculations(&mut tree).unwrap();
342
343 assert_eq!(result.updated_fields.len(), 0);
344 if let FormNodeType::Field { value } = &tree.get(FormNodeId(0)).node_type {
345 assert_eq!(value, "original");
346 }
347 }
348
349 #[test]
350 fn validation_passes_for_truthy() {
351 let mut tree = FormTree::new();
352 let id = tree.add_node(FormNode {
353 name: "Amount".to_string(),
354 node_type: FormNodeType::Field {
355 value: "100".to_string(),
356 },
357 box_model: BoxModel {
358 width: Some(100.0),
359 height: Some(20.0),
360 max_width: f64::MAX,
361 max_height: f64::MAX,
362 ..Default::default()
363 },
364 layout: LayoutStrategy::Positioned,
365 children: vec![],
366 occur: Occur::once(),
367 font: FontMetrics::default(),
368 calculate: None,
369 validate: Some("1".to_string()), column_widths: vec![],
371 col_span: 1,
372 });
373 let _ = id;
374
375 let result = run_validations(&tree).unwrap();
376 assert!(result.validation_errors.is_empty());
377 }
378
379 #[test]
380 fn validation_fails_for_falsy() {
381 let mut tree = FormTree::new();
382 tree.add_node(FormNode {
383 name: "Required".to_string(),
384 node_type: FormNodeType::Field {
385 value: "".to_string(),
386 },
387 box_model: BoxModel {
388 width: Some(100.0),
389 height: Some(20.0),
390 max_width: f64::MAX,
391 max_height: f64::MAX,
392 ..Default::default()
393 },
394 layout: LayoutStrategy::Positioned,
395 children: vec![],
396 occur: Occur::once(),
397 font: FontMetrics::default(),
398 calculate: None,
399 validate: Some("0".to_string()), column_widths: vec![],
401 col_span: 1,
402 });
403
404 let result = run_validations(&tree).unwrap();
405 assert_eq!(result.validation_errors.len(), 1);
406 }
407
408 #[test]
409 fn prepare_form_runs_both() {
410 let mut tree = FormTree::new();
411 make_field_with_calc(&mut tree, "Sum", "0", Some("5 * 3"));
413 tree.add_node(FormNode {
415 name: "Check".to_string(),
416 node_type: FormNodeType::Field {
417 value: "ok".to_string(),
418 },
419 box_model: BoxModel {
420 width: Some(100.0),
421 height: Some(20.0),
422 max_width: f64::MAX,
423 max_height: f64::MAX,
424 ..Default::default()
425 },
426 layout: LayoutStrategy::Positioned,
427 children: vec![],
428 occur: Occur::once(),
429 font: FontMetrics::default(),
430 calculate: None,
431 validate: Some("0".to_string()), column_widths: vec![],
433 col_span: 1,
434 });
435
436 let result = prepare_form(&mut tree).unwrap();
437
438 assert_eq!(result.updated_fields.len(), 1);
440 if let FormNodeType::Field { value } = &tree.get(FormNodeId(0)).node_type {
441 assert_eq!(value, "15");
442 }
443 assert_eq!(result.validation_errors.len(), 1);
445 }
446
447 #[test]
448 fn complex_calculation() {
449 let mut tree = FormTree::new();
450 make_field_with_calc(&mut tree, "Tax", "0", Some("Round(100 * 0.21, 2)"));
451
452 let result = run_calculations(&mut tree).unwrap();
453 assert_eq!(result.updated_fields.len(), 1);
454 if let FormNodeType::Field { value } = &tree.get(result.updated_fields[0]).node_type {
455 assert_eq!(value, "21");
456 }
457 }
458}