Forms: simplified field notation written as real JavaScript, not AFMakeNumber(0) (#1995)

calculate_js wrote a field's simplified field notation as
"/** BVCALC expr EVCALC **/ event.value = AFMakeNumber(0);". PdfCraft reads the
comment, but a viewer that runs the script instead (Acrobat among them) computed
0. notation_js now writes Acrobat's own translation after the comment: each
field name becomes AFMakeNumber(this.getField("name").value), with \ escapes
in names honoured, numbers and operators kept.

Co-authored-by: Osvaldo Duran <338149330+regendorf-ux@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
regendorf-ux
2026-10-15 02:37:30 -04:00
committed by GitHub
co-authored by Osvaldo Duran Claude Opus 5.5
parent 77640a030f
commit 9ed12cb6a8
+44 -1
View File
@@ -446,7 +446,9 @@ pub fn calculate_js(c: &Calculate) -> Option<String> {
js_str(op.code()),
fields.iter().map(|f| js_str(f)).collect::<Vec<_>>().join(", ")
)),
Calculate::Notation(expr) => Some(format!("/** BVCALC {expr} EVCALC **/ event.value = AFMakeNumber(0);")),
// Acrobat's own translation follows the comment, so a viewer that runs the script instead
// of reading the comment computes the same value (a placeholder here showed 0).
Calculate::Notation(expr) => Some(format!("/** BVCALC {expr} EVCALC **/ event.value = {};", notation_js(expr))),
}
}
@@ -864,6 +866,41 @@ pub fn validate(v: &Validate, value: &str) -> Result<(), String> {
})
}
/// Simplified field notation as the JavaScript Acrobat writes after its `BVCALC` comment:
/// each field name becomes `AFMakeNumber(this.getField("name").value)`, the rest stays.
pub fn notation_js(expr: &str) -> String {
let mut out = String::new();
let cs: Vec<char> = expr.chars().collect();
let mut i = 0;
while let Some(&c) = cs.get(i) {
if c.is_whitespace() || "+-*/()".contains(c) {
out.push(c);
i += 1;
} else if c.is_ascii_digit() || c == '.' {
while let Some(&d) = cs.get(i).filter(|d| d.is_ascii_digit() || **d == '.') {
out.push(d);
i += 1;
}
} else {
let mut name = String::new();
while let Some(&n) = cs.get(i).filter(|n| !(n.is_whitespace() || "+-*/()".contains(**n))) {
// `\` escapes a space or an operator inside a name, as Acrobat writes them.
if n == '\\'
&& let Some(&next) = cs.get(i + 1)
{
name.push(next);
i += 2;
continue;
}
name.push(n);
i += 1;
}
out.push_str(&format!("AFMakeNumber(this.getField({}).value)", js_str(&name)));
}
}
out
}
/// Evaluate simplified field notation: + - * / and parentheses over numbers and field names
/// (`\` escapes spaces and operators in names, as Acrobat writes them).
fn eval_notation(src: &str, value_of: &dyn Fn(&str) -> f64) -> Option<f64> {
@@ -1024,6 +1061,12 @@ mod tests {
assert_eq!(parse_calculate(&calculate_js(&c).unwrap()), Some(c));
let c = Calculate::Notation("a + b".into());
assert_eq!(parse_calculate(&calculate_js(&c).unwrap()), Some(c));
// The JavaScript after the comment is Acrobat's translation, so viewers that run it
// instead of reading the comment compute the same value (not 0).
assert_eq!(
calculate_js(&Calculate::Notation("(Unit\\ Price * Qty) / 2".into())).unwrap(),
"/** BVCALC (Unit\\ Price * Qty) / 2 EVCALC **/ event.value = (AFMakeNumber(this.getField(\"Unit Price\").value) * AFMakeNumber(this.getField(\"Qty\").value)) / 2;"
);
}
#[test]