Files
pdfcraft/crates/js/src/document.rs
T
FredAppsandDWAA1660 059dbd6e00 feat(forms): open, Will Save and Will Print actions, widget pointer/focus scripts, setItems/setAction/toSource (port of #344) (#6969)
* feat(js, forms): field setItems, setAction and a bounded toSource (#269)

Ported from #344 by @DWAA1660 onto the per-document script session.

- `field.setItems(items)` replaces a combo or list box's items (text, or
  [text, export] pairs). The value stays when an item still has it, and
  otherwise becomes the first item's. Written as the field's /Opt (pairs
  only where the export value differs), /I dropped, field redrawn.
- `field.setAction(trigger, script)` installs a JavaScript action for one
  trigger (MouseUp/Down/Enter/Exit, OnFocus/OnBlur, Keystroke, Validate,
  Calculate, Format), replacing only that trigger's action.
- `Object.prototype.toSource()` writes plain data (strings, numbers,
  booleans, null, undefined, arrays, objects) that eval reads back. It
  refuses functions, cycles, more than 64 levels, 10,000 values or 1 MiB
  with an error the script can catch.

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* feat(engine): run a form's open, Will Save and Will Print actions and its widgets' pointer and focus scripts (#269)

Ported from #344 by @DWAA1660 onto the per-document script session: each
runs in the document's retained realm, so state the document scripts and
earlier events left is kept.

- The catalog /OpenAction's JavaScript (and its /Next chain, bounded and
  cycle-safe; other action types skipped) runs once on opening, after the
  document scripts, for background opens and reloads too, and again on
  Revert in a fresh realm. Event Doc/Open.
- `Session::run_document_actions` runs /AA /WS (Will Save) or /AA /WP
  (Will Print). A failing action is reported and never stops the save or
  print. doc_save and doc_print run them first and report what they said
  (`will_save`, `will_print`).
- Scripts that run unasked (open, Will Save, Will Print, pointer over,
  focus) may only ask for highlight, zoom, focus or a page; a click's
  Mouse Down / Mouse Up keep their requests.
- `Session::field_trigger` runs a widget's Mouse Enter, Mouse Exit, Mouse
  Down, Mouse Up, Focus or Blur JavaScript (one undoable edit), reading only
  that widget's dictionary. `field_focus` now runs an AcroForm widget's On
  Focus / On Blur as well as XFA enter/exit.
- New tool `js_event`; form_focus's description says AcroForm focus
  scripts run too.

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* feat(ui): Will Save before saving, Will Print before printing, and widget pointer scripts in the app (#269, #247)

Ported from #344 by @DWAA1660.

- Save (both paths) runs the document's Will Save actions first, and Print
  its Will Print actions; what they change is saved or printed, and what
  they say is shown.
- The pointer moving onto or off a form widget queues its Mouse Enter /
  Mouse Exit script, a press its Mouse Down. A click on a check box or radio
  button queues its Mouse Up after the toggle, so the script reads the value
  the click stored (#247). The queue runs after the frame's edit.

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* docs(parity): document actions and widget triggers are partly supported (#269)

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* test: check toSource's text (scripts can't eval it) and the trigger event names (#269)

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* test: a check box's Mouse Up reads its new value; keep the fixtures on screen and the realm across an in-place save (#247, #269)

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* fix(engine, forms, js, ui): review fixes for document actions and field triggers

- widget trigger actions follow /Next, each action its own edit; errors don't stop the chain
- setAction installs on the field the object stands for when names repeat
- toSource refuses non-plain objects and a __proto__ key
- doc_print checks the destination before Will Print; js_event returns only its own output
- combo/list clicks run Mouse Up; a refused click's Mouse Up doesn't run
- label crops are re-read when Will Print changed the document

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* forms: run a pointer trigger on the field it hit and refresh the edited field's draft

A trigger now carries the field's object, so of fields sharing a name the one under the
pointer runs its own script (Session::field_trigger_target); js_event passes the field it
resolved. A trigger script changing the field being edited shows in its editor unless the
user already typed there, as focus scripts do.

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

* forms: carry the field's identity through focus scripts and js_event

Focus transitions now track the field object, so moving between fields sharing a name is a
move and the field that gained or lost focus runs its own script
(Session::field_focus_target). js_event takes an optional field_id from form_fields to pick
one of fields sharing a name.

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>

---------

Co-authored-by: DWAA1660 <dwatnip123@gmail.com>
2026-11-11 13:09:37 +01:00

486 lines
25 KiB
Rust

use super::host::{FieldKey, arg, colour, error, function, host, read, s, set_value, strings, text, text_line, value_of, write};
use super::model::{DISPLAY_HIDDEN, DISPLAY_VISIBLE, FieldType, Request};
use super::{Attribute, Context, JsArray, JsObject, JsResult, JsString, JsValue, NativeFunction, ObjectInitializer, js_string};
macro_rules! getter {
($ctx:expr, $name:expr, |$f:ident, $c:ident| $body:expr) => {{
fn get(_: &JsValue, _: &[JsValue], name: &FieldKey, $c: &mut Context) -> JsResult<JsValue> {
let $f = read($c, name)?;
Ok($body)
}
function($ctx, NativeFunction::from_copy_closure_with_captures(get, $name.clone()))
}};
}
/// Resolve public name lookups to their first matching object, preserving ordinary getField behavior.
pub(crate) fn field_object(ctx: &mut Context, name: &str) -> JsObject {
let id = host(ctx).ok().and_then(|h| h.borrow().fields.iter().find(|f| f.name == name).and_then(|f| f.object_id));
field_object_by_id(ctx, name, id)
}
/// Cache by object identity as well as name so duplicate-named targets remain distinct.
pub(crate) fn field_object_by_id(ctx: &mut Context, name: &str, id: Option<u64>) -> JsObject {
let token = host(ctx)
.ok()
.and_then(|h| h.borrow().fields.iter().find(|f| f.object_id == id && (id.is_some() || f.name == name)).map(|f| f.instance_id))
.unwrap_or(0);
let key = crate::host::field_cache_key(name, id, token);
if let Some(object) = host(ctx).ok().and_then(|h| h.borrow().objects.get(&key).cloned()) {
return object;
}
let object = field_object_new(ctx, name, id, token);
if let Ok(h) = host(ctx)
&& h.borrow().field_keys.contains(&key)
{
h.borrow_mut().objects.insert(key, object.clone());
}
object
}
/// Construct live accessors whose identity survives host snapshot refreshes.
fn field_object_new(ctx: &mut Context, name: &str, id: Option<u64>, token: u64) -> JsObject {
let n = (JsString::from(name), id, token);
let value_get = getter!(ctx, n, |f, c| value_of(&f, c));
let value_set = {
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let vals = strings(&arg(args, 0), c)?;
write(c, name, |f| set_value(f, vals))?;
Ok(JsValue::undefined())
}
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
};
let as_string = getter!(ctx, n, |f, _c| s(&match f.kind {
FieldType::CheckBox | FieldType::RadioButton => f.value.first().cloned().unwrap_or_else(|| "Off".into()),
_ => f.value.join(","),
}));
let default = getter!(ctx, n, |f, _c| s(&f.default.first().cloned().unwrap_or_default()));
let kind = getter!(ctx, n, |f, _c| s(f.kind.name()));
let fname = getter!(ctx, n, |f, _c| s(&f.name));
let readonly_get = getter!(ctx, n, |f, _c| JsValue::from(f.readonly));
let readonly_set = {
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let on = arg(args, 0).to_boolean();
write(c, name, |f| f.readonly = on)?;
Ok(JsValue::undefined())
}
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
};
let required_get = getter!(ctx, n, |f, _c| JsValue::from(f.required));
let required_set = {
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let on = arg(args, 0).to_boolean();
write(c, name, |f| f.required = on)?;
Ok(JsValue::undefined())
}
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
};
let display_get = getter!(ctx, n, |f, _c| JsValue::from(f.display));
let display_set = {
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let d = arg(args, 0).to_number(c)? as i32;
write(c, name, |f| f.display = d.clamp(0, 3))?;
Ok(JsValue::undefined())
}
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
};
let hidden_get = getter!(ctx, n, |f, _c| JsValue::from(f.display == DISPLAY_HIDDEN));
let hidden_set = {
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let on = arg(args, 0).to_boolean();
write(c, name, |f| f.display = if on { DISPLAY_HIDDEN } else { DISPLAY_VISIBLE })?;
Ok(JsValue::undefined())
}
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
};
let num_items = getter!(ctx, n, |f, _c| JsValue::from(f.options.len() as f64));
let char_limit = getter!(ctx, n, |f, _c| JsValue::from(f.char_limit.unwrap_or(0) as f64));
let export_values = getter!(ctx, n, |f, c| {
let vals: Vec<JsValue> = f.options.iter().map(|o| s(&o.0)).collect();
JsArray::from_iter(vals, c).into()
});
let indices = getter!(ctx, n, |f, c| {
let idx: Vec<JsValue> = f.options.iter().enumerate().filter(|(_, o)| f.value.contains(&o.0)).map(|(i, _)| JsValue::from(i as f64)).collect();
if idx.len() == 1 { idx[0].clone() } else { JsArray::from_iter(idx, c).into() }
});
let text_color_get = getter!(ctx, n, |f, c| colour(f.text_color.as_deref(), c));
let text_color_set = {
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let v = strings(&arg(args, 0), c)?;
write(c, name, |f| f.text_color = Some(v.into()))?;
Ok(JsValue::undefined())
}
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
};
let fill_color_get = getter!(ctx, n, |f, c| colour(f.fill_color.as_deref(), c));
let fill_color_set = {
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let v = strings(&arg(args, 0), c)?;
write(c, name, |f| f.fill_color = Some(v.into()))?;
Ok(JsValue::undefined())
}
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
};
let text_size_get = getter!(ctx, n, |f, c| JsValue::from(f.text_size));
let text_size_set = {
/// Validate a finite text size in 0..=300 points before staging the field appearance change.
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let v = arg(args, 0).to_number(c)?;
if !v.is_finite() || !(0.0..=300.0).contains(&v) {
return Err(error("textSize must be between 0 and 300"));
}
write(c, name, |f| f.text_size = v)?;
Ok(JsValue::undefined())
}
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
};
let line_width_get = getter!(ctx, n, |f, c| JsValue::from(f.line_width));
let line_width_set = {
/// Validate a finite border width in 0..=12 points before staging the appearance change.
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let v = arg(args, 0).to_number(c)?;
if !v.is_finite() || !(0.0..=12.0).contains(&v) {
return Err(error("lineWidth must be between 0 and 12"));
}
write(c, name, |f| f.line_width = v)?;
Ok(JsValue::undefined())
}
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
};
let border_style_get = getter!(ctx, n, |f, c| s(&f.border_style));
let border_style_set = {
/// Accept only supported border styles and stage the validated value for PDF writeback.
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let v = text(&arg(args, 0), c)?;
if !["solid", "dashed", "beveled", "inset", "underline"].contains(&v.as_str()) {
return Err(error("unsupported borderStyle"));
}
write(c, name, |f| f.border_style = v)?;
Ok(JsValue::undefined())
}
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
};
let user_name_get = getter!(ctx, n, |f, c| s(&f.user_name));
let user_name_set = {
/// Bound the tooltip string to 4,096 bytes before staging the field user name.
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let v = text(&arg(args, 0), c)?;
if v.len() > 4096 {
return Err(error("userName is too long"));
}
write(c, name, |f| f.user_name = v)?;
Ok(JsValue::undefined())
}
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
};
let stroke_color_get = getter!(ctx, n, |f, c| colour(f.stroke_color.as_deref(), c));
let stroke_color_set = {
/// Read the bounded stroke-colour array for validation at the form writeback boundary.
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let colour = strings(&arg(args, 0), c)?;
write(c, name, |f| f.stroke_color = Some(colour.into()))?;
Ok(JsValue::undefined())
}
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
};
let rect_get = getter!(ctx, n, |f, c| f.rect.map_or_else(JsValue::undefined, |r| JsArray::from_iter(r.map(JsValue::from), c).into()));
let rect_set = {
/// Validate finite ordered rectangle coordinates while permitting zero-sized script overlays.
fn set(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let a = arg(args, 0).as_object().ok_or_else(|| error("rect needs an array"))?;
let a = JsArray::from_object(a)?;
if a.length(c)? != 4 {
return Err(error("rect needs four coordinates"));
}
let mut coords = [0.0; 4];
for (i, v) in coords.iter_mut().enumerate() {
*v = a.get(i as u32, c)?.to_number(c)?;
if !v.is_finite() || v.abs() > 1e7 {
return Err(error("rect coordinates are out of range"));
}
}
if coords[2] < coords[0] || coords[1] < coords[3] {
return Err(error("rect must use left, top, right, bottom order"));
}
let v = Some(coords);
write(c, name, |f| f.rect = v)?;
Ok(JsValue::undefined())
}
function(ctx, NativeFunction::from_copy_closure_with_captures(set, n.clone()))
};
/// Read the normal caption of the first button widget and refuse unsupported caption faces.
fn button_get_caption(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let f = read(c, name)?;
if f.kind != FieldType::Button {
return Err(error(format!("{} is not a push button", f.name)));
}
let face = if args.is_empty() { 0.0 } else { arg(args, 0).to_number(c)? };
if !face.is_finite() || face != 0.0 {
return Err(error("only the normal button face (0) is supported"));
}
Ok(s(f.caption.as_deref().unwrap_or("")))
}
/// Record a normal-caption setter even when widget zero already matches, allowing all-widget writeback.
fn button_set_caption(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let f = read(c, name)?;
if f.kind != FieldType::Button {
return Err(error("buttonSetCaption is only available on push buttons"));
}
let caption_arg = args.first().ok_or_else(|| error("buttonSetCaption requires a caption argument"))?;
let caption = text_line(caption_arg, c)?;
let face = if args.get(1).is_none() { 0.0 } else { arg(args, 1).to_number(c)? };
if !face.is_finite() || face != 0.0 {
return Err(error("only the normal button face (0) is supported"));
}
if host(c)?.borrow().event_name == "Format" {
return Err(error("buttonSetCaption is not available in a Format event"));
}
write(c, name, |f| {
f.caption = Some(caption);
f.caption_written = true;
})?;
Ok(JsValue::undefined())
}
fn get_item_at(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let f = read(c, name)?;
let i = arg(args, 0).to_number(c)?;
// bExportValue defaults to true.
let export = args.get(1).is_none_or(JsValue::to_boolean);
let i = if i < 0.0 { f.options.len().saturating_sub(1) } else { i as usize };
let o = f.options.get(i).ok_or_else(|| error(format!("{} has no item {i}", f.name)))?;
Ok(s(if export { &o.0 } else { &o.1 }))
}
fn is_box_checked(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let f = read(c, name)?;
let i = arg(args, 0).to_number(c)? as usize;
let on = f.options.get(i).is_some_and(|o| f.value.first() == Some(&o.0));
Ok(JsValue::from(on))
}
fn check_this_box(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let f = read(c, name)?;
let i = arg(args, 0).to_number(c)? as usize;
let on = args.get(1).is_none_or(JsValue::to_boolean);
let export = f.options.get(i).map(|o| o.0.clone()).ok_or_else(|| error(format!("{} has no widget {i}", f.name)))?;
write(c, name, |f| {
if on {
f.value = vec![export];
} else if f.value.first() == Some(&export) {
f.value.clear();
}
})?;
Ok(JsValue::undefined())
}
fn set_focus(_: &JsValue, _: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let field = read(c, name)?;
let request = match field.object_id {
Some(object_id) => Request::FocusField { name: field.name, object_id },
None => Request::Focus(field.name),
};
host(c)?.borrow_mut().request(request);
Ok(JsValue::undefined())
}
/// `setAction(cTrigger, cScript)`: install `cScript` as the field's JavaScript for
/// `cTrigger`, replacing what it ran before; saved with the document (#269).
fn set_action(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let trigger = text(&arg(args, 0), c)?;
if !SET_ACTION_TRIGGERS.contains(&trigger.as_str()) {
return Err(error(format!("setAction: unknown trigger {trigger:?}; use one of {}", SET_ACTION_TRIGGERS.join(", "))));
}
let script = text(&arg(args, 1), c)?;
if let Some(why) = crate::run::refuse(&script) {
return Err(error(format!("setAction: {why}")));
}
write(c, name, |f| {
f.actions.retain(|(t, _)| *t != trigger);
f.actions.push((trigger, script));
})?;
Ok(JsValue::undefined())
}
/// `setItems(oArray)`: replace a combo box's or list box's items. Each item is its text, or
/// a `[text, exportValue]` pair. The value stays when an item still has it, and otherwise
/// becomes the first item's (#269).
fn set_items(_: &JsValue, args: &[JsValue], name: &FieldKey, c: &mut Context) -> JsResult<JsValue> {
let f = read(c, name)?;
if !matches!(f.kind, FieldType::ComboBox | FieldType::ListBox) {
return Err(error(format!("setItems: {} is not a combo box or list box", f.name)));
}
let list = arg(args, 0).as_object().filter(JsObject::is_array).ok_or_else(|| error("setItems needs an array of items"))?;
let list = JsArray::from_object(list)?;
let len = list.length(c)?;
if len > MAX_ITEMS as u64 {
return Err(error(format!("setItems: at most {MAX_ITEMS} items")));
}
let mut items = Vec::new();
let mut bytes = 0usize;
for i in 0..len {
let item = list.get(i, c)?;
let (label, export) = match item.as_object().filter(JsObject::is_array) {
Some(pair) => {
let pair = JsArray::from_object(pair.clone())?;
let label = text(&pair.get(0, c)?, c)?;
let export = pair.get(1, c)?;
let export = if export.is_undefined() { label.clone() } else { text(&export, c)? };
(label, export)
}
None => {
let label = text(&item, c)?;
(label.clone(), label)
}
};
bytes = bytes.saturating_add(label.len()).saturating_add(export.len());
if bytes > MAX_ITEM_BYTES {
return Err(error("setItems: the items are too long"));
}
items.push((export, label));
}
write(c, name, |f| {
let kept: Vec<String> = f.value.iter().filter(|v| items.iter().any(|(export, _)| export == *v)).cloned().collect();
f.value = if kept.is_empty() { items.first().map(|(export, _)| vec![export.clone()]).unwrap_or_default() } else { kept };
f.options = items;
})?;
Ok(JsValue::undefined())
}
let rw = Attribute::CONFIGURABLE | Attribute::ENUMERABLE;
let set_action = NativeFunction::from_copy_closure_with_captures(set_action, n.clone());
let set_items = NativeFunction::from_copy_closure_with_captures(set_items, n.clone());
let get_item_at = NativeFunction::from_copy_closure_with_captures(get_item_at, n.clone());
let is_box_checked = NativeFunction::from_copy_closure_with_captures(is_box_checked, n.clone());
let check_this_box = NativeFunction::from_copy_closure_with_captures(check_this_box, n.clone());
let set_focus = NativeFunction::from_copy_closure_with_captures(set_focus, n.clone());
let button_get_caption = NativeFunction::from_copy_closure_with_captures(button_get_caption, n.clone());
let button_set_caption = NativeFunction::from_copy_closure_with_captures(button_set_caption, n.clone());
let button_get_icon = NativeFunction::from_copy_closure_with_captures(crate::icons::get, n.clone());
let button_set_icon = NativeFunction::from_copy_closure_with_captures(crate::icons::set, n.clone());
ObjectInitializer::new(ctx)
.accessor(js_string!("value"), Some(value_get), Some(value_set), rw)
.accessor(js_string!("valueAsString"), Some(as_string), None, rw)
.accessor(js_string!("defaultValue"), Some(default), None, rw)
.accessor(js_string!("type"), Some(kind), None, rw)
.accessor(js_string!("name"), Some(fname), None, rw)
.accessor(js_string!("readonly"), Some(readonly_get), Some(readonly_set), rw)
.accessor(js_string!("required"), Some(required_get), Some(required_set), rw)
.accessor(js_string!("display"), Some(display_get), Some(display_set), rw)
.accessor(js_string!("hidden"), Some(hidden_get), Some(hidden_set), rw)
.accessor(js_string!("numItems"), Some(num_items), None, rw)
.accessor(js_string!("charLimit"), Some(char_limit), None, rw)
.accessor(js_string!("exportValues"), Some(export_values), None, rw)
.accessor(js_string!("currentValueIndices"), Some(indices), None, rw)
.accessor(js_string!("textColor"), Some(text_color_get), Some(text_color_set), rw)
.accessor(js_string!("fillColor"), Some(fill_color_get), Some(fill_color_set), rw)
.accessor(js_string!("textSize"), Some(text_size_get), Some(text_size_set), rw)
.accessor(js_string!("lineWidth"), Some(line_width_get), Some(line_width_set), rw)
.accessor(js_string!("borderStyle"), Some(border_style_get), Some(border_style_set), rw)
.accessor(js_string!("userName"), Some(user_name_get), Some(user_name_set), rw)
.accessor(js_string!("strokeColor"), Some(stroke_color_get), Some(stroke_color_set), rw)
.accessor(js_string!("rect"), Some(rect_get), Some(rect_set), rw)
.function(get_item_at, js_string!("getItemAt"), 2)
.function(is_box_checked, js_string!("isBoxChecked"), 1)
.function(check_this_box, js_string!("checkThisBox"), 2)
.function(set_focus, js_string!("setFocus"), 0)
.function(set_action, js_string!("setAction"), 2)
.function(set_items, js_string!("setItems"), 1)
.function(button_get_icon, js_string!("buttonGetIcon"), 1)
.function(button_set_icon, js_string!("buttonSetIcon"), 2)
.function(button_get_caption, js_string!("buttonGetCaption"), 1)
.function(button_set_caption, js_string!("buttonSetCaption"), 2)
.build()
}
/// The triggers `field.setAction` takes (Acrobat JavaScript reference, Field.setAction).
const SET_ACTION_TRIGGERS: [&str; 10] =
["MouseUp", "MouseDown", "MouseEnter", "MouseExit", "OnFocus", "OnBlur", "Keystroke", "Validate", "Calculate", "Format"];
/// Most items one `setItems` call takes, and most bytes of their texts and export values: a
/// script must not grow a field without bound.
const MAX_ITEMS: usize = 10_000;
const MAX_ITEM_BYTES: usize = 1 << 20;
// ── the document (global object) ────────────────────────────────────────────────────────────
pub(crate) fn get_field(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
let name = text(&arg(args, 0), ctx)?;
let exists = host(ctx)?.borrow().fields.iter().any(|f| f.name == name);
if exists {
return Ok(field_object(ctx, &name).into());
}
// A parent name (e.g. "total" for "total.0"): its first kid stands in, as for groups.
let prefix = format!("{name}.");
let kid = host(ctx)?.borrow().fields.iter().find(|f| f.name.starts_with(&prefix)).map(|f| f.name.clone());
Ok(kid.map_or(JsValue::null(), |k| field_object(ctx, &k).into()))
}
pub(crate) fn get_nth_field_name(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
let i = arg(args, 0).to_number(ctx)?;
let name = host(ctx)?.borrow().fields.get(i.max(0.0) as usize).map(|f| f.name.clone());
Ok(name.map_or(JsValue::null(), |n| s(&n)))
}
pub(crate) fn reset_form(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
{
let mut h = host(ctx)?.borrow_mut();
if h.reset_calls >= crate::MAX_RESET_CALLS {
h.reset_limit_exceeded = true;
return Err(error("reset call limit exceeded"));
}
h.reset_calls = h.reset_calls.saturating_add(1);
}
let names = strings(&arg(args, 0), ctx)?;
let mut h = host(ctx)?.borrow_mut();
let queued = h.requests.len();
h.request(Request::Reset(names));
// Keep the live shadow and the bounded host request in agreement, including clipped names.
let Some(Request::Reset(names)) = h.requests.get(queued).cloned() else { return Ok(JsValue::undefined()) };
let super::host::Host { fields, changed, .. } = &mut *h;
for (i, field) in std::sync::Arc::make_mut(fields).iter_mut().enumerate() {
if field.default_known
&& !matches!(field.kind, FieldType::Button | FieldType::Signature)
&& (names.is_empty() || names.iter().any(|n| field.name == *n || field.name.strip_prefix(n).is_some_and(|rest| rest.starts_with('.'))))
{
field.value.clone_from(&field.default);
// Retain the final snapshot even when a later assignment restores the pre-reset value.
changed.insert(i);
}
}
Ok(JsValue::undefined())
}
pub(crate) fn print(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
host(ctx)?.borrow_mut().request(Request::Print);
Ok(JsValue::undefined())
}
pub(crate) fn submit_form(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
let a = arg(args, 0);
let url = match a.as_object() {
Some(o) if !o.is_array() => {
let u = o.get(js_string!("cURL"), ctx)?;
text(&u, ctx)?
}
_ => text(&a, ctx)?,
};
host(ctx)?.borrow_mut().request(Request::Submit(url));
Ok(JsValue::undefined())
}
pub(crate) fn noop(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::undefined())
}
pub(crate) fn num_fields(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from(host(ctx)?.borrow().fields.len() as f64))
}
pub(crate) fn page_num_get(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from(host(ctx)?.borrow().doc.page as f64))
}
pub(crate) fn page_num_set(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
let n = arg(args, 0).to_number(ctx)?;
let mut h = host(ctx)?.borrow_mut();
let last = h.doc.num_pages.saturating_sub(1);
let p = (n.max(0.0) as usize).min(last);
h.doc.page = p;
h.request(Request::GoToPage(p));
Ok(JsValue::undefined())
}