From e0eb1eff012b160a0d8e89a23fc321f82b85dc2a Mon Sep 17 00:00:00 2001
From: Eric Kaden <608341+EricKaden@users.noreply.github.com>
Date: Sat, 24 Oct 2026 14:50:22 -0400
Subject: [PATCH] M6: a script's field-highlight change reaches the next edit
of a batch, and is kept only when its request is
---
crates/engine/src/lib.rs | 6 ++++++
crates/engine/src/tests.rs | 29 +++++++++++++++++++++++++++++
crates/js/src/tests.rs | 10 ++++++++++
crates/js/src/xfa.rs | 7 +++++--
4 files changed, 50 insertions(+), 2 deletions(-)
diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs
index c4e15e4854..67083e1233 100644
--- a/crates/engine/src/lib.rs
+++ b/crates/engine/src/lib.rs
@@ -2687,6 +2687,8 @@ fn run_edit(doc: &mut pdfcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) ->
let ran = xfa::on_event(doc, &tpl, som, activity, &mut run);
cx.xfa_datasets = run.datasets;
cx.xfa_ran_away |= run.ran_away;
+ // The next edit of a batch reads the highlight this one's scripts set.
+ cx.highlight = run.highlight;
ran.map_err(EditError::Invalid)?;
}
Edit::SetFieldImage { name, image } => {
@@ -3484,6 +3486,8 @@ fn xfa_after_data_change(doc: &mut pdfcraft_cos::Document, cx: &mut EditCtx, cal
.unwrap_or_else(|m| Err(format!("laying the form out again failed unexpectedly ({m})")));
cx.xfa_datasets = run.datasets;
cx.xfa_ran_away |= run.ran_away;
+ // The next edit of a batch reads the highlight this one's scripts set.
+ cx.highlight = run.highlight;
if let Err(e) = r {
cx.xfa_out.errors.push(e);
}
@@ -4071,6 +4075,8 @@ impl Session {
.unwrap_or_else(|m| Err(format!("the form's scripts failed unexpectedly ({m})")));
cx.xfa_datasets = run.datasets;
cx.xfa_ran_away |= run.ran_away;
+ // The next edit of a batch reads the highlight this one's scripts set.
+ cx.highlight = run.highlight;
if let Err(e) = ran {
cx.xfa_out.errors.push(e);
}
diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs
index 16278390cc..8b739e3056 100644
--- a/crates/engine/src/tests.rs
+++ b/crates/engine/src/tests.rs
@@ -8102,6 +8102,35 @@ fn a_forms_layout_ready_scripts_run_once_it_is_laid_out() {
assert!(s.take_js_output(id).requests.is_empty());
}
+/// In a batch of XFA events, a later event's scripts read the field highlight an earlier one's set:
+/// `on`'s click turns it on, and `look`'s click, in the same batch, records what it reads.
+#[test]
+fn a_later_xfa_event_in_a_batch_reads_the_highlight_an_earlier_one_set() {
+ let script = |js: &str| format!(r#""#);
+ let xdp = format!(
+ r##"
+
+
+
+
+ {on}
+ {look}
+
+
+
+
+"##,
+ on = script("app.runtimeHighlight = true;"),
+ look = script("xfa.form.form.seen.rawValue = String(app.runtimeHighlight);"),
+ );
+ let mut s = Session::new();
+ let id = s.open("batch.pdf", None, Arc::new(pdfcraft_xfa::fixtures::shell(&xdp)), None).expect("opens");
+ let click = |som: &str| Edit::XfaEvent { som: som.into(), activity: "click".into() };
+ s.apply(id, Edit::Batch { edits: vec![click("form[0].on[0]"), click("form[0].look[0]")], label: "Click".into() }).expect("clicks");
+ let seen = s.get(id).unwrap().form.iter().find(|f| f.name == "seen").map(|f| f.value.clone()).unwrap_or_default();
+ assert_eq!(seen, vec!["true".to_string()]);
+}
+
/// Field scripts of one edit read the field highlight an earlier one set: `on`'s calculation turns
/// it on, and `seen`'s, which runs after it, records what it reads.
#[test]
diff --git a/crates/js/src/tests.rs b/crates/js/src/tests.rs
index d29899125c..9ceb8dbcf2 100644
--- a/crates/js/src/tests.rs
+++ b/crates/js/src/tests.rs
@@ -1579,6 +1579,16 @@ mod runtime_highlight {
assert_eq!(o.effects, vec![XfaEffect::Highlight(true)]);
let o = run_xfa(script, &ev, &XfaDoc { highlight: true, ..Default::default() }, &root, Limits::default());
assert_eq!((o.result.as_deref(), o.effects.len()), (Some("true,true"), 0), "already on: nothing to ask for");
+ // Past the effect cap the request is dropped, and the script still reads the setting the
+ // viewer has.
+ let full = format!(
+ "for (var i = 0; i < {}; i++) {{ xfa.host.beep(); }} app.runtimeHighlight = true; String(app.runtimeHighlight)",
+ crate::xfa::MAX_EFFECTS
+ );
+ let o = run_xfa(&full, &ev, &XfaDoc::default(), &root, Limits::default());
+ assert_eq!(o.error, None, "{o:?}");
+ assert_eq!(o.result.as_deref(), Some("false"));
+ assert!(!o.effects.contains(&XfaEffect::Highlight(true)));
}
}
diff --git a/crates/js/src/xfa.rs b/crates/js/src/xfa.rs
index 3ad410c0d6..766fbf566c 100644
--- a/crates/js/src/xfa.rs
+++ b/crates/js/src/xfa.rs
@@ -1159,9 +1159,12 @@ fn runtime_highlight(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<
fn set_runtime_highlight(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult {
let on = arg(args, 0).to_boolean();
let mut h = host(ctx)?.borrow_mut();
- if h.doc.highlight != on {
+ let e = XfaEffect::Highlight(on);
+ // Changed only when the request is kept: past the effect cap the script goes on reading the
+ // setting the viewer has.
+ if h.doc.highlight != on && h.reserve(&e, payload(&e)) {
h.doc.highlight = on;
- h.push(XfaEffect::Highlight(on));
+ h.record(e);
}
Ok(JsValue::undefined())
}