mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-28 03:58:45 -06:00
refactor(js): split xfa.rs into xfa/
New modules: model, host, nodes, instances, globals, run.
This commit is contained in:
+17
-1636
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,362 @@
|
||||
use super::host::host;
|
||||
use super::instances::instance_manager;
|
||||
use super::model::{MAX_DEPTH, XfaDoc, XfaEffect, XfaEvent, clip, reset_names};
|
||||
use super::nodes::{node_index, node_object, noop, resolve_som, resolver, sink};
|
||||
use super::*;
|
||||
|
||||
// ── xfa.host, xfa.layout, xfa.event, app, console ───────────────────────────────────────────
|
||||
|
||||
/// Records effect `e` on the host, as [`XHost::push`] does.
|
||||
fn effect(ctx: &mut Context, e: XfaEffect) -> JsResult<()> {
|
||||
host(ctx)?.borrow_mut().push(e);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `xfa.host.messageBox(message)`: records the message, cut to [`MAX_LINE`] characters, and
|
||||
/// returns 1.
|
||||
fn message_box(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let m = clip(text(&arg(args, 0), ctx)?);
|
||||
effect(ctx, XfaEffect::MessageBox(m))?;
|
||||
Ok(JsValue::from(1.0))
|
||||
}
|
||||
|
||||
/// `xfa.host.resetData(names)`: asks to reset the comma-separated fields, or every field when
|
||||
/// the list is empty.
|
||||
fn reset_data(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let list = text(&arg(args, 0), ctx)?;
|
||||
effect(ctx, XfaEffect::ResetData(reset_names(&list)))?;
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
/// `xfa.host.print(…)`: asks to print; its arguments are ignored.
|
||||
fn print(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
effect(ctx, XfaEffect::Print)?;
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
/// `xfa.host.beep()`: asks to beep.
|
||||
fn beep(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
effect(ctx, XfaEffect::Beep)?;
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
/// `xfa.host.gotoURL(url)`: asks to open the URL.
|
||||
fn goto_url(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let u = text(&arg(args, 0), ctx)?;
|
||||
effect(ctx, XfaEffect::LaunchUrl(u))?;
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
/// `xfa.host.setFocus(target)`: asks to focus the node, given as a node object or as a SOM
|
||||
/// expression resolved from the current node; an expression that names no node is passed on
|
||||
/// as written.
|
||||
fn set_focus(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let target = arg(args, 0);
|
||||
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()
|
||||
}
|
||||
_ => {
|
||||
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)
|
||||
}
|
||||
};
|
||||
effect(ctx, XfaEffect::SetFocus(som))?;
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
/// `app.execMenuItem(name)`: the effect for the request the shared menu table gives the name
|
||||
/// (Save opens Save As too); an unknown name does nothing.
|
||||
fn exec_menu_item(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let item = text(&arg(args, 0), ctx)?;
|
||||
match crate::menu_item(&item) {
|
||||
Some(crate::Request::SaveAs) => effect(ctx, XfaEffect::SaveAs)?,
|
||||
Some(crate::Request::Print) => effect(ctx, XfaEffect::Print)?,
|
||||
Some(crate::Request::ShowPanel(p)) => effect(ctx, XfaEffect::ShowPanel(p))?,
|
||||
_ => {}
|
||||
}
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
pub(super) fn recalculate(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
effect(ctx, XfaEffect::Recalculate)?;
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
pub(super) fn relayout(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
effect(ctx, XfaEffect::Relayout)?;
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
fn console_println(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let m = clip(text(&arg(args, 0), ctx)?);
|
||||
host(ctx)?.borrow_mut().print(m);
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
fn response(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
|
||||
Ok(JsValue::null())
|
||||
}
|
||||
|
||||
fn layout_page(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
Ok(JsValue::from(host(ctx)?.borrow().doc.page as f64 + 1.0))
|
||||
}
|
||||
|
||||
fn layout_page0(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
Ok(JsValue::from(host(ctx)?.borrow().doc.page as f64))
|
||||
}
|
||||
|
||||
fn layout_page_count(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
Ok(JsValue::from(host(ctx)?.borrow().doc.page_count as f64))
|
||||
}
|
||||
|
||||
fn zero(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
|
||||
Ok(JsValue::from(0.0))
|
||||
}
|
||||
|
||||
fn xfa_resolve(ctx: &mut Context, many: bool) -> JsResult<JsValue> {
|
||||
let current = host(ctx)?.borrow().current;
|
||||
resolver(ctx, current, many)
|
||||
}
|
||||
|
||||
/// Defines the globals for one script on node `current`: `xfa` (`form`, `host`, `layout`, `event`,
|
||||
/// `resolveNode` and more), `event` as the same object as `xfa.event`, `app`, `console`, `util`,
|
||||
/// and the nodes in scope by name, with `_name` for each repeatable one's instance manager.
|
||||
pub(super) fn install(ctx: &mut Context, event: &XfaEvent, doc: &XfaDoc, current: usize) -> JsResult<()> {
|
||||
let rw = Attribute::CONFIGURABLE;
|
||||
let form = node_object(ctx, 0)?;
|
||||
let this_node = node_object(ctx, current)?;
|
||||
let host_obj = ObjectInitializer::new(ctx)
|
||||
.function(NativeFunction::from_fn_ptr(message_box), js_string!("messageBox"), 4)
|
||||
.function(NativeFunction::from_fn_ptr(reset_data), js_string!("resetData"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(print), js_string!("print"), 8)
|
||||
.function(NativeFunction::from_fn_ptr(beep), js_string!("beep"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(goto_url), js_string!("gotoURL"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(set_focus), js_string!("setFocus"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(response), js_string!("response"), 4)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("openList"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("exportData"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("importData"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("pageDown"), 0)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("pageUp"), 0)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("getFocus"), 0)
|
||||
.property(js_string!("currentPage"), JsValue::from(doc.page as f64), rw)
|
||||
.property(js_string!("numPages"), JsValue::from(doc.page_count as f64), rw)
|
||||
.property(js_string!("name"), s("Acrobat"), Attribute::READONLY)
|
||||
.property(js_string!("appType"), s("Exchange-Pro"), Attribute::READONLY)
|
||||
.property(js_string!("version"), s("11"), Attribute::READONLY)
|
||||
.property(js_string!("variation"), s("Full"), Attribute::READONLY)
|
||||
.property(js_string!("language"), s("en"), Attribute::READONLY)
|
||||
.property(js_string!("platform"), s(platform()), Attribute::READONLY)
|
||||
.property(js_string!("calculationsEnabled"), JsValue::from(true), rw)
|
||||
.property(js_string!("validationsEnabled"), JsValue::from(true), rw)
|
||||
.property(js_string!("title"), s(&doc.file_name), rw)
|
||||
.build();
|
||||
let layout = ObjectInitializer::new(ctx)
|
||||
.function(NativeFunction::from_fn_ptr(layout_page), js_string!("page"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(layout_page0), js_string!("absPage"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(layout_page_count), js_string!("pageCount"), 0)
|
||||
.function(NativeFunction::from_fn_ptr(layout_page_count), js_string!("absPageCount"), 0)
|
||||
.function(NativeFunction::from_fn_ptr(relayout), js_string!("relayout"), 0)
|
||||
.function(NativeFunction::from_fn_ptr(relayout), js_string!("relayoutPageArea"), 0)
|
||||
.function(NativeFunction::from_fn_ptr(zero), js_string!("h"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(zero), js_string!("w"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(zero), js_string!("x"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(zero), js_string!("y"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(zero), js_string!("pageSpan"), 1)
|
||||
.property(js_string!("ready"), JsValue::from(true), Attribute::READONLY)
|
||||
.build();
|
||||
let ev = ObjectInitializer::new(ctx)
|
||||
.property(js_string!("name"), s(&event.activity), Attribute::all())
|
||||
.property(js_string!("newText"), s(&event.new_text), Attribute::all())
|
||||
.property(js_string!("prevText"), s(&event.prev_text), Attribute::all())
|
||||
.property(js_string!("fullText"), s(&event.new_text), Attribute::all())
|
||||
.property(js_string!("change"), s(""), Attribute::all())
|
||||
.property(js_string!("target"), this_node.clone(), Attribute::all())
|
||||
.property(js_string!("selStart"), JsValue::from(0.0), Attribute::all())
|
||||
.property(js_string!("selEnd"), JsValue::from(0.0), Attribute::all())
|
||||
.property(js_string!("cancelAction"), JsValue::from(false), Attribute::all())
|
||||
.property(js_string!("reenter"), JsValue::from(false), Attribute::all())
|
||||
.property(js_string!("shift"), JsValue::from(false), Attribute::all())
|
||||
.property(js_string!("modifier"), JsValue::from(false), Attribute::all())
|
||||
.property(js_string!("keyDown"), JsValue::from(false), Attribute::all())
|
||||
.property(js_string!("commitKey"), JsValue::from(0.0), Attribute::all())
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("emit"), 0)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("reset"), 0)
|
||||
.build();
|
||||
let datasets = ObjectInitializer::new(ctx).property(js_string!("data"), form.clone(), rw).build();
|
||||
let template = sink(ctx)?;
|
||||
let resolve_one = xfa_resolve(ctx, false)?;
|
||||
let resolve_many = xfa_resolve(ctx, true)?;
|
||||
let xfa = ObjectInitializer::new(ctx)
|
||||
.property(js_string!("form"), form.clone(), rw)
|
||||
.property(js_string!("host"), host_obj, rw)
|
||||
.property(js_string!("layout"), layout, rw)
|
||||
.property(js_string!("event"), ev.clone(), rw)
|
||||
.property(js_string!("datasets"), datasets, rw)
|
||||
.property(js_string!("record"), form.clone(), rw)
|
||||
.property(js_string!("template"), template, rw)
|
||||
.property(js_string!("resolveNode"), resolve_one, rw)
|
||||
.property(js_string!("resolveNodes"), resolve_many, rw)
|
||||
.function(NativeFunction::from_fn_ptr(recalculate), js_string!("recalculate"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(relayout), js_string!("remerge"), 0)
|
||||
.build();
|
||||
ctx.register_global_property(js_string!("xfa"), xfa, rw)?;
|
||||
ctx.register_global_property(js_string!("event"), ev, rw)?;
|
||||
ctx.register_global_property(js_string!("__xfa_this"), this_node, rw)?;
|
||||
let app = ObjectInitializer::new(ctx)
|
||||
.function(NativeFunction::from_fn_ptr(message_box), js_string!("alert"), 4)
|
||||
.function(NativeFunction::from_fn_ptr(beep), js_string!("beep"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(response), js_string!("response"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(goto_url), js_string!("launchURL"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(exec_menu_item), js_string!("execMenuItem"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("setTimeOut"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("setInterval"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("clearTimeOut"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("clearInterval"), 1)
|
||||
.property(js_string!("viewerType"), s("Exchange-Pro"), Attribute::READONLY)
|
||||
.property(js_string!("viewerVariation"), s("Full"), Attribute::READONLY)
|
||||
.property(js_string!("viewerVersion"), JsValue::from(24.0), Attribute::READONLY)
|
||||
.property(js_string!("platform"), s(platform()), Attribute::READONLY)
|
||||
.property(js_string!("language"), s("ENU"), Attribute::READONLY)
|
||||
.build();
|
||||
ctx.register_global_property(js_string!("app"), app, rw)?;
|
||||
let console = ObjectInitializer::new(ctx)
|
||||
.function(NativeFunction::from_fn_ptr(console_println), js_string!("println"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("show"), 0)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("hide"), 0)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("clear"), 0)
|
||||
.build();
|
||||
ctx.register_global_property(js_string!("console"), console, rw)?;
|
||||
let util = ObjectInitializer::new(ctx)
|
||||
.function(NativeFunction::from_fn_ptr(printf), js_string!("printf"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(printd), js_string!("printd"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(printx), js_string!("printx"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(scand), js_string!("scand"), 2)
|
||||
.build();
|
||||
ctx.register_global_property(js_string!("util"), util, rw)?;
|
||||
// Unqualified names: the current node's children, its siblings, then each ancestor's
|
||||
// children, the nearest first. Instance managers as `_name`.
|
||||
let scopes: Vec<usize> = {
|
||||
let h = host(ctx)?.borrow();
|
||||
let mut out = vec![current];
|
||||
let mut at = h.nodes.get(current).and_then(|n| n.parent);
|
||||
let mut guard = 0;
|
||||
while let Some(p) = at
|
||||
&& guard < MAX_DEPTH
|
||||
{
|
||||
out.push(p);
|
||||
at = h.nodes.get(p).and_then(|n| n.parent);
|
||||
guard += 1;
|
||||
}
|
||||
out
|
||||
};
|
||||
// Names that stay what JavaScript (or this host) means by them: a field called `Date`
|
||||
// is reached as `this.parent.Date` or by resolveNode.
|
||||
const RESERVED: &[&str] = &[
|
||||
"xfa",
|
||||
"event",
|
||||
"app",
|
||||
"console",
|
||||
"util",
|
||||
"this",
|
||||
"__xfa_this",
|
||||
"__xfa_src",
|
||||
"eval",
|
||||
"arguments",
|
||||
"globalThis",
|
||||
"window",
|
||||
"undefined",
|
||||
"NaN",
|
||||
"Infinity",
|
||||
"Object",
|
||||
"Function",
|
||||
"Array",
|
||||
"Number",
|
||||
"String",
|
||||
"Boolean",
|
||||
"Symbol",
|
||||
"Date",
|
||||
"RegExp",
|
||||
"Error",
|
||||
"TypeError",
|
||||
"RangeError",
|
||||
"SyntaxError",
|
||||
"Math",
|
||||
"JSON",
|
||||
"Map",
|
||||
"Set",
|
||||
"WeakMap",
|
||||
"WeakSet",
|
||||
"Promise",
|
||||
"Proxy",
|
||||
"Reflect",
|
||||
"parseInt",
|
||||
"parseFloat",
|
||||
"isNaN",
|
||||
"isFinite",
|
||||
"encodeURIComponent",
|
||||
"decodeURIComponent",
|
||||
"encodeURI",
|
||||
"decodeURI",
|
||||
"escape",
|
||||
"unescape",
|
||||
"BigInt",
|
||||
"ArrayBuffer",
|
||||
"DataView",
|
||||
"Intl",
|
||||
];
|
||||
let mut defined: Vec<String> = RESERVED.iter().map(ToString::to_string).collect();
|
||||
for scope in scopes {
|
||||
let kids: Vec<(usize, String, bool)> = {
|
||||
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())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
for (c, name, repeatable) in kids {
|
||||
if !is_identifier(&name) || defined.contains(&name) {
|
||||
continue;
|
||||
}
|
||||
let obj = node_object(ctx, c)?;
|
||||
ctx.register_global_property(JsString::from(name.as_str()), obj, rw)?;
|
||||
if repeatable {
|
||||
let im = instance_manager(ctx, scope, &name)?;
|
||||
ctx.register_global_property(JsString::from(format!("_{name}").as_str()), im, rw)?;
|
||||
}
|
||||
defined.push(name);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_identifier(name: &str) -> bool {
|
||||
let mut chars = name.chars();
|
||||
match chars.next() {
|
||||
Some(c) if c.is_ascii_alphabetic() || c == '_' || c == '$' => {}
|
||||
_ => return false,
|
||||
}
|
||||
chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
|
||||
&& !matches!(
|
||||
name,
|
||||
"var"
|
||||
| "let"
|
||||
| "const"
|
||||
| "function"
|
||||
| "return"
|
||||
| "if"
|
||||
| "else"
|
||||
| "for"
|
||||
| "while"
|
||||
| "new"
|
||||
| "null"
|
||||
| "true"
|
||||
| "false"
|
||||
| "undefined"
|
||||
| "typeof"
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
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 super::*;
|
||||
|
||||
/// One node of the flattened form, shared by the JavaScript and FormCalc engines.
|
||||
pub(crate) struct HNode {
|
||||
pub(crate) name: String,
|
||||
pub(crate) som: String,
|
||||
pub(crate) kind: XfaKind,
|
||||
pub(crate) value: String,
|
||||
pub(crate) numeric: bool,
|
||||
pub(crate) presence: String,
|
||||
pub(crate) access: String,
|
||||
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>,
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
/// The form as a script sees and changes it, with the effects it records.
|
||||
pub(crate) struct XHost {
|
||||
pub(crate) nodes: Vec<HNode>,
|
||||
pub(crate) effects: Vec<XfaEffect>,
|
||||
pub(crate) console: Vec<String>,
|
||||
pub(crate) doc: XfaDoc,
|
||||
pub(super) added: usize,
|
||||
/// Index of `this`.
|
||||
pub(crate) current: usize,
|
||||
/// Where the effect setting a property of an object is in `effects`, by (property, SOM):
|
||||
/// setting it again replaces that effect (the last value wins) unless something that
|
||||
/// depends on order (rows added or removed, a reset) came after it.
|
||||
set_at: HashMap<(u8, String), usize>,
|
||||
/// Effects before this index are behind an order-dependent effect.
|
||||
barrier: usize,
|
||||
alerts: usize,
|
||||
dropped_effects: usize,
|
||||
clipped_values: usize,
|
||||
dropped_alerts: usize,
|
||||
dropped_console: usize,
|
||||
/// Bytes of text kept in effects, console lines and values written, against [`MAX_PAYLOAD`].
|
||||
kept: usize,
|
||||
dropped_bytes: usize,
|
||||
}
|
||||
|
||||
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)));
|
||||
XHost {
|
||||
nodes,
|
||||
effects: Vec::new(),
|
||||
console: Vec::new(),
|
||||
doc: doc.clone(),
|
||||
added: 0,
|
||||
current,
|
||||
set_at: HashMap::new(),
|
||||
barrier: 0,
|
||||
alerts: 0,
|
||||
dropped_effects: 0,
|
||||
clipped_values: 0,
|
||||
dropped_alerts: 0,
|
||||
dropped_console: 0,
|
||||
kept: 0,
|
||||
dropped_bytes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Room for `bytes` more bytes of kept text: charged when there is, a drop counted when not.
|
||||
fn room(&mut self, bytes: usize) -> bool {
|
||||
match self.kept.checked_add(bytes).filter(|k| *k <= MAX_PAYLOAD) {
|
||||
Some(k) => {
|
||||
self.kept = k;
|
||||
true
|
||||
}
|
||||
None => {
|
||||
self.dropped_bytes = self.dropped_bytes.saturating_add(1);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Record an effect: values, presence and access set again on the same object replace the
|
||||
/// earlier one; past [`MAX_EFFECTS`] (or [`MAX_ALERTS`] message boxes, or [`MAX_PAYLOAD`]
|
||||
/// bytes of text) effects are counted and dropped.
|
||||
pub(crate) fn push(&mut self, e: XfaEffect) {
|
||||
let e = self.clip_effect(e);
|
||||
if self.room(payload(&e)) {
|
||||
self.record(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Room for `e`, which holds `bytes` of text, charged now: a change to the snapshot is made
|
||||
/// only when the effect that applies it will be kept, so a script never sees a change its
|
||||
/// effects leave out. An effect that replaces an earlier one needs no new slot. A drop is
|
||||
/// counted when there is no room.
|
||||
pub(super) fn reserve(&mut self, e: &XfaEffect, bytes: usize) -> bool {
|
||||
let replaces = effect_key(e).and_then(|k| self.set_at.get(&k)).is_some_and(|&at| at >= self.barrier && at < self.effects.len());
|
||||
if !replaces && self.effects.len() >= MAX_EFFECTS {
|
||||
self.dropped_effects += 1;
|
||||
return false;
|
||||
}
|
||||
self.room(bytes)
|
||||
}
|
||||
|
||||
/// Set the presence or access of node `i` to what `e` (a [`XfaEffect::SetPresence`] or
|
||||
/// [`XfaEffect::SetAccess`]) says, in the snapshot only when the effect is kept.
|
||||
pub(crate) fn set_state(&mut self, i: usize, e: XfaEffect) {
|
||||
if !self.reserve(&e, payload(&e)) {
|
||||
return;
|
||||
}
|
||||
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(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.record(e);
|
||||
}
|
||||
|
||||
/// `e` with its text cut to the lengths an effect keeps.
|
||||
fn clip_effect(&mut self, e: XfaEffect) -> XfaEffect {
|
||||
match e {
|
||||
XfaEffect::MessageBox(m) => XfaEffect::MessageBox(clip(m)),
|
||||
XfaEffect::LaunchUrl(u) => XfaEffect::LaunchUrl(clip(u)),
|
||||
XfaEffect::SetFocus(s) => XfaEffect::SetFocus(clip(s)),
|
||||
XfaEffect::ResetData(names) => XfaEffect::ResetData(names.into_iter().map(clip).collect()),
|
||||
XfaEffect::SetValue { som, value } => {
|
||||
let (value, cut) = clip_value(value);
|
||||
if cut {
|
||||
self.clipped_values += 1;
|
||||
}
|
||||
XfaEffect::SetValue { som, value }
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep `e`, whose bytes are already charged, against the count limits.
|
||||
pub(super) fn record(&mut self, e: XfaEffect) {
|
||||
if let Some(key) = effect_key(&e) {
|
||||
if let Some(&at) = self.set_at.get(&key)
|
||||
&& at >= self.barrier
|
||||
&& let Some(slot) = self.effects.get_mut(at)
|
||||
{
|
||||
*slot = e;
|
||||
return;
|
||||
}
|
||||
if self.effects.len() >= MAX_EFFECTS {
|
||||
self.dropped_effects += 1;
|
||||
return;
|
||||
}
|
||||
self.set_at.insert(key, self.effects.len());
|
||||
self.effects.push(e);
|
||||
return;
|
||||
}
|
||||
if matches!(e, XfaEffect::MessageBox(_)) {
|
||||
if self.alerts >= MAX_ALERTS {
|
||||
self.dropped_alerts += 1;
|
||||
return;
|
||||
}
|
||||
self.alerts += 1;
|
||||
}
|
||||
if self.effects.len() >= MAX_EFFECTS {
|
||||
self.dropped_effects += 1;
|
||||
return;
|
||||
}
|
||||
if matches!(e, XfaEffect::AddInstance { .. } | XfaEffect::RemoveInstance { .. } | XfaEffect::ResetData(_)) {
|
||||
self.barrier = self.effects.len() + 1;
|
||||
}
|
||||
self.effects.push(e);
|
||||
}
|
||||
|
||||
pub(crate) fn print(&mut self, line: String) {
|
||||
if self.console.len() >= MAX_CONSOLE {
|
||||
self.dropped_console += 1;
|
||||
} else if self.room(line.len()) {
|
||||
self.console.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
/// What the limits left out.
|
||||
pub(crate) fn notes(&self) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
if self.dropped_effects > 0 {
|
||||
out.push(format!("the script made more than {MAX_EFFECTS} changes; {} more were left out", self.dropped_effects));
|
||||
}
|
||||
if self.dropped_alerts > 0 {
|
||||
out.push(format!("the script showed more than {MAX_ALERTS} messages; {} more were left out", self.dropped_alerts));
|
||||
}
|
||||
if self.dropped_console > 0 {
|
||||
out.push(format!("the script printed more than {MAX_CONSOLE} console lines; {} more were left out", self.dropped_console));
|
||||
}
|
||||
if self.dropped_bytes > 0 {
|
||||
out.push(format!("the script's changes and output passed {} MiB of text; {} more were left out", MAX_PAYLOAD >> 20, self.dropped_bytes));
|
||||
}
|
||||
if self.clipped_values > 0 {
|
||||
out.push(format!("the script set {} value(s) longer than {MAX_VALUE} characters; they were cut", self.clipped_values));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Give node `i` the value `text` when it is a field or exclusion group (nothing else
|
||||
/// holds a value; writing one into a subform would put text where another viewer expects
|
||||
/// a data group). Returns whether it was.
|
||||
pub(crate) fn set_value(&mut self, i: usize, text: String) -> bool {
|
||||
let Some(n) = self.nodes.get(i) else { return false };
|
||||
if n.gone || !matches!(n.kind, XfaKind::Field | XfaKind::ExclGroup) {
|
||||
return false;
|
||||
}
|
||||
let som = n.som.clone();
|
||||
// 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);
|
||||
if cut {
|
||||
self.clipped_values += 1;
|
||||
}
|
||||
// The snapshot's copy and the effect's are charged together, before either is kept:
|
||||
// past the byte budget or the effect cap the write is left out of both.
|
||||
let effect = XfaEffect::SetValue { som, value: text.clone() };
|
||||
if !self.reserve(&effect, text.len().saturating_add(payload(&effect))) {
|
||||
return true;
|
||||
}
|
||||
if let Some(n) = self.nodes.get_mut(i) {
|
||||
n.value = text;
|
||||
n.formatted = None;
|
||||
}
|
||||
self.record(effect);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) type Shared = RefCell<XHost>;
|
||||
|
||||
/// The form host of the script running in `ctx`.
|
||||
pub(super) fn host(ctx: &Context) -> JsResult<&Shared> {
|
||||
ctx.get_data::<Shared>().ok_or_else(|| error("the XFA script host is not set up"))
|
||||
}
|
||||
|
||||
/// The snapshot `n` and its descendants flattened into `out` under `parent` (to [`MAX_DEPTH`]
|
||||
/// and [`MAX_NODES`]): the index of `n`'s node, `None` past the limits.
|
||||
fn flatten(n: &XfaNode, parent: Option<usize>, out: &mut Vec<HNode>, depth: usize) -> Option<usize> {
|
||||
if depth > MAX_DEPTH || out.len() >= MAX_NODES {
|
||||
return None;
|
||||
}
|
||||
let idx = out.len();
|
||||
out.push(HNode {
|
||||
name: n.name.clone(),
|
||||
som: n.som.clone(),
|
||||
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() },
|
||||
repeatable: n.repeatable,
|
||||
occur_min: n.occur_min,
|
||||
occur_max: n.occur_max,
|
||||
index: n.index,
|
||||
parent,
|
||||
children: Vec::new(),
|
||||
gone: false,
|
||||
formatted: n.formatted.clone(),
|
||||
});
|
||||
let mut kids = Vec::new();
|
||||
for c in &n.children {
|
||||
if let Some(ci) = flatten(c, Some(idx), out, depth + 1) {
|
||||
kids.push(ci);
|
||||
}
|
||||
}
|
||||
if let Some(h) = out.get_mut(idx) {
|
||||
h.children = kids;
|
||||
}
|
||||
Some(idx)
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
use super::host::{HNode, XHost, host};
|
||||
use super::model::{MAX_ADDED, MAX_DEPTH, MAX_NODES, XfaEffect};
|
||||
use super::nodes::{instances, node_object};
|
||||
use super::*;
|
||||
|
||||
pub(super) fn list_object(ctx: &mut Context, items: &[usize]) -> JsResult<JsValue> {
|
||||
let vals: Vec<JsValue> = items.iter().map(|i| JsValue::from(*i as f64)).collect();
|
||||
let arr = JsArray::from_iter(vals, ctx);
|
||||
let arr_obj: JsObject = arr.into();
|
||||
fn item(_: &JsValue, args: &[JsValue], arr: &JsObject, ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let i = arg(args, 0).to_number(ctx)?;
|
||||
if !i.is_finite() || i < 0.0 {
|
||||
return Ok(JsValue::null());
|
||||
}
|
||||
let v = JsArray::from_object(arr.clone())?.get(i as u64, ctx)?;
|
||||
if v.is_undefined() {
|
||||
return Ok(JsValue::null());
|
||||
}
|
||||
let idx = v.to_number(ctx)? as usize;
|
||||
node_object(ctx, idx)
|
||||
}
|
||||
let item_fn = function(ctx, NativeFunction::from_copy_closure_with_captures(item, arr_obj));
|
||||
Ok(ObjectInitializer::new(ctx)
|
||||
.property(js_string!("length"), JsValue::from(items.len() as f64), Attribute::READONLY)
|
||||
.property(js_string!("item"), item_fn, Attribute::all())
|
||||
.build()
|
||||
.into())
|
||||
}
|
||||
|
||||
/// The instance manager of the subform named `name` under `parent`.
|
||||
pub(super) fn instance_manager(ctx: &mut Context, parent: usize, name: &str) -> JsResult<JsValue> {
|
||||
let (count, min, max) = {
|
||||
let h = host(ctx)?.borrow();
|
||||
let live = instances(&h, parent, name);
|
||||
let first = live.first().and_then(|i| h.nodes.get(*i));
|
||||
(live.len(), first.map_or(0, |n| n.occur_min), first.and_then(|n| n.occur_max))
|
||||
};
|
||||
let key = ObjectInitializer::new(ctx)
|
||||
.property(js_string!("parent"), JsValue::from(parent as f64), Attribute::READONLY)
|
||||
.property(js_string!("name"), s(name), Attribute::READONLY)
|
||||
.build();
|
||||
fn target_of(key: &JsObject, ctx: &mut Context) -> JsResult<(usize, String)> {
|
||||
let p = key.get(js_string!("parent"), ctx)?.to_number(ctx)? as usize;
|
||||
let n = text(&key.get(js_string!("name"), ctx)?, ctx)?;
|
||||
Ok((p, n))
|
||||
}
|
||||
fn add_instance(_: &JsValue, _: &[JsValue], key: &JsObject, ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let (p, n) = target_of(key, ctx)?;
|
||||
match add_instance_of(ctx, p, &n)? {
|
||||
Some(i) => node_object(ctx, i),
|
||||
None => Ok(JsValue::null()),
|
||||
}
|
||||
}
|
||||
fn insert_instance(this: &JsValue, args: &[JsValue], key: &JsObject, ctx: &mut Context) -> JsResult<JsValue> {
|
||||
add_instance(this, args, key, ctx)
|
||||
}
|
||||
fn remove_instance(_: &JsValue, args: &[JsValue], key: &JsObject, ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let (p, n) = target_of(key, ctx)?;
|
||||
let i = arg(args, 0).to_number(ctx)?;
|
||||
if i.is_finite() && i >= 0.0 {
|
||||
remove_instance_of(ctx, p, &n, i as usize)?;
|
||||
}
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
fn set_instances(_: &JsValue, args: &[JsValue], key: &JsObject, ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let (p, n) = target_of(key, ctx)?;
|
||||
let want = arg(args, 0).to_number(ctx)?;
|
||||
if !want.is_finite() || want < 0.0 {
|
||||
return Ok(JsValue::undefined());
|
||||
}
|
||||
host(ctx)?.borrow_mut().set_instances(p, &n, want as usize);
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
fn move_instance(_: &JsValue, _: &[JsValue], _: &JsObject, _: &mut Context) -> JsResult<JsValue> {
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
fn count_get(_: &JsValue, _: &[JsValue], key: &JsObject, ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let (p, n) = target_of(key, ctx)?;
|
||||
let c = instances(&host(ctx)?.borrow(), p, &n).len();
|
||||
Ok(JsValue::from(c as f64))
|
||||
}
|
||||
fn count_set(_: &JsValue, args: &[JsValue], key: &JsObject, ctx: &mut Context) -> JsResult<JsValue> {
|
||||
set_instances(&JsValue::undefined(), args, key, ctx)
|
||||
}
|
||||
let count_g = function(ctx, NativeFunction::from_copy_closure_with_captures(count_get, key.clone()));
|
||||
let count_s = function(ctx, NativeFunction::from_copy_closure_with_captures(count_set, key.clone()));
|
||||
let add = function(ctx, NativeFunction::from_copy_closure_with_captures(add_instance, key.clone()));
|
||||
let insert = function(ctx, NativeFunction::from_copy_closure_with_captures(insert_instance, key.clone()));
|
||||
let remove = function(ctx, NativeFunction::from_copy_closure_with_captures(remove_instance, key.clone()));
|
||||
let set = function(ctx, NativeFunction::from_copy_closure_with_captures(set_instances, key.clone()));
|
||||
let mv = function(ctx, NativeFunction::from_copy_closure_with_captures(move_instance, key));
|
||||
let _ = count;
|
||||
let o = ObjectInitializer::new(ctx)
|
||||
.property(js_string!("min"), JsValue::from(min as f64), Attribute::READONLY)
|
||||
.property(js_string!("max"), max.map_or(JsValue::from(-1.0), |m| JsValue::from(m as f64)), Attribute::READONLY)
|
||||
.property(js_string!("addInstance"), add, Attribute::all())
|
||||
.property(js_string!("insertInstance"), insert, Attribute::all())
|
||||
.property(js_string!("removeInstance"), remove, Attribute::all())
|
||||
.property(js_string!("setInstances"), set, Attribute::all())
|
||||
.property(js_string!("moveInstance"), mv, Attribute::all())
|
||||
.property(js_string!("className"), s("instanceManager"), Attribute::READONLY)
|
||||
.build();
|
||||
use boa_engine::property::PropertyDescriptor;
|
||||
o.define_property_or_throw(js_string!("count"), PropertyDescriptor::builder().get(count_g).set(count_s).configurable(true), ctx)?;
|
||||
Ok(o.into())
|
||||
}
|
||||
|
||||
/// Re-derive the SOM of `idx` and everything under it from its parent and index.
|
||||
fn renumber(h: &mut XHost, idx: usize, depth: usize) {
|
||||
if depth > MAX_DEPTH {
|
||||
return;
|
||||
}
|
||||
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();
|
||||
(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;
|
||||
}
|
||||
for k in kids {
|
||||
renumber(h, k, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// The SOM of instance `index` of the node `name` under a parent at `parent_som`.
|
||||
fn child_som(parent_som: String, name: &str, index: usize) -> String {
|
||||
if name.starts_with('#') || name.is_empty() {
|
||||
parent_som
|
||||
} else if parent_som.is_empty() {
|
||||
format!("{name}[{index}]")
|
||||
} else {
|
||||
format!("{parent_som}.{name}[{index}]")
|
||||
}
|
||||
}
|
||||
|
||||
/// A copy of `src`'s subtree (values cleared) under `parent`, returning the copy's index.
|
||||
fn clone_subtree(h: &mut XHost, src: usize, parent: Option<usize>, depth: usize) -> Option<usize> {
|
||||
if depth > MAX_DEPTH || h.nodes.len() >= MAX_NODES {
|
||||
return None;
|
||||
}
|
||||
let (template, kids) = {
|
||||
let n = h.nodes.get(src)?;
|
||||
(
|
||||
HNode {
|
||||
name: n.name.clone(),
|
||||
som: String::new(),
|
||||
kind: n.kind,
|
||||
value: String::new(),
|
||||
numeric: n.numeric,
|
||||
presence: n.presence.clone(),
|
||||
access: n.access.clone(),
|
||||
repeatable: n.repeatable,
|
||||
occur_min: n.occur_min,
|
||||
occur_max: n.occur_max,
|
||||
index: n.index,
|
||||
parent,
|
||||
children: Vec::new(),
|
||||
gone: false,
|
||||
// The copy holds no value, so nothing is shown in its picture either.
|
||||
formatted: None,
|
||||
},
|
||||
n.children.clone(),
|
||||
)
|
||||
};
|
||||
let idx = h.nodes.len();
|
||||
h.nodes.push(template);
|
||||
let mut copied = Vec::new();
|
||||
for k in kids {
|
||||
// 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 {
|
||||
continue;
|
||||
}
|
||||
if let Some(ci) = clone_subtree(h, k, Some(idx), depth + 1) {
|
||||
copied.push(ci);
|
||||
}
|
||||
}
|
||||
if let Some(n) = h.nodes.get_mut(idx) {
|
||||
n.children = copied;
|
||||
}
|
||||
Some(idx)
|
||||
}
|
||||
|
||||
fn add_instance_of(ctx: &mut Context, parent: usize, name: &str) -> JsResult<Option<usize>> {
|
||||
let mut h = host(ctx)?.borrow_mut();
|
||||
Ok(h.add_instance(parent, name))
|
||||
}
|
||||
|
||||
fn remove_instance_of(ctx: &mut Context, parent: usize, name: &str, i: usize) -> JsResult<()> {
|
||||
let mut h = host(ctx)?.borrow_mut();
|
||||
h.remove_instance(parent, name, i);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl XHost {
|
||||
/// A new instance of the subform `name` under `parent`, after the last one; `None` when the
|
||||
/// subform doesn't repeat, is at its maximum, or the run added its limit.
|
||||
pub(crate) fn add_instance(&mut self, parent: usize, name: &str) -> Option<usize> {
|
||||
let h = self;
|
||||
let live = instances(h, parent, name);
|
||||
let &first = live.first()?;
|
||||
let (repeatable, max) = h.nodes.get(first).map(|n| (n.repeatable, n.occur_max)).unwrap_or((false, Some(1)));
|
||||
if !repeatable || max.is_some_and(|m| live.len() >= m) || h.added >= MAX_ADDED || h.nodes.len() >= MAX_NODES {
|
||||
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 som = child_som(parent_som, &name, live.len());
|
||||
if !h.reserve(&XfaEffect::AddInstance { som: String::new() }, som.len()) {
|
||||
return None;
|
||||
}
|
||||
let new = clone_subtree(h, first, Some(parent), 0)?;
|
||||
let last = live.last().copied().unwrap_or(first);
|
||||
if let Some(n) = h.nodes.get_mut(new) {
|
||||
n.index = live.len();
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
renumber(h, new, 0);
|
||||
h.added += 1;
|
||||
h.record(XfaEffect::AddInstance { som });
|
||||
Some(new)
|
||||
}
|
||||
|
||||
/// Add or remove instances of the subform `name` under `parent` until there are `want`.
|
||||
/// Bounded: adding stops at the cap or the maximum, removing at the floor, and a step that
|
||||
/// changes nothing ends the loop.
|
||||
pub(crate) fn set_instances(&mut self, parent: usize, name: &str, want: usize) {
|
||||
let want = want.min(MAX_ADDED);
|
||||
for _ in 0..=MAX_ADDED {
|
||||
let have = instances(self, parent, name).len();
|
||||
if have < want {
|
||||
if self.add_instance(parent, name).is_none() {
|
||||
break;
|
||||
}
|
||||
} else if have > want && have > 0 {
|
||||
self.remove_instance(parent, name, have - 1);
|
||||
if instances(self, parent, name).len() == have {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove instance `i` of the subform `name` under `parent` (never below the minimum, or
|
||||
/// below one).
|
||||
pub(crate) fn remove_instance(&mut self, parent: usize, name: &str, i: usize) {
|
||||
let h = self;
|
||||
let live = instances(h, parent, name);
|
||||
let Some(&gone) = live.get(i) else { return };
|
||||
let min = h.nodes.get(gone).map_or(0, |n| n.occur_min).max(1);
|
||||
if live.len() <= min {
|
||||
return;
|
||||
}
|
||||
let som = h.nodes.get(gone).map(|n| n.som.clone()).unwrap_or_default();
|
||||
if !h.reserve(&XfaEffect::RemoveInstance { som: String::new() }, som.len()) {
|
||||
return;
|
||||
}
|
||||
if let Some(n) = h.nodes.get_mut(gone) {
|
||||
n.gone = true;
|
||||
}
|
||||
// Later instances move down one.
|
||||
for (k, &inst) in live.iter().enumerate().skip(i + 1) {
|
||||
if let Some(n) = h.nodes.get_mut(inst) {
|
||||
n.index = k - 1;
|
||||
}
|
||||
renumber(h, inst, 0);
|
||||
}
|
||||
h.record(XfaEffect::RemoveInstance { som });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
use super::*;
|
||||
|
||||
/// What kind of template object a node is.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum XfaKind {
|
||||
/// `xfa.form` itself.
|
||||
Form,
|
||||
Subform,
|
||||
Field,
|
||||
ExclGroup,
|
||||
Draw,
|
||||
Area,
|
||||
}
|
||||
|
||||
impl XfaKind {
|
||||
pub(crate) fn class_name(self) -> &'static str {
|
||||
match self {
|
||||
XfaKind::Form => "form",
|
||||
XfaKind::Subform => "subform",
|
||||
XfaKind::Field => "field",
|
||||
XfaKind::ExclGroup => "exclGroup",
|
||||
XfaKind::Draw => "draw",
|
||||
XfaKind::Area => "area",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One object of the form, as the caller snapshots it.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct XfaNode {
|
||||
pub name: String,
|
||||
/// The SOM expression naming this instance (`form[0].head[0].familyName[0]`).
|
||||
pub som: String,
|
||||
pub kind: Option<XfaKind>,
|
||||
/// Fields: the raw value (ISO dates, on values for buttons); empty when null.
|
||||
pub value: String,
|
||||
/// Numeric fields give numbers to scripts.
|
||||
pub numeric: bool,
|
||||
/// `visible`, `invisible`, `hidden` or `inactive`.
|
||||
pub presence: String,
|
||||
/// `open`, `readOnly`, `protected` or `nonInteractive`.
|
||||
pub access: String,
|
||||
/// Subforms: may have several instances (`occur max` other than 1).
|
||||
pub repeatable: bool,
|
||||
pub occur_min: usize,
|
||||
pub occur_max: Option<usize>,
|
||||
/// This instance's index among its same-named siblings.
|
||||
pub index: usize,
|
||||
pub children: Vec<XfaNode>,
|
||||
/// Fields: the value shown in the field's display picture, what `formattedValue` reads;
|
||||
/// `None` when the picture doesn't change it (the raw value is read then).
|
||||
pub formatted: Option<String>,
|
||||
}
|
||||
|
||||
/// The event a script runs for.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct XfaEvent {
|
||||
/// `click`, `calculate`, `validate`, `initialize`, `change`, `exit`, …
|
||||
pub activity: String,
|
||||
/// The SOM of `this`.
|
||||
pub target: String,
|
||||
pub new_text: String,
|
||||
pub prev_text: String,
|
||||
}
|
||||
|
||||
/// About the document.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct XfaDoc {
|
||||
pub file_name: String,
|
||||
/// The current page, 0-based.
|
||||
pub page: usize,
|
||||
pub page_count: usize,
|
||||
}
|
||||
|
||||
/// Something a script changed or asked for.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum XfaEffect {
|
||||
SetValue {
|
||||
som: String,
|
||||
value: String,
|
||||
},
|
||||
SetPresence {
|
||||
som: String,
|
||||
presence: String,
|
||||
},
|
||||
SetAccess {
|
||||
som: String,
|
||||
access: String,
|
||||
},
|
||||
/// A new instance of a repeating subform, named by its own SOM (`…row[3]`).
|
||||
AddInstance {
|
||||
som: String,
|
||||
},
|
||||
/// The instance at this SOM is removed.
|
||||
RemoveInstance {
|
||||
som: String,
|
||||
},
|
||||
MessageBox(String),
|
||||
/// `xfa.host.resetData()`: the named fields, or every field when empty.
|
||||
ResetData(Vec<String>),
|
||||
/// `app.execMenuItem("ShowHide…")`: show or hide a side panel.
|
||||
ShowPanel(crate::Panel),
|
||||
Print,
|
||||
SaveAs,
|
||||
LaunchUrl(String),
|
||||
SetFocus(String),
|
||||
Beep,
|
||||
/// `xfa.form.recalculate(true)`.
|
||||
Recalculate,
|
||||
/// `xfa.form.remerge()`, `xfa.layout.relayout()`.
|
||||
Relayout,
|
||||
}
|
||||
|
||||
/// What running a script did.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct XfaOutcome {
|
||||
pub effects: Vec<XfaEffect>,
|
||||
pub console: Vec<String>,
|
||||
/// The script's error, if it failed.
|
||||
pub error: Option<String>,
|
||||
/// The script's completion value (a calculate script's result; a validate script's verdict).
|
||||
pub result: Option<String>,
|
||||
/// The completion value as a boolean, when it was one.
|
||||
pub result_bool: Option<bool>,
|
||||
/// Limits the script ran into (too many effects, message boxes or console lines): what
|
||||
/// was left out, for the caller to report.
|
||||
pub notes: Vec<String>,
|
||||
/// The script ran past its time limit and was left running on its own thread (the engine
|
||||
/// can't interrupt it): the caller should run no more of this form's scripts.
|
||||
pub abandoned: bool,
|
||||
}
|
||||
|
||||
/// Most nodes a snapshot may hold.
|
||||
pub(super) const MAX_NODES: usize = 200_000;
|
||||
|
||||
/// Most instances a script may add in one run.
|
||||
pub(crate) const MAX_ADDED: usize = 1_000;
|
||||
|
||||
/// Deepest tree followed.
|
||||
pub(super) const MAX_DEPTH: usize = 64;
|
||||
|
||||
/// Most effects one run may produce (after values set again on the same object are merged).
|
||||
pub const MAX_EFFECTS: usize = 10_000;
|
||||
|
||||
/// Most message boxes one run may show.
|
||||
pub const MAX_ALERTS: usize = 100;
|
||||
|
||||
/// Most console lines one run may print.
|
||||
pub const MAX_CONSOLE: usize = 1_000;
|
||||
|
||||
/// Longest message or console line kept, in characters.
|
||||
pub(crate) const MAX_LINE: usize = 4_096;
|
||||
|
||||
/// Most bytes of text one run may hand back or write: messages, console lines, URLs, names
|
||||
/// and the values it sets. Past it, more are left out and noted.
|
||||
pub(crate) const MAX_PAYLOAD: usize = 32 << 20;
|
||||
|
||||
/// Longest value a script may put in a field (characters); longer ones are cut and noted.
|
||||
pub(super) const MAX_VALUE: usize = 65_536;
|
||||
|
||||
/// `m` cut to [`MAX_LINE`] characters.
|
||||
pub(crate) fn clip(m: String) -> String {
|
||||
match m.char_indices().nth(MAX_LINE) {
|
||||
Some((at, _)) => format!("{}…", m.get(..at).unwrap_or_default()),
|
||||
None => m,
|
||||
}
|
||||
}
|
||||
|
||||
/// `v` cut to [`MAX_VALUE`] characters, and whether it was cut.
|
||||
pub(crate) fn clip_value(v: String) -> (String, bool) {
|
||||
match v.char_indices().nth(MAX_VALUE) {
|
||||
Some((at, _)) => (v.get(..at).unwrap_or_default().to_string(), true),
|
||||
None => (v, false),
|
||||
}
|
||||
}
|
||||
|
||||
/// The field names of a `resetData` list (comma-separated), each cut to [`MAX_LINE`] and
|
||||
/// named once, at most as many as a snapshot holds nodes.
|
||||
pub(crate) fn reset_names(list: &str) -> Vec<String> {
|
||||
let mut seen = HashSet::new();
|
||||
let mut out = Vec::new();
|
||||
for name in list.split(',').map(str::trim).filter(|s| !s.is_empty()) {
|
||||
if out.len() >= MAX_NODES {
|
||||
break;
|
||||
}
|
||||
let name = clip(name.to_string());
|
||||
if seen.insert(name.clone()) {
|
||||
out.push(name);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The (property, SOM) an effect setting a property of an object is merged by, see
|
||||
/// [`XHost::set_at`].
|
||||
pub(super) fn effect_key(e: &XfaEffect) -> Option<(u8, String)> {
|
||||
match e {
|
||||
XfaEffect::SetValue { som, .. } => Some((0, som.clone())),
|
||||
XfaEffect::SetPresence { som, .. } => Some((1, som.clone())),
|
||||
XfaEffect::SetAccess { som, .. } => Some((2, som.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytes of text `e` holds.
|
||||
pub(super) fn payload(e: &XfaEffect) -> usize {
|
||||
match e {
|
||||
XfaEffect::SetValue { som, value: s } | XfaEffect::SetPresence { som, presence: s } | XfaEffect::SetAccess { som, access: s } => {
|
||||
som.len().saturating_add(s.len())
|
||||
}
|
||||
XfaEffect::AddInstance { som } | XfaEffect::RemoveInstance { som } => som.len(),
|
||||
XfaEffect::MessageBox(s) | XfaEffect::LaunchUrl(s) | XfaEffect::SetFocus(s) => s.len(),
|
||||
XfaEffect::ResetData(names) => names.iter().fold(0usize, |n, s| n.saturating_add(s.len()).saturating_add(std::mem::size_of::<String>())),
|
||||
XfaEffect::Print | XfaEffect::SaveAs | XfaEffect::Beep | XfaEffect::Recalculate | XfaEffect::Relayout | XfaEffect::ShowPanel(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// A script's completion value (what a calculate script puts in its field) cut to
|
||||
/// [`MAX_VALUE`] characters, as values a script sets are, with a note when it was.
|
||||
pub(crate) fn clip_result(out: &mut XfaOutcome) {
|
||||
if let Some(r) = out.result.take() {
|
||||
let (r, cut) = clip_value(r);
|
||||
if cut {
|
||||
out.notes.push(format!("the script's result was longer than {MAX_VALUE} characters; it was cut"));
|
||||
}
|
||||
out.result = Some(r);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
use super::globals::{recalculate, relayout};
|
||||
use super::host::{HNode, XHost, host};
|
||||
use super::instances::{instance_manager, list_object};
|
||||
use super::model::{MAX_DEPTH, XfaEffect};
|
||||
use super::*;
|
||||
|
||||
// ── node objects ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const IDX: &str = "__xfaNode";
|
||||
|
||||
pub(super) fn node_index(target: &JsValue, ctx: &mut Context) -> JsResult<usize> {
|
||||
let o = target.as_object().ok_or_else(|| error("not an XFA node"))?;
|
||||
let v = o.get(JsString::from(IDX), ctx)?;
|
||||
let n = v.to_number(ctx)?;
|
||||
if !n.is_finite() || n < 0.0 {
|
||||
return Err(error("not an XFA node"));
|
||||
}
|
||||
Ok(n as usize)
|
||||
}
|
||||
|
||||
/// A Proxy standing for node `idx`.
|
||||
pub(super) fn node_object(ctx: &mut Context, idx: usize) -> JsResult<JsValue> {
|
||||
let target = ObjectInitializer::new(ctx).property(JsString::from(IDX), JsValue::from(idx as f64), Attribute::READONLY).build();
|
||||
let proxy = JsProxy::builder(target).get(node_get).set(node_set).has(node_has).build(ctx)?;
|
||||
Ok(proxy.into())
|
||||
}
|
||||
|
||||
/// An object that takes any property read or write without complaint (`this.border.fill…`,
|
||||
/// `this.fillColor = …`): what a script sets there changes nothing here.
|
||||
pub(super) fn sink(ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let target = ObjectInitializer::new(ctx).build();
|
||||
let proxy = JsProxy::builder(target).get(sink_get).set(sink_set).build(ctx)?;
|
||||
Ok(proxy.into())
|
||||
}
|
||||
|
||||
fn sink_get(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let key = arg(args, 1);
|
||||
if key.as_symbol().is_some() {
|
||||
return Ok(JsValue::undefined());
|
||||
}
|
||||
match text(&key, ctx)?.as_str() {
|
||||
"value" | "toString" | "valueOf" => Ok(s("")),
|
||||
_ => sink(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
fn sink_set(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
|
||||
Ok(JsValue::from(true))
|
||||
}
|
||||
|
||||
fn node_has(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
|
||||
Ok(JsValue::from(true))
|
||||
}
|
||||
|
||||
fn value_js(h: &HNode) -> JsValue {
|
||||
if h.value.is_empty() {
|
||||
return JsValue::null();
|
||||
}
|
||||
if h.numeric
|
||||
&& let Some(n) = as_number(&h.value)
|
||||
{
|
||||
return JsValue::from(n);
|
||||
}
|
||||
s(&h.value)
|
||||
}
|
||||
|
||||
/// `(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))
|
||||
}
|
||||
|
||||
/// 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())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Resolve a SOM expression from node `from`: `$`, `$form`, `$record`, `$host`, `xfa.form…`,
|
||||
/// `parent`, `..name` (descendant search), `name[n]`, `name[*]`, `_name`.
|
||||
pub(crate) fn resolve_som(h: &XHost, from: usize, expr: &str) -> Vec<usize> {
|
||||
let expr = expr.trim();
|
||||
if expr.is_empty() {
|
||||
return vec![from];
|
||||
}
|
||||
let root = 0usize;
|
||||
let mut cur: Vec<usize> = vec![from];
|
||||
let mut rest = expr;
|
||||
let starts =
|
||||
[("$form", root), ("xfa.form", root), ("$record", root), ("xfa.datasets.data", root), ("xfa.record", root), ("$", from), ("this", from)];
|
||||
for (prefix, at) in starts {
|
||||
if let Some(r) = rest.strip_prefix(prefix)
|
||||
&& (r.is_empty() || r.starts_with('.') || r.starts_with('['))
|
||||
{
|
||||
cur = vec![at];
|
||||
rest = r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut depth = 0;
|
||||
while !rest.is_empty() && depth < MAX_DEPTH {
|
||||
depth += 1;
|
||||
let descend = rest.starts_with("..");
|
||||
rest = rest.trim_start_matches('.');
|
||||
let end = rest.find(['.', '[']).unwrap_or(rest.len());
|
||||
let (name, after) = rest.split_at(end);
|
||||
let name = name.trim();
|
||||
let (index, after) = match after.strip_prefix('[') {
|
||||
Some(a) => {
|
||||
let close = a.find(']').unwrap_or(a.len());
|
||||
let idx = a.get(..close).map(str::trim).unwrap_or("");
|
||||
(Some(idx.to_string()), a.get(close + 1..).unwrap_or(""))
|
||||
}
|
||||
None => (None, after),
|
||||
};
|
||||
rest = after;
|
||||
let mut next = Vec::new();
|
||||
for &c in &cur {
|
||||
match name {
|
||||
"" => {}
|
||||
"parent" => {
|
||||
if let Some(p) = h.nodes.get(c).and_then(|n| n.parent) {
|
||||
next.push(p);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let mut found: Vec<usize> = if descend { descendants_named(h, c, name, 0) } else { instances(h, c, name) };
|
||||
// A relative name a node has no child for is looked for among its siblings
|
||||
// and in each enclosing container, nearest first (so a field's script can
|
||||
// say `resolveNode("otherField")`).
|
||||
if found.is_empty() && depth == 1 && !descend {
|
||||
let mut at = h.nodes.get(c).and_then(|n| n.parent);
|
||||
let mut guard = 0;
|
||||
while let Some(p) = at
|
||||
&& guard < MAX_DEPTH
|
||||
{
|
||||
found = instances(h, p, name);
|
||||
if !found.is_empty() {
|
||||
break;
|
||||
}
|
||||
at = h.nodes.get(p).and_then(|n| n.parent);
|
||||
guard += 1;
|
||||
}
|
||||
}
|
||||
match index.as_deref() {
|
||||
Some("*") => next.extend(found),
|
||||
Some(i) => {
|
||||
if let Some(k) = i.parse::<usize>().ok().and_then(|i| found.get(i)) {
|
||||
next.push(*k);
|
||||
}
|
||||
}
|
||||
None => next.extend(found.first()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Each node once: `row[*].parent.row[*]` would otherwise multiply the list by the
|
||||
// number of rows at every step.
|
||||
let mut seen = std::collections::HashSet::with_capacity(next.len());
|
||||
next.retain(|i| seen.insert(*i));
|
||||
cur = next;
|
||||
if cur.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
cur
|
||||
}
|
||||
|
||||
/// The live descendants of node `from` named `name` (`from..name` in a SOM path), in document
|
||||
/// order, to [`MAX_DEPTH`].
|
||||
pub(crate) fn descendants_named(h: &XHost, from: usize, name: &str, depth: usize) -> Vec<usize> {
|
||||
let mut out = Vec::new();
|
||||
if depth > MAX_DEPTH {
|
||||
return out;
|
||||
}
|
||||
let Some(n) = h.nodes.get(from) else { return out };
|
||||
for &c in &n.children {
|
||||
if let Some(k) = h.nodes.get(c)
|
||||
&& !k.gone
|
||||
{
|
||||
if k.name == name {
|
||||
out.push(c);
|
||||
} else {
|
||||
out.extend(descendants_named(h, c, name, depth + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A property read on a form node's Proxy: its value (and `formattedValue` as its picture shows
|
||||
/// it), attributes, relatives, methods, instance managers and children by name.
|
||||
fn node_get(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let idx = node_index(&arg(args, 0), ctx)?;
|
||||
let key = arg(args, 1);
|
||||
if key.as_symbol().is_some() {
|
||||
return Ok(JsValue::undefined());
|
||||
}
|
||||
let key = text(&key, ctx)?;
|
||||
let snapshot = {
|
||||
let h = host(ctx)?.borrow();
|
||||
let Some(n) = h.nodes.get(idx) else { return Ok(JsValue::undefined()) };
|
||||
(n.name.clone(), n.som.clone(), n.kind, n.value.clone(), n.presence.clone(), n.access.clone(), n.index, n.parent, n.children.clone())
|
||||
};
|
||||
let (name, som, kind, value, presence, access, index, parent, children) = snapshot;
|
||||
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)),
|
||||
"name" => s(&name),
|
||||
"somExpression" => s(&som),
|
||||
"className" => s(kind.class_name()),
|
||||
"index" | "instanceIndex" => JsValue::from(index as f64),
|
||||
"presence" => s(&presence),
|
||||
"access" => s(&access),
|
||||
"isNull" => JsValue::from(value.is_empty()),
|
||||
"mandatory" => s("disabled"),
|
||||
"parent" => match parent {
|
||||
Some(p) => node_object(ctx, p)?,
|
||||
None => JsValue::null(),
|
||||
},
|
||||
"nodes" => {
|
||||
let live: Vec<usize> = {
|
||||
let h = host(ctx)?.borrow();
|
||||
children.iter().copied().filter(|&c| h.nodes.get(c).is_some_and(|n| !n.gone)).collect()
|
||||
};
|
||||
list_object(ctx, &live)?
|
||||
}
|
||||
"all" => {
|
||||
let live = match parent {
|
||||
Some(p) => instances(&host(ctx)?.borrow(), p, &name),
|
||||
None => vec![idx],
|
||||
};
|
||||
list_object(ctx, &live)?
|
||||
}
|
||||
"instanceManager" => match parent {
|
||||
Some(p) => instance_manager(ctx, p, &name)?,
|
||||
None => JsValue::undefined(),
|
||||
},
|
||||
"resolveNode" => resolver(ctx, idx, false)?,
|
||||
"resolveNodes" => resolver(ctx, idx, true)?,
|
||||
"recalculate" => function(ctx, NativeFunction::from_fn_ptr(recalculate)).into(),
|
||||
"remerge" => function(ctx, NativeFunction::from_fn_ptr(relayout)).into(),
|
||||
"getAttribute"
|
||||
| "setAttribute"
|
||||
| "execEvent"
|
||||
| "execInitialize"
|
||||
| "execCalculate"
|
||||
| "execValidate"
|
||||
| "getElement"
|
||||
| "setElement"
|
||||
| "isPropertySpecified"
|
||||
| "loadXML"
|
||||
| "saveXML"
|
||||
| "clone" => function(ctx, NativeFunction::from_fn_ptr(noop)).into(),
|
||||
"toString" | "valueOf" | "toJSON" => function(ctx, NativeFunction::from_fn_ptr(node_to_string)).into(),
|
||||
"length" => JsValue::undefined(),
|
||||
"border" | "ui" | "font" | "caption" | "fillColor" | "borderColor" | "fontColor" | "margin" | "para" | "assist" | "validate" | "format"
|
||||
| "items" | "bind" | "keep" | "h" | "w" | "x" | "y" | "minH" | "maxH" | "minW" | "maxW" | "layout" | "model" | "oneOfChild" | "locale"
|
||||
| "relevant" | "colSpan" | "dataNode" | "defaultValue" | "editValue" | "selectedIndex" | "event" | "calculate" | "traversal" | "extras"
|
||||
| "desc" => sink(ctx)?,
|
||||
other => {
|
||||
if let Some(child) = other.strip_prefix('_') {
|
||||
let exists = child_named(&host(ctx)?.borrow(), idx, child).is_some();
|
||||
return if exists { instance_manager(ctx, idx, child) } else { Ok(JsValue::undefined()) };
|
||||
}
|
||||
let found = child_named(&host(ctx)?.borrow(), idx, other);
|
||||
match found {
|
||||
Some(c) => node_object(ctx, c)?,
|
||||
None => JsValue::undefined(),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn node_set(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let idx = node_index(&arg(args, 0), ctx)?;
|
||||
let key = arg(args, 1);
|
||||
if key.as_symbol().is_some() {
|
||||
return Ok(JsValue::from(true));
|
||||
}
|
||||
let key = text(&key, ctx)?;
|
||||
let value = arg(args, 2);
|
||||
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();
|
||||
match key.as_str() {
|
||||
"rawValue" | "value" | "formattedValue" => {
|
||||
// Not a field: the property doesn't exist, as in Acrobat, and nothing is recorded.
|
||||
h.set_value(idx, value);
|
||||
}
|
||||
"presence" => {
|
||||
let p = match value.as_str() {
|
||||
"hidden" | "invisible" | "inactive" | "visible" => value.clone(),
|
||||
_ => return Ok(JsValue::from(true)),
|
||||
};
|
||||
h.set_state(idx, XfaEffect::SetPresence { som, presence: p });
|
||||
}
|
||||
"access" => {
|
||||
let a = match value.as_str() {
|
||||
"open" | "readOnly" | "protected" | "nonInteractive" => value.clone(),
|
||||
_ => return Ok(JsValue::from(true)),
|
||||
};
|
||||
h.set_state(idx, XfaEffect::SetAccess { som, access: a });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(JsValue::from(true))
|
||||
}
|
||||
|
||||
fn node_to_string(this: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let idx = node_index(this, ctx)?;
|
||||
Ok(s(&host(ctx)?.borrow().nodes.get(idx).map(|n| n.value.clone()).unwrap_or_default()))
|
||||
}
|
||||
|
||||
pub(super) fn noop(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
|
||||
/// `resolveNode(som)` / `resolveNodes(som)` bound to node `from`.
|
||||
pub(super) fn resolver(ctx: &mut Context, from: usize, many: bool) -> JsResult<JsValue> {
|
||||
let key = ObjectInitializer::new(ctx)
|
||||
.property(js_string!("from"), JsValue::from(from as f64), Attribute::READONLY)
|
||||
.property(js_string!("many"), JsValue::from(many), Attribute::READONLY)
|
||||
.build();
|
||||
fn resolve(_: &JsValue, args: &[JsValue], key: &JsObject, ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let from = key.get(js_string!("from"), ctx)?.to_number(ctx)? as usize;
|
||||
let many = key.get(js_string!("many"), ctx)?.to_boolean();
|
||||
let expr = text(&arg(args, 0), ctx)?;
|
||||
let found = resolve_som(&host(ctx)?.borrow(), from, &expr);
|
||||
if many {
|
||||
return list_object(ctx, &found);
|
||||
}
|
||||
match found.first() {
|
||||
Some(i) => node_object(ctx, *i),
|
||||
None => Ok(JsValue::null()),
|
||||
}
|
||||
}
|
||||
Ok(function(ctx, NativeFunction::from_copy_closure_with_captures(resolve, key)).into())
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
use super::globals::install;
|
||||
use super::host::{Shared, XHost};
|
||||
use super::model::{XfaDoc, XfaEvent, XfaNode, XfaOutcome, clip_result};
|
||||
use super::*;
|
||||
|
||||
/// Run `script` for `event` on the form `root` (the root subform), from node `event.target`.
|
||||
pub fn run_xfa(script: &str, event: &XfaEvent, doc: &XfaDoc, root: &XfaNode, limits: Limits) -> XfaOutcome {
|
||||
let failed = |why: String| XfaOutcome { error: Some(why), ..Default::default() };
|
||||
if let Some(why) = refuse(script) {
|
||||
return failed(why);
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
std::thread::scope(|scope| {
|
||||
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, &|| {}));
|
||||
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}")),
|
||||
}
|
||||
})
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
run_here(script, event, doc, root.clone(), limits, &|| {})
|
||||
}
|
||||
|
||||
/// [`run_xfa`] with a time limit: a script still running `timeout` after the engine is ready
|
||||
/// (loops inside nested function calls can run for hours within the engine's per-frame loop
|
||||
/// limit) is left on its thread, which ends when the engine's own limits stop it, and an
|
||||
/// outcome with `abandoned` set comes back at once. In the browser build there are no
|
||||
/// threads: the script runs to its limits.
|
||||
pub fn run_xfa_within(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNode, limits: Limits, timeout: std::time::Duration) -> XfaOutcome {
|
||||
if let Some(why) = refuse(script) {
|
||||
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))
|
||||
}
|
||||
|
||||
/// Longest the script engine may take to be ready for a script (its thread and context, and
|
||||
/// for the first script in a process the engine's own set-up) before the script is abandoned.
|
||||
/// It doesn't count against the script's time: on a busy machine the start alone can take
|
||||
/// longer than the tightest limit, and abandoning a script turns the form's scripts off.
|
||||
pub(crate) const ENGINE_START_LIMIT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
/// Run `script` on its own thread (with the script stack) and wait at most `timeout` for its
|
||||
/// outcome, counted from when it calls `started` (once the engine is ready); past that the
|
||||
/// thread is abandoned and the outcome says so. On wasm there are no threads: the script runs
|
||||
/// here.
|
||||
pub(crate) fn run_within(name: &str, timeout: std::time::Duration, script: impl FnOnce(&dyn Fn()) -> XfaOutcome + Send + 'static) -> XfaOutcome {
|
||||
run_within_limits(name, ENGINE_START_LIMIT, timeout, script)
|
||||
}
|
||||
|
||||
/// [`run_within`] with the engine's start-up limit given.
|
||||
pub(crate) fn run_within_limits(
|
||||
name: &str,
|
||||
start_limit: std::time::Duration,
|
||||
timeout: std::time::Duration,
|
||||
script: impl FnOnce(&dyn Fn()) -> XfaOutcome + Send + 'static,
|
||||
) -> XfaOutcome {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
use std::sync::mpsc::RecvTimeoutError;
|
||||
use std::time::Instant;
|
||||
enum Step {
|
||||
Started,
|
||||
Done(XfaOutcome),
|
||||
}
|
||||
let failed = |why: String| XfaOutcome { error: Some(why), ..Default::default() };
|
||||
let abandoned = |why: String| XfaOutcome { error: Some(why), abandoned: true, ..Default::default() };
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let spawned = std::thread::Builder::new().name(name.into()).stack_size(crate::SCRIPT_STACK).spawn(move || {
|
||||
// The receiver is gone when the caller stopped waiting: nothing to report then.
|
||||
let started = tx.clone();
|
||||
let outcome = script(&move || {
|
||||
let _ = started.send(Step::Started);
|
||||
});
|
||||
let _ = tx.send(Step::Done(outcome));
|
||||
});
|
||||
if let Err(e) = spawned {
|
||||
return failed(format!("the script engine could not start: {e}"));
|
||||
}
|
||||
// Wait for the engine within its start-up limit, then for the script within its own.
|
||||
let (mut running, mut limit) = (false, start_limit);
|
||||
let mut deadline = Instant::now().checked_add(limit);
|
||||
loop {
|
||||
let left = deadline.map_or(limit, |d| d.saturating_duration_since(Instant::now()));
|
||||
match rx.recv_timeout(left) {
|
||||
Ok(Step::Done(o)) => return o,
|
||||
Ok(Step::Started) => {
|
||||
// A second call changes nothing: the script's time has already begun.
|
||||
if !running {
|
||||
(running, limit) = (true, timeout);
|
||||
deadline = Instant::now().checked_add(limit);
|
||||
}
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) if running => {
|
||||
return abandoned(format!("the script ran longer than {:.1} s and was abandoned", timeout.as_secs_f64()));
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => {
|
||||
return abandoned(format!("the script engine took longer than {:.0} s to start", start_limit.as_secs_f64()));
|
||||
}
|
||||
Err(RecvTimeoutError::Disconnected) => return failed("the script stopped with an internal error".into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = (name, start_limit, timeout);
|
||||
script(&|| {})
|
||||
}
|
||||
}
|
||||
|
||||
/// The first script in a process sets the engine up (its built-ins, parser and compiler come
|
||||
/// into use): a trivial script takes that on, so a form's script is never timed with it.
|
||||
pub(crate) fn warm_up() {
|
||||
static WARM: std::sync::Once = std::sync::Once::new();
|
||||
// `call_once_force`: a poisoned `Once` (a panic while warming up) only means warming up again.
|
||||
WARM.call_once_force(|_| {
|
||||
let mut ctx = Context::default();
|
||||
let _ = ctx.eval(Source::from_bytes(b"(function () { return eval('0'); }).call({})"));
|
||||
});
|
||||
}
|
||||
|
||||
/// `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 {
|
||||
warm_up();
|
||||
let mut ctx = match Context::builder().host_hooks(std::rc::Rc::new(crate::CheckedCodeFromStrings)).build() {
|
||||
Ok(ctx) => ctx,
|
||||
Err(e) => return XfaOutcome { error: Some(format!("the script engine could not start: {e}")), ..Default::default() },
|
||||
};
|
||||
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 current = host_state.current;
|
||||
ctx.insert_data(RefCell::new(host_state));
|
||||
let mut out = XfaOutcome::default();
|
||||
let result = (|| -> JsResult<()> {
|
||||
install(&mut ctx, event, doc, current)?;
|
||||
// Ready for the script: its time starts now, the set-up above not charged to it.
|
||||
started();
|
||||
ctx.register_global_property(js_string!("__xfa_src"), s(script), Attribute::CONFIGURABLE)?;
|
||||
// Direct eval inside a function called on the node: `this` is the node and the
|
||||
// completion value is the script's last expression (what a calculate script yields).
|
||||
let r = ctx.eval(Source::from_bytes(b"(function () { return eval(__xfa_src); }).call(__xfa_this)"))?;
|
||||
if !r.is_undefined() {
|
||||
// Best effort: a value that can't be made text (a symbol, say) is no failure.
|
||||
out.result = Some(text(&r, &mut ctx).unwrap_or_default());
|
||||
if r.is_boolean() {
|
||||
out.result_bool = Some(r.to_boolean());
|
||||
} else if r.is_null() {
|
||||
out.result = Some(String::new());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
if let Err(e) = result {
|
||||
out.error = Some(e.to_string());
|
||||
}
|
||||
if let Some(h) = ctx.remove_data::<Shared>() {
|
||||
let h = h.into_inner();
|
||||
out.notes = h.notes();
|
||||
out.effects = h.effects;
|
||||
out.console = h.console;
|
||||
}
|
||||
clip_result(&mut out);
|
||||
out
|
||||
}
|
||||
Reference in New Issue
Block a user