mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
M12: XFA open-time scripts start from one shared copy of the form
On a large dynamic XFA form, every script of an event copied the whole form twice before it ran (the engine's live form to the script engine's tree, then that tree to the flattened form the host works on), and every resetData call rewrote and rescanned the whole document. On a 1,300-script form opening spent its 3 s budget on these copies and on twenty resets, and skipped the last 850 initialize and calculate scripts. The engine now sends the scripts of an event one shared tree (Arc) until the live form changes in any way, and the script thread keeps the flattened form it built from that tree; each script starts from a copy whose text is shared (Arc<str>), so it costs a fraction of a flatten. Each script still gets its own copy: nothing one script changes reaches the next except through the engine. Resets in a row in one script's effects are applied as one reset of the names together. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK
This commit is contained in:
1 parent
bc5aff5ff6
commit
d65c465b1b
13 files changed
+294
-71
No files matched your search
@@ -6576,6 +6576,57 @@ fn xfa_initialize_scripts_that_hide_subforms_lay_the_form_out_once() {
|
||||
assert_eq!(names(&s, again), opened);
|
||||
}
|
||||
|
||||
/// A script that resets several subforms one after another (`xfa.host.resetData` once for
|
||||
/// each, as forms with optional pages do) resets every one of them, and nothing else; resets
|
||||
/// in a row are applied together.
|
||||
#[test]
|
||||
fn xfa_resets_in_a_row_reset_every_named_subform_and_nothing_else() {
|
||||
let sections = 5;
|
||||
let mut body = String::from(
|
||||
r#"<field name="wanted" w="0.3in" h="0.3in"><ui><checkButton/></ui><items><integer>1</integer><integer>0</integer></items><value><integer>0</integer></value>"#,
|
||||
);
|
||||
let resets: String = (0..sections).map(|n| format!("xfa.host.resetData('form.extra{n}');")).collect();
|
||||
body.push_str(&format!(
|
||||
r#"<event activity="initialize"><script contentType="application/x-javascript">{resets} keep.rawValue = "kept";</script></event></field>"#
|
||||
));
|
||||
body.push_str(r#"<field name="keep" w="3in" h="0.3in"><ui><textEdit/></ui></field>"#);
|
||||
for n in 0..sections {
|
||||
body.push_str(&format!(
|
||||
r#"<subform name="extra{n}" layout="tb"><field name="note" w="3in" h="0.3in"><ui><textEdit/></ui><value><text>default</text></value></field></subform>"#
|
||||
));
|
||||
}
|
||||
let notes: String = (0..sections).map(|n| format!("<extra{n}><note>typed {n}</note></extra{n}>")).collect();
|
||||
let tpl = format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
|
||||
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
|
||||
<subform name="form" layout="tb">
|
||||
<pageSet><pageArea name="front"><occur max="-1"/><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||||
{body}
|
||||
</subform>
|
||||
</template>
|
||||
<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data><form><keep>typed</keep>{notes}</form></xfa:data></xfa:datasets>
|
||||
</xdp:xdp>
|
||||
"#
|
||||
);
|
||||
let mut s = Session::new().with_clock(|| 1_700_000_000).with_xfa_time_scale(100);
|
||||
// Without the resets the sections show what was typed.
|
||||
let untouched = Arc::new(pdfcraft_xfa::fixtures::shell(&tpl.replace(&resets, "")));
|
||||
let plain = s.open("plain.pdf", None, untouched, None).expect("opens");
|
||||
let typed: Vec<_> = s.get(plain).unwrap().form.iter().filter(|f| f.name.contains("note")).map(|f| f.value.clone()).collect();
|
||||
assert_eq!(typed, (0..sections).map(|n| vec![format!("typed {n}")]).collect::<Vec<_>>());
|
||||
let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&tpl));
|
||||
let id = s.open("resets.pdf", None, bytes, None).expect("opens");
|
||||
let out = s.take_js_output(id);
|
||||
assert!(out.errors.is_empty(), "{:?}", out.errors);
|
||||
let form = &s.get(id).unwrap().form;
|
||||
let notes: Vec<_> = form.iter().filter(|f| f.name.contains("note")).map(|f| f.value.clone()).collect();
|
||||
assert_eq!(notes.len(), sections, "{form:?}");
|
||||
assert!(notes.iter().all(|v| v == &vec!["default".to_string()]), "every section is reset: {notes:?}");
|
||||
let keep = form.iter().find(|f| f.name.contains("keep")).map(|f| f.value.clone());
|
||||
assert_eq!(keep, Some(vec!["kept".to_string()]), "the script's later write stands");
|
||||
}
|
||||
|
||||
/// The scripts of one event share the form as the engine holds it: a failed script's
|
||||
/// half-done writes reach nobody, and a script reads the saved value a choice list was given
|
||||
/// by its shown text.
|
||||
|
||||
@@ -159,6 +159,23 @@ fn to_js(n: &FormNode) -> XfaNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the names of a `resetData` call to the run of resets before it. No names means the
|
||||
/// whole form, which takes in every other.
|
||||
fn merge_reset(merged: &mut Vec<String>, names: &[String]) {
|
||||
if merged.is_empty() {
|
||||
return;
|
||||
}
|
||||
if names.is_empty() {
|
||||
merged.clear();
|
||||
return;
|
||||
}
|
||||
for n in names {
|
||||
if !merged.contains(n) {
|
||||
merged.push(n.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_container<'a>(n: &'a FormNode, som: &str, depth: usize) -> Option<&'a FormNode> {
|
||||
if depth > 64 {
|
||||
return None;
|
||||
@@ -275,6 +292,9 @@ struct Runner<'a, 'b> {
|
||||
fields: Option<HashMap<String, FieldInfo>>,
|
||||
/// The live form as last built; dropped when a script changed the form.
|
||||
cached: Option<LiveForm>,
|
||||
/// `cached` as the scripts get it, made once and sent to each script until the live form
|
||||
/// changes in any way (a value too): the script engine reuses what it built from it.
|
||||
js_root: Option<Arc<XfaNode>>,
|
||||
/// The script engine for this event: one thread for all its scripts, each run in a fresh
|
||||
/// context on the form as it is then (`cached`, kept up to date in place for values and
|
||||
/// rebuilt when the structure changed).
|
||||
@@ -309,6 +329,7 @@ impl<'a, 'b> Runner<'a, 'b> {
|
||||
by_path: None,
|
||||
fields: None,
|
||||
cached: None,
|
||||
js_root: None,
|
||||
session: None,
|
||||
changes: Changes::default(),
|
||||
}
|
||||
@@ -318,6 +339,7 @@ impl<'a, 'b> Runner<'a, 'b> {
|
||||
/// script.
|
||||
fn invalidate(&mut self) {
|
||||
self.cached = None;
|
||||
self.js_root = None;
|
||||
}
|
||||
|
||||
/// The script engine for this event.
|
||||
@@ -334,6 +356,8 @@ impl<'a, 'b> Runner<'a, 'b> {
|
||||
let op = pdfcraft_xfa::DataOp::Value { path, text };
|
||||
self.data.get_or_insert_with(Default::default).apply(&op);
|
||||
self.pending.push(op);
|
||||
// The value changes in the live form below: the scripts' copy is made again.
|
||||
self.js_root = None;
|
||||
let refreshed = match self.cached.as_mut() {
|
||||
Some(live) => pdfcraft_xfa::refresh_value(&mut live.root, self.tpl, self.data.as_ref(), som).is_some(),
|
||||
None => true,
|
||||
@@ -591,7 +615,9 @@ impl<'a, 'b> Runner<'a, 'b> {
|
||||
}
|
||||
}
|
||||
}
|
||||
for e in effects {
|
||||
// Resets in a row, applied together when the run of them ends.
|
||||
let mut pending_reset: Option<Vec<String>> = None;
|
||||
for (at, e) in effects.iter().enumerate() {
|
||||
// Only a click script may print, save, open links or move the focus (not the
|
||||
// calculations a click sets off, nor anything run on open or on a change).
|
||||
if let Some(what) = click_only(e)
|
||||
@@ -640,7 +666,19 @@ impl<'a, 'b> Runner<'a, 'b> {
|
||||
relayout = true;
|
||||
}
|
||||
XfaEffect::MessageBox(m) => self.alert(m.clone()),
|
||||
XfaEffect::ResetData(names) => self.reset(names)?,
|
||||
XfaEffect::ResetData(names) => {
|
||||
// A script resetting several subforms in a row (`resetData` once for each)
|
||||
// resets them together: the same fields end up at their defaults.
|
||||
if let Some(merged) = pending_reset.as_mut() {
|
||||
merge_reset(merged, names);
|
||||
} else {
|
||||
pending_reset = Some(names.clone());
|
||||
}
|
||||
let next_is_reset = effects.get(at.saturating_add(1)).is_some_and(|e| matches!(e, XfaEffect::ResetData(_)));
|
||||
if !next_is_reset && let Some(names) = pending_reset.take() {
|
||||
self.reset(&names)?;
|
||||
}
|
||||
}
|
||||
XfaEffect::Print => self.run.out.requests.push(Request::Print),
|
||||
XfaEffect::SaveAs => self.run.out.requests.push(Request::SaveAs),
|
||||
XfaEffect::LaunchUrl(u) => self.run.out.requests.push(Request::LaunchUrl(u.clone())),
|
||||
@@ -669,7 +707,14 @@ impl<'a, 'b> Runner<'a, 'b> {
|
||||
#[cfg(test)]
|
||||
self.check_live();
|
||||
let event = XfaEvent { activity: ev.activity.clone(), target: ev.som.clone(), new_text: new_text.to_string(), prev_text: String::new() };
|
||||
let root = to_js(&self.live().root);
|
||||
let root = match &self.js_root {
|
||||
Some(r) => Arc::clone(r),
|
||||
None => {
|
||||
let r = Arc::new(to_js(&self.live().root));
|
||||
self.js_root = Some(Arc::clone(&r));
|
||||
r
|
||||
}
|
||||
};
|
||||
let script = Script {
|
||||
source: ev.script.clone(),
|
||||
formcalc: ev.formcalc,
|
||||
|
||||
@@ -109,7 +109,7 @@ type R<T> = Result<T, String>;
|
||||
/// Run a FormCalc `script` for `event` on the form `root`, as [`crate::xfa::run_xfa`] runs a
|
||||
/// JavaScript one.
|
||||
pub fn run_formcalc(script: &str, event: &XfaEvent, doc: &XfaDoc, root: &XfaNode, limits: Limits) -> XfaOutcome {
|
||||
run_formcalc_at(script, event, doc, root.clone(), limits, None)
|
||||
run_formcalc_at(script, event, doc, std::sync::Arc::new(root.clone()), false, limits, None)
|
||||
}
|
||||
|
||||
/// [`run_formcalc`] on an owned form, giving up at `deadline` (the clock is read every
|
||||
@@ -118,7 +118,8 @@ pub(crate) fn run_formcalc_at(
|
||||
script: &str,
|
||||
event: &XfaEvent,
|
||||
doc: &XfaDoc,
|
||||
root: XfaNode,
|
||||
root: std::sync::Arc<XfaNode>,
|
||||
reuse: bool,
|
||||
limits: Limits,
|
||||
deadline: Option<std::time::Duration>,
|
||||
) -> XfaOutcome {
|
||||
@@ -133,7 +134,7 @@ pub(crate) fn run_formcalc_at(
|
||||
Ok(b) => b,
|
||||
Err(e) => return XfaOutcome { error: Some(e), ..Default::default() },
|
||||
};
|
||||
let mut host = XHost::new(root, &event.target, doc);
|
||||
let mut host = XHost::new(root, reuse, &event.target, doc);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = deadline;
|
||||
let mut interp = Interp {
|
||||
@@ -177,7 +178,7 @@ pub fn run_formcalc_within(script: &str, event: &XfaEvent, doc: &XfaDoc, root: X
|
||||
// FormCalc has no engine to start: its time (and its own deadline) begins at once.
|
||||
run_within("pdfcraft-formcalc", timeout, move |started| {
|
||||
started();
|
||||
run_formcalc_at(&script, &event, &doc, root, limits, Some(timeout))
|
||||
run_formcalc_at(&script, &event, &doc, std::sync::Arc::new(root), false, limits, Some(timeout))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -313,16 +313,16 @@ impl Interp<'_> {
|
||||
let Some(n) = self.node(i) else { return Ok(Val::Null) };
|
||||
Ok(match name {
|
||||
"formattedValue" => match &n.formatted {
|
||||
Some(f) => Val::Str(f.clone()),
|
||||
Some(f) => Val::Str(f.to_string()),
|
||||
None => self.node_value(i),
|
||||
},
|
||||
"rawValue" | "value" => self.node_value(i),
|
||||
"name" => Val::Str(n.name.clone()),
|
||||
"somExpression" => Val::Str(n.som.clone()),
|
||||
"name" => Val::Str(n.name.to_string()),
|
||||
"somExpression" => Val::Str(n.som.to_string()),
|
||||
"className" => Val::Str(n.kind.class_name().into()),
|
||||
"index" | "instanceIndex" => Val::Num(n.index as f64),
|
||||
"presence" => Val::Str(n.presence.clone()),
|
||||
"access" => Val::Str(n.access.clone()),
|
||||
"presence" => Val::Str(n.presence.to_string()),
|
||||
"access" => Val::Str(n.access.to_string()),
|
||||
"isNull" => Val::Num(if n.value.is_empty() { 1.0 } else { 0.0 }),
|
||||
"mandatory" => Val::Str("disabled".into()),
|
||||
"parent" => n.parent.map_or(Val::Null, Val::Node),
|
||||
@@ -332,7 +332,7 @@ impl Interp<'_> {
|
||||
None => Val::Node(i),
|
||||
},
|
||||
"instanceManager" => match n.parent {
|
||||
Some(p) => Val::Im(p, n.name.clone()),
|
||||
Some(p) => Val::Im(p, n.name.to_string()),
|
||||
None => Val::Null,
|
||||
},
|
||||
"border" | "ui" | "font" | "caption" | "fillColor" | "borderColor" | "fontColor" | "margin" | "para" | "assist" | "validate"
|
||||
|
||||
@@ -277,11 +277,11 @@ impl Interp<'_> {
|
||||
Some(e) => {
|
||||
let raw = self.eval(e)?;
|
||||
match self.nodes_of(&raw).first() {
|
||||
Some(i) => self.node(*i).map(|n| n.som.clone()).unwrap_or_default(),
|
||||
Some(i) => self.node(*i).map(|n| n.som.to_string()).unwrap_or_default(),
|
||||
None => {
|
||||
let s = self.to_str(&raw);
|
||||
let found = resolve_som(self.h, self.h.current, &s);
|
||||
found.first().and_then(|i| self.node(*i)).map(|n| n.som.clone()).unwrap_or(s)
|
||||
found.first().and_then(|i| self.node(*i)).map(|n| n.som.to_string()).unwrap_or(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -396,7 +396,7 @@ impl Interp<'_> {
|
||||
match target {
|
||||
Val::Node(i) => {
|
||||
let Some(n) = self.node(i) else { return Ok(()) };
|
||||
let som = n.som.clone();
|
||||
let som = n.som.to_string();
|
||||
match prop {
|
||||
"rawValue" | "value" | "formattedValue" => {
|
||||
let text = self.to_str(&v);
|
||||
|
||||
+45
-3
@@ -1542,7 +1542,8 @@ $host.gotoURL("https://example.org")"#,
|
||||
"for i = 1 upto 100000 do Eval(\"1\") endfor",
|
||||
&ev,
|
||||
&doc,
|
||||
form(),
|
||||
std::sync::Arc::new(form()),
|
||||
false,
|
||||
Limits { loop_iterations: u64::MAX, recursion: 32 },
|
||||
Some(std::time::Duration::ZERO),
|
||||
);
|
||||
@@ -1660,12 +1661,53 @@ mod session {
|
||||
source: source.into(),
|
||||
formcalc,
|
||||
event: XfaEvent { activity: activity.into(), target: target.into(), ..Default::default() },
|
||||
root: form(),
|
||||
root: std::sync::Arc::new(form()),
|
||||
limits: Limits::default(),
|
||||
timeout: std::time::Duration::from_secs(3),
|
||||
}
|
||||
}
|
||||
|
||||
/// The scripts of an event given the same form start from one flattened copy of it: the
|
||||
/// second host shares the first one's text, and each host changes only its own copy.
|
||||
#[test]
|
||||
fn hosts_given_the_same_form_reuse_its_flattened_copy_and_change_only_their_own() {
|
||||
let root = std::sync::Arc::new(form());
|
||||
let doc = XfaDoc::default();
|
||||
let mut first = crate::xfa::XHost::new(std::sync::Arc::clone(&root), true, "form1[0].page1[0].qty[0]", &doc);
|
||||
let second = crate::xfa::XHost::new(std::sync::Arc::clone(&root), true, "form1[0].page1[0].qty[0]", &doc);
|
||||
let shared = std::sync::Arc::clone(&first.nodes.get(1).expect("form1").som);
|
||||
assert!(std::sync::Arc::ptr_eq(&shared, &second.nodes.get(1).expect("form1").som), "the second host starts from the first one's copy");
|
||||
let qty = first.current;
|
||||
let before = second.nodes.get(qty).map(|n| n.value.clone());
|
||||
first.set_state(qty, XfaEffect::SetPresence { som: "form1[0].page1[0].qty[0]".into(), presence: "hidden".into() });
|
||||
if let Some(n) = first.nodes.get_mut(qty) {
|
||||
n.value = "99".into();
|
||||
}
|
||||
let third = crate::xfa::XHost::new(std::sync::Arc::clone(&root), true, "form1[0].page1[0].qty[0]", &doc);
|
||||
for h in [&second, &third] {
|
||||
assert_eq!(h.nodes.get(qty).map(|n| n.value.clone()), before);
|
||||
assert_eq!(h.nodes.get(qty).map(|n| n.presence.to_string()).as_deref(), Some("visible"));
|
||||
}
|
||||
// Another form (a new `Arc`, even with the same content) is flattened again.
|
||||
let other = crate::xfa::XHost::new(std::sync::Arc::new(form()), true, "form1[0].page1[0].qty[0]", &doc);
|
||||
assert!(!std::sync::Arc::ptr_eq(&other.nodes.get(1).expect("form1").som, &shared));
|
||||
}
|
||||
|
||||
/// One session given the same form for two scripts: what the first changed on its copy is
|
||||
/// not in the second's.
|
||||
#[test]
|
||||
fn a_form_sent_again_does_not_carry_the_last_scripts_changes() {
|
||||
let mut s = XfaSession::new(XfaDoc::default());
|
||||
let root = std::sync::Arc::new(form());
|
||||
let mut a = script("qty.rawValue = 10; details.presence = 'visible'; qty.rawValue", false, "form1[0].page1[0].total[0]", "calculate");
|
||||
a.root = std::sync::Arc::clone(&root);
|
||||
assert_eq!(s.run(a).result.as_deref(), Some("10"));
|
||||
let mut b = script("qty.rawValue + ':' + details.presence", false, "form1[0].page1[0].total[0]", "calculate");
|
||||
b.root = std::sync::Arc::clone(&root);
|
||||
let o = s.run(b);
|
||||
assert_eq!(o.result.as_deref(), Some("4:hidden"), "{o:?}");
|
||||
}
|
||||
|
||||
/// JavaScript and FormCalc scripts run in turn in one session, each on the form the caller
|
||||
/// sends: nothing an earlier script changed or defined leaks into the next, a failing script
|
||||
/// doesn't end the session, and hundreds of scripts run on its one thread.
|
||||
@@ -1683,7 +1725,7 @@ mod session {
|
||||
let o = s.run(script("qty * price", true, "form1[0].page1[0].total[0]", "calculate"));
|
||||
assert_eq!(o.result.as_deref(), Some("10"), "{o:?}");
|
||||
let mut with = script("qty * price", true, "form1[0].page1[0].total[0]", "calculate");
|
||||
with.root.children[0].children[0].value = "10".into();
|
||||
std::sync::Arc::make_mut(&mut with.root).children[0].children[0].value = "10".into();
|
||||
assert_eq!(s.run(with).result.as_deref(), Some("25"));
|
||||
// Nor do variables, globals, prototype changes or kept objects.
|
||||
s.run(script("leak = 5; String.prototype.trim = function () { return 'P' }; kept = qty; 1", false, "form1[0].page1[0].qty[0]", "calculate"));
|
||||
|
||||
@@ -24,6 +24,8 @@ mod model;
|
||||
mod nodes;
|
||||
mod run;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) use host::forget_base;
|
||||
pub(crate) use host::{HNode, XHost};
|
||||
pub use model::{MAX_ALERTS, MAX_CONSOLE, MAX_EFFECTS, XfaDoc, XfaEffect, XfaEvent, XfaKind, XfaNode, XfaOutcome};
|
||||
pub(crate) use model::{MAX_LINE, MAX_PAYLOAD, clip, clip_result, reset_names};
|
||||
|
||||
@@ -69,13 +69,13 @@ fn set_focus(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsVal
|
||||
let som = match target.as_object() {
|
||||
Some(_) if node_index(&target, ctx).is_ok() => {
|
||||
let i = node_index(&target, ctx)?;
|
||||
host(ctx)?.borrow().nodes.get(i).map(|n| n.som.clone()).unwrap_or_default()
|
||||
host(ctx)?.borrow().nodes.get(i).map(|n| n.som.to_string()).unwrap_or_default()
|
||||
}
|
||||
_ => {
|
||||
let expr = text(&target, ctx)?;
|
||||
let h = host(ctx)?.borrow();
|
||||
let found = resolve_som(&h, h.current, &expr);
|
||||
found.first().and_then(|i| h.nodes.get(*i)).map(|n| n.som.clone()).unwrap_or(expr)
|
||||
found.first().and_then(|i| h.nodes.get(*i)).map(|n| n.som.to_string()).unwrap_or(expr)
|
||||
}
|
||||
};
|
||||
effect(ctx, XfaEffect::SetFocus(som))?;
|
||||
@@ -329,7 +329,7 @@ pub(super) fn install(ctx: &mut Context, event: &XfaEvent, doc: &XfaDoc, current
|
||||
let h = host(ctx)?.borrow();
|
||||
h.nodes
|
||||
.get(scope)
|
||||
.map(|n| n.children.iter().filter_map(|&c| h.nodes.get(c).map(|k| (c, k.name.clone(), k.repeatable && !k.gone))).collect())
|
||||
.map(|n| n.children.iter().filter_map(|&c| h.nodes.get(c).map(|k| (c, k.name.to_string(), k.repeatable && !k.gone))).collect())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
for (c, name, repeatable) in kids {
|
||||
|
||||
+80
-22
@@ -2,27 +2,71 @@ use super::model::{
|
||||
MAX_ALERTS, MAX_CONSOLE, MAX_DEPTH, MAX_EFFECTS, MAX_NODES, MAX_PAYLOAD, MAX_VALUE, XfaDoc, XfaEffect, XfaKind, XfaNode, clip, clip_value,
|
||||
effect_key, payload,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{Context, HashMap, JsResult, Message, RefCell, error};
|
||||
|
||||
/// One node of the flattened form, shared by the JavaScript and FormCalc engines.
|
||||
/// One node of the flattened form, shared by the JavaScript and FormCalc engines. Its text is
|
||||
/// shared (`Arc<str>`), so the copy each script starts from costs little: the scripts of one
|
||||
/// event start from the same flattened form until it changes (see [`XHost::new`]).
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct HNode {
|
||||
pub(crate) name: String,
|
||||
pub(crate) som: String,
|
||||
pub(crate) name: Arc<str>,
|
||||
pub(crate) som: Arc<str>,
|
||||
pub(crate) kind: XfaKind,
|
||||
pub(crate) value: String,
|
||||
pub(crate) numeric: bool,
|
||||
pub(crate) presence: String,
|
||||
pub(crate) access: String,
|
||||
pub(crate) presence: Arc<str>,
|
||||
pub(crate) access: Arc<str>,
|
||||
pub(crate) repeatable: bool,
|
||||
pub(crate) occur_min: usize,
|
||||
pub(crate) occur_max: Option<usize>,
|
||||
pub(crate) index: usize,
|
||||
pub(crate) parent: Option<usize>,
|
||||
pub(crate) children: Vec<usize>,
|
||||
pub(crate) children: Arc<[usize]>,
|
||||
/// Removed by `removeInstance`: skipped everywhere.
|
||||
pub(crate) gone: bool,
|
||||
/// See [`XfaNode::formatted`]; dropped when a script sets the value, which it no longer shows.
|
||||
pub(crate) formatted: Option<String>,
|
||||
pub(crate) formatted: Option<Arc<str>>,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// The form the last script on this thread started from, flattened: the next script of the
|
||||
/// event starts from a copy of it while the engine sends the same form.
|
||||
static BASE: std::cell::RefCell<Option<(Arc<XfaNode>, Vec<HNode>)>> = const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Let go of the form the scripts on this thread started from.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) fn forget_base() {
|
||||
BASE.with(|b| b.borrow_mut().take());
|
||||
}
|
||||
|
||||
/// `root` flattened under a synthetic `form` node (index 0).
|
||||
fn flattened(root: &XfaNode) -> Vec<HNode> {
|
||||
let mut nodes = vec![HNode {
|
||||
name: "form".into(),
|
||||
som: "".into(),
|
||||
kind: XfaKind::Form,
|
||||
value: String::new(),
|
||||
numeric: false,
|
||||
presence: "visible".into(),
|
||||
access: "open".into(),
|
||||
repeatable: false,
|
||||
occur_min: 0,
|
||||
occur_max: None,
|
||||
index: 0,
|
||||
parent: None,
|
||||
children: Arc::from([]),
|
||||
gone: false,
|
||||
formatted: None,
|
||||
}];
|
||||
if let Some(ci) = flatten(root, Some(0), &mut nodes, 1)
|
||||
&& let Some(f) = nodes.get_mut(0)
|
||||
{
|
||||
f.children = Arc::from([ci]);
|
||||
}
|
||||
nodes
|
||||
}
|
||||
|
||||
/// The form as a script sees and changes it, with the effects it records.
|
||||
@@ -53,11 +97,25 @@ pub(crate) struct XHost {
|
||||
impl XHost {
|
||||
/// The host for a script on `root` (the template's root subform), with `this` at the node
|
||||
/// named by `target`. `xfa.form` is a synthetic root whose child is the root subform.
|
||||
pub(crate) fn new(root: XfaNode, target: &str, doc: &XfaDoc) -> XHost {
|
||||
let form = XfaNode { name: "form".into(), kind: Some(XfaKind::Form), children: vec![root], ..Default::default() };
|
||||
let mut nodes = Vec::new();
|
||||
flatten(&form, None, &mut nodes, 0);
|
||||
let current = nodes.iter().position(|n| n.som == target && n.kind != XfaKind::Form).unwrap_or(1.min(nodes.len().saturating_sub(1)));
|
||||
/// With `reuse`, the flattened form is kept on this thread for the next script given the
|
||||
/// same `root` (the scripts of one event, [`crate::xfa_session`]).
|
||||
pub(crate) fn new(root: Arc<XfaNode>, reuse: bool, target: &str, doc: &XfaDoc) -> XHost {
|
||||
let nodes = if !reuse {
|
||||
flattened(&root)
|
||||
} else {
|
||||
BASE.with(|b| {
|
||||
let mut b = b.borrow_mut();
|
||||
match b.as_ref() {
|
||||
Some((r, nodes)) if Arc::ptr_eq(r, &root) => nodes.clone(),
|
||||
_ => {
|
||||
let nodes = flattened(&root);
|
||||
*b = Some((root, nodes.clone()));
|
||||
nodes
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
let current = nodes.iter().position(|n| &*n.som == target && n.kind != XfaKind::Form).unwrap_or(1.min(nodes.len().saturating_sub(1)));
|
||||
XHost {
|
||||
nodes,
|
||||
effects: Vec::new(),
|
||||
@@ -122,8 +180,8 @@ impl XHost {
|
||||
}
|
||||
if let Some(n) = self.nodes.get_mut(i) {
|
||||
match &e {
|
||||
XfaEffect::SetPresence { presence, .. } => n.presence = presence.clone(),
|
||||
XfaEffect::SetAccess { access, .. } => n.access = access.clone(),
|
||||
XfaEffect::SetPresence { presence, .. } => n.presence = presence.as_str().into(),
|
||||
XfaEffect::SetAccess { access, .. } => n.access = access.as_str().into(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -220,7 +278,7 @@ impl XHost {
|
||||
if n.gone || !matches!(n.kind, XfaKind::Field | XfaKind::ExclGroup) {
|
||||
return false;
|
||||
}
|
||||
let som = n.som.clone();
|
||||
let som = n.som.to_string();
|
||||
// Cut here, not only in the effect: later reads in the script see what is saved, and
|
||||
// the snapshot stays bounded however often a long value is written.
|
||||
let (text, cut) = clip_value(text);
|
||||
@@ -257,21 +315,21 @@ fn flatten(n: &XfaNode, parent: Option<usize>, out: &mut Vec<HNode>, depth: usiz
|
||||
}
|
||||
let idx = out.len();
|
||||
out.push(HNode {
|
||||
name: n.name.clone(),
|
||||
som: n.som.clone(),
|
||||
name: n.name.as_str().into(),
|
||||
som: n.som.as_str().into(),
|
||||
kind: n.kind.unwrap_or(XfaKind::Subform),
|
||||
value: n.value.clone(),
|
||||
numeric: n.numeric,
|
||||
presence: if n.presence.is_empty() { "visible".into() } else { n.presence.clone() },
|
||||
access: if n.access.is_empty() { "open".into() } else { n.access.clone() },
|
||||
presence: if n.presence.is_empty() { "visible".into() } else { n.presence.as_str().into() },
|
||||
access: if n.access.is_empty() { "open".into() } else { n.access.as_str().into() },
|
||||
repeatable: n.repeatable,
|
||||
occur_min: n.occur_min,
|
||||
occur_max: n.occur_max,
|
||||
index: n.index,
|
||||
parent,
|
||||
children: Vec::new(),
|
||||
children: Arc::from([]),
|
||||
gone: false,
|
||||
formatted: n.formatted.clone(),
|
||||
formatted: n.formatted.as_deref().map(Arc::from),
|
||||
});
|
||||
let mut kids = Vec::new();
|
||||
for c in &n.children {
|
||||
@@ -280,7 +338,7 @@ fn flatten(n: &XfaNode, parent: Option<usize>, out: &mut Vec<HNode>, depth: usiz
|
||||
}
|
||||
}
|
||||
if let Some(h) = out.get_mut(idx) {
|
||||
h.children = kids;
|
||||
h.children = kids.into();
|
||||
}
|
||||
Some(idx)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::host::{HNode, XHost, host};
|
||||
use super::model::{MAX_ADDED, MAX_DEPTH, MAX_NODES, XfaEffect};
|
||||
use super::nodes::{instances, node_object};
|
||||
@@ -112,14 +114,14 @@ fn renumber(h: &mut XHost, idx: usize, depth: usize) {
|
||||
}
|
||||
let (parent_som, name, index, kids) = {
|
||||
let Some(n) = h.nodes.get(idx) else { return };
|
||||
let parent_som = n.parent.and_then(|p| h.nodes.get(p)).map(|p| p.som.clone()).unwrap_or_default();
|
||||
let parent_som = n.parent.and_then(|p| h.nodes.get(p)).map(|p| p.som.to_string()).unwrap_or_default();
|
||||
(parent_som, n.name.clone(), n.index, n.children.clone())
|
||||
};
|
||||
let som = child_som(parent_som, &name, index);
|
||||
if let Some(n) = h.nodes.get_mut(idx) {
|
||||
n.som = som;
|
||||
n.som = som.into();
|
||||
}
|
||||
for k in kids {
|
||||
for k in kids.iter().copied() {
|
||||
renumber(h, k, depth + 1);
|
||||
}
|
||||
}
|
||||
@@ -145,7 +147,7 @@ fn clone_subtree(h: &mut XHost, src: usize, parent: Option<usize>, depth: usize)
|
||||
(
|
||||
HNode {
|
||||
name: n.name.clone(),
|
||||
som: String::new(),
|
||||
som: "".into(),
|
||||
kind: n.kind,
|
||||
value: String::new(),
|
||||
numeric: n.numeric,
|
||||
@@ -156,7 +158,7 @@ fn clone_subtree(h: &mut XHost, src: usize, parent: Option<usize>, depth: usize)
|
||||
occur_max: n.occur_max,
|
||||
index: n.index,
|
||||
parent,
|
||||
children: Vec::new(),
|
||||
children: Arc::from([]),
|
||||
gone: false,
|
||||
// The copy holds no value, so nothing is shown in its picture either.
|
||||
formatted: None,
|
||||
@@ -167,7 +169,7 @@ fn clone_subtree(h: &mut XHost, src: usize, parent: Option<usize>, depth: usize)
|
||||
let idx = h.nodes.len();
|
||||
h.nodes.push(template);
|
||||
let mut copied = Vec::new();
|
||||
for k in kids {
|
||||
for k in kids.iter().copied() {
|
||||
// Nested repeating subforms copy their first instance only.
|
||||
let skip = h.nodes.get(k).is_some_and(|c| c.gone || c.repeatable && c.index > 0);
|
||||
if skip {
|
||||
@@ -178,7 +180,7 @@ fn clone_subtree(h: &mut XHost, src: usize, parent: Option<usize>, depth: usize)
|
||||
}
|
||||
}
|
||||
if let Some(n) = h.nodes.get_mut(idx) {
|
||||
n.children = copied;
|
||||
n.children = copied.into();
|
||||
}
|
||||
Some(idx)
|
||||
}
|
||||
@@ -206,8 +208,8 @@ impl XHost {
|
||||
return None;
|
||||
}
|
||||
// The copy's SOM, as `renumber` will give it, so its effect is reserved before the copy.
|
||||
let name = h.nodes.get(first).map(|n| n.name.clone()).unwrap_or_default();
|
||||
let parent_som = h.nodes.get(parent).map(|p| p.som.clone()).unwrap_or_default();
|
||||
let name = h.nodes.get(first).map(|n| n.name.to_string()).unwrap_or_default();
|
||||
let parent_som = h.nodes.get(parent).map(|p| p.som.to_string()).unwrap_or_default();
|
||||
let som = child_som(parent_som, &name, live.len());
|
||||
if !h.reserve(&XfaEffect::AddInstance { som: String::new() }, som.len()) {
|
||||
return None;
|
||||
@@ -219,8 +221,10 @@ impl XHost {
|
||||
}
|
||||
// Keep instances together: right after the last one.
|
||||
if let Some(p) = h.nodes.get_mut(parent) {
|
||||
let pos = p.children.iter().position(|&c| c == last).map_or(p.children.len(), |i| i + 1);
|
||||
p.children.insert(pos, new);
|
||||
let mut kids = p.children.to_vec();
|
||||
let pos = kids.iter().position(|&c| c == last).map_or(kids.len(), |i| i + 1);
|
||||
kids.insert(pos, new);
|
||||
p.children = kids.into();
|
||||
}
|
||||
renumber(h, new, 0);
|
||||
h.added += 1;
|
||||
@@ -260,7 +264,7 @@ impl XHost {
|
||||
if live.len() <= min {
|
||||
return;
|
||||
}
|
||||
let som = h.nodes.get(gone).map(|n| n.som.clone()).unwrap_or_default();
|
||||
let som = h.nodes.get(gone).map(|n| n.som.to_string()).unwrap_or_default();
|
||||
if !h.reserve(&XfaEffect::RemoveInstance { som: String::new() }, som.len()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,14 +70,14 @@ fn value_js(h: &HNode) -> JsValue {
|
||||
/// `(idx, name)` of the child of `parent` named `name`, first instance.
|
||||
pub(crate) fn child_named(h: &XHost, parent: usize, name: &str) -> Option<usize> {
|
||||
let p = h.nodes.get(parent)?;
|
||||
p.children.iter().copied().find(|&c| h.nodes.get(c).is_some_and(|n| !n.gone && n.name == name))
|
||||
p.children.iter().copied().find(|&c| h.nodes.get(c).is_some_and(|n| !n.gone && &*n.name == name))
|
||||
}
|
||||
|
||||
/// Every live instance named `name` under `parent`, in order.
|
||||
pub(crate) fn instances(h: &XHost, parent: usize, name: &str) -> Vec<usize> {
|
||||
h.nodes
|
||||
.get(parent)
|
||||
.map(|p| p.children.iter().copied().filter(|&c| h.nodes.get(c).is_some_and(|n| !n.gone && n.name == name)).collect())
|
||||
.map(|p| p.children.iter().copied().filter(|&c| h.nodes.get(c).is_some_and(|n| !n.gone && &*n.name == name)).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -179,11 +179,11 @@ pub(crate) fn descendants_named(h: &XHost, from: usize, name: &str, depth: usize
|
||||
return out;
|
||||
}
|
||||
let Some(n) = h.nodes.get(from) else { return out };
|
||||
for &c in &n.children {
|
||||
for &c in n.children.iter() {
|
||||
if let Some(k) = h.nodes.get(c)
|
||||
&& !k.gone
|
||||
{
|
||||
if k.name == name {
|
||||
if &*k.name == name {
|
||||
out.push(c);
|
||||
} else {
|
||||
out.extend(descendants_named(h, c, name, depth + 1));
|
||||
@@ -211,7 +211,7 @@ fn node_get(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValu
|
||||
Ok(match key.as_str() {
|
||||
IDX => JsValue::from(idx as f64),
|
||||
"rawValue" | "value" => host(ctx)?.borrow().nodes.get(idx).map_or(JsValue::null(), value_js),
|
||||
"formattedValue" => s(&host(ctx)?.borrow().nodes.get(idx).and_then(|n| n.formatted.clone()).unwrap_or(value)),
|
||||
"formattedValue" => s(&host(ctx)?.borrow().nodes.get(idx).and_then(|n| n.formatted.as_deref().map(str::to_string)).unwrap_or(value)),
|
||||
"name" => s(&name),
|
||||
"somExpression" => s(&som),
|
||||
"className" => s(kind.class_name()),
|
||||
@@ -289,7 +289,7 @@ fn node_set(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValu
|
||||
let value = if value.is_null() || value.is_undefined() { String::new() } else { text(&value, ctx)? };
|
||||
let mut h = host(ctx)?.borrow_mut();
|
||||
let Some(n) = h.nodes.get(idx) else { return Ok(JsValue::from(true)) };
|
||||
let som = n.som.clone();
|
||||
let som = n.som.to_string();
|
||||
match key.as_str() {
|
||||
"rawValue" | "value" | "formattedValue" => {
|
||||
// Not a field: the property doesn't exist, as in Acrobat, and nothing is recorded.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::globals::install;
|
||||
use super::host::{Shared, XHost};
|
||||
use super::model::{XfaDoc, XfaEvent, XfaNode, XfaOutcome, clip_result};
|
||||
@@ -15,7 +17,7 @@ pub fn run_xfa(script: &str, event: &XfaEvent, doc: &XfaDoc, root: &XfaNode, lim
|
||||
let spawned = std::thread::Builder::new()
|
||||
.name("pdfcraft-xfa-js".into())
|
||||
.stack_size(crate::SCRIPT_STACK)
|
||||
.spawn_scoped(scope, || run_here(script, event, doc, root.clone(), limits, &|| {}));
|
||||
.spawn_scoped(scope, || run_here(script, event, doc, Arc::new(root.clone()), false, limits, &|| {}));
|
||||
match spawned {
|
||||
Ok(thread) => thread.join().unwrap_or_else(|_| failed("the script stopped with an internal error".into())),
|
||||
Err(e) => failed(format!("the script engine could not start: {e}")),
|
||||
@@ -23,7 +25,7 @@ pub fn run_xfa(script: &str, event: &XfaEvent, doc: &XfaDoc, root: &XfaNode, lim
|
||||
})
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
run_here(script, event, doc, root.clone(), limits, &|| {})
|
||||
run_here(script, event, doc, Arc::new(root.clone()), false, limits, &|| {})
|
||||
}
|
||||
|
||||
/// [`run_xfa`] with a time limit: a script still running `timeout` after the engine is ready
|
||||
@@ -36,7 +38,7 @@ pub fn run_xfa_within(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNod
|
||||
return XfaOutcome { error: Some(why), ..Default::default() };
|
||||
}
|
||||
let (script, event, doc) = (script.to_string(), event.clone(), doc.clone());
|
||||
run_within("pdfcraft-xfa-js", timeout, move |started| run_here(&script, &event, &doc, root, limits, started))
|
||||
run_within("pdfcraft-xfa-js", timeout, move |started| run_here(&script, &event, &doc, Arc::new(root), false, limits, started))
|
||||
}
|
||||
|
||||
/// Longest the script engine may take to be ready for a script (its thread and context, and
|
||||
@@ -125,7 +127,15 @@ pub(crate) fn warm_up() {
|
||||
}
|
||||
|
||||
/// `started` is called once the engine is ready: a time limit on the script runs from then.
|
||||
pub(crate) fn run_here(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNode, limits: Limits, started: &dyn Fn()) -> XfaOutcome {
|
||||
pub(crate) fn run_here(
|
||||
script: &str,
|
||||
event: &XfaEvent,
|
||||
doc: &XfaDoc,
|
||||
root: Arc<XfaNode>,
|
||||
reuse: bool,
|
||||
limits: Limits,
|
||||
started: &dyn Fn(),
|
||||
) -> XfaOutcome {
|
||||
warm_up();
|
||||
let mut ctx = match Context::builder().host_hooks(std::rc::Rc::new(crate::CheckedCodeFromStrings)).build() {
|
||||
Ok(ctx) => ctx,
|
||||
@@ -133,7 +143,7 @@ pub(crate) fn run_here(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNo
|
||||
};
|
||||
ctx.runtime_limits_mut().set_loop_iteration_limit(limits.loop_iterations);
|
||||
ctx.runtime_limits_mut().set_recursion_limit(limits.recursion);
|
||||
let host_state = XHost::new(root, &event.target, doc);
|
||||
let host_state = XHost::new(root, reuse, &event.target, doc);
|
||||
let current = host_state.current;
|
||||
ctx.insert_data(RefCell::new(host_state));
|
||||
let mut out = XfaOutcome::default();
|
||||
|
||||
@@ -36,8 +36,10 @@ pub struct Script {
|
||||
pub source: String,
|
||||
pub formcalc: bool,
|
||||
pub event: XfaEvent,
|
||||
/// The form as the caller holds it now.
|
||||
pub root: XfaNode,
|
||||
/// The form as the caller holds it now. Send the same `Arc` while the form is unchanged:
|
||||
/// the next script then starts from the copy the last one was given, without flattening the
|
||||
/// form again.
|
||||
pub root: std::sync::Arc<XfaNode>,
|
||||
pub limits: Limits,
|
||||
pub timeout: std::time::Duration,
|
||||
}
|
||||
@@ -58,13 +60,13 @@ fn run_script(s: Script, doc: &XfaDoc, started: &dyn Fn()) -> XfaOutcome {
|
||||
if s.formcalc {
|
||||
// FormCalc has no engine to start: its time (and its own deadline) begins at once.
|
||||
started();
|
||||
return crate::formcalc::run_formcalc_at(&s.source, &s.event, doc, s.root, s.limits, Some(s.timeout));
|
||||
return crate::formcalc::run_formcalc_at(&s.source, &s.event, doc, s.root, true, s.limits, Some(s.timeout));
|
||||
}
|
||||
if let Some(why) = crate::refuse(&s.source) {
|
||||
started();
|
||||
return XfaOutcome { error: Some(why), ..Default::default() };
|
||||
}
|
||||
crate::xfa::run_here(&s.source, &s.event, doc, s.root, s.limits, started)
|
||||
crate::xfa::run_here(&s.source, &s.event, doc, s.root, true, s.limits, started)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -236,6 +238,14 @@ mod native {
|
||||
run_script(script, &self.doc, &|| {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for XfaSession {
|
||||
/// The scripts ran on this thread: the form they started from is let go of with the
|
||||
/// event (on other targets it goes with the session's thread).
|
||||
fn drop(&mut self) {
|
||||
crate::xfa::forget_base();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use native::XfaSession;
|
||||
Reference in new issue
Block a user