use boa_engine::object::ObjectInitializer;
use boa_engine::property::Attribute;
use boa_engine::{Context, JsArgs, JsResult, JsValue, NativeFunction};
use super::bind::{now_ms, param_error, say, thrown};
use super::transcript::TranscriptLine;
fn report(effects: &pdfrum_script::AfEffects, context: &Context) {
for alert in &effects.alerts {
say(
context,
TranscriptLine::FunctionAlert {
caller: alert.caller.to_string(),
message: alert.message.to_string(),
},
);
}
}
fn settle<T>(
result: Result<T, pdfrum_script::Thrown>,
context: &Context,
name: &str,
) -> JsResult<T> {
match result {
Ok(value) => Ok(value),
Err(thrown_error) => {
report(&thrown_error.effects, context);
Err(thrown(name, &thrown_error.error))
}
}
}
fn text(args: &[JsValue], index: usize, context: &mut Context) -> JsResult<String> {
let value = args.get_or_undefined(index).clone();
Ok(value.to_string(context)?.to_std_string_lossy())
}
fn int(args: &[JsValue], index: usize, context: &mut Context) -> JsResult<i32> {
args.get_or_undefined(index).clone().to_i32(context)
}
fn number(args: &[JsValue], index: usize, context: &mut Context) -> JsResult<f64> {
args.get_or_undefined(index).clone().to_number(context)
}
fn truthy(args: &[JsValue], index: usize) -> bool {
args.get_or_undefined(index).to_boolean()
}
fn exactly(args: &[JsValue], expected: usize, name: &str) -> JsResult<()> {
if args.len() != expected {
return Err(param_error(name));
}
Ok(())
}
fn at_least(args: &[JsValue], expected: usize, name: &str) -> JsResult<()> {
if args.len() < expected {
return Err(param_error(name));
}
Ok(())
}
fn formatted(outcome: pdfrum_script::AfOutcome) -> Option<String> {
match outcome {
pdfrum_script::AfOutcome::Formatted(value) => Some(value),
pdfrum_script::AfOutcome::Rejected | pdfrum_script::AfOutcome::Unchanged => None,
}
}
fn set_event_rc(accepted: bool, context: &mut Context) {
if let Some(host) = super::bind::host(context) {
host.borrow_mut().event.rc = accepted;
}
}
fn write_event_value(value: Option<String>, context: &mut Context) {
let Some(value) = value else {
return;
};
if let Some(host) = super::bind::host(context) {
host.borrow_mut().event.value = value;
}
}
fn keystroke_of(context: &mut Context) -> pdfrum_script::Keystroke {
let Some(host) = super::bind::host(context) else {
return pdfrum_script::Keystroke::default();
};
let event = &host.borrow().event;
pdfrum_script::Keystroke {
value: event.value.clone(),
change: event.change.clone(),
sel_start: event.sel_start,
sel_end: event.sel_end,
will_commit: event.will_commit,
field_full: event.field_full,
}
}
fn apply_keystroke(outcome: &pdfrum_script::KeystrokeOutcome, context: &mut Context) {
let Some(host) = super::bind::host(context) else {
return;
};
let event = &mut host.borrow_mut().event;
match outcome {
pdfrum_script::KeystrokeOutcome::Reject => event.rc = false,
pdfrum_script::KeystrokeOutcome::Accept { value, change } => {
if let Some(value) = value {
event.value.clone_from(value);
}
if let Some(change) = change {
event.change.clone_from(change);
}
}
}
}
macro_rules! af {
($fn_name:ident, $acrobat:literal, $body:expr) => {
fn $fn_name(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let body = $body;
body(args, context, $acrobat)
}
};
}
af!(
af_number_format,
"AFNumber_Format",
|args: &[JsValue], context: &mut Context, name: &str| {
exactly(args, 6, name)?;
let (n_dec, sep, neg) = (
int(args, 0, context)?,
int(args, 1, context)?,
int(args, 2, context)?,
);
let currency = text(args, 4, context)?;
let prepend = truthy(args, 5);
let event = keystroke_of(context);
let out =
pdfrum_script::af_number_format(&event.value, n_dec, sep, neg, ¤cy, prepend);
report(&out.effects, context);
write_event_value(formatted(out.outcome), context);
Ok(JsValue::undefined())
}
);
af!(
af_number_keystroke,
"AFNumber_Keystroke",
|args: &[JsValue], context: &mut Context, name: &str| {
at_least(args, 2, name)?;
let (n_dec, sep) = (int(args, 0, context)?, int(args, 1, context)?);
let event = keystroke_of(context);
let out = settle(
pdfrum_script::af_number_keystroke(&event, n_dec, sep),
context,
name,
)?;
report(&out.effects, context);
apply_keystroke(&out.outcome, context);
Ok(JsValue::undefined())
}
);
af!(
af_percent_format,
"AFPercent_Format",
|args: &[JsValue], context: &mut Context, name: &str| {
at_least(args, 2, name)?;
let (n_dec, sep) = (int(args, 0, context)?, int(args, 1, context)?);
let prepend = args.len() > 2 && truthy(args, 2);
let event = keystroke_of(context);
let out = settle(
pdfrum_script::af_percent_format(&event.value, n_dec, sep, prepend),
context,
name,
)?;
report(&out.effects, context);
write_event_value(formatted(out.outcome), context);
Ok(JsValue::undefined())
}
);
af!(
af_percent_keystroke,
"AFPercent_Keystroke",
|args: &[JsValue], context: &mut Context, name: &str| {
at_least(args, 2, name)?;
let sep = int(args, 1, context)?;
let event = keystroke_of(context);
let n_dec = int(args, 0, context)?;
let out = settle(
pdfrum_script::af_percent_keystroke(&event, n_dec, sep),
context,
name,
)?;
report(&out.effects, context);
apply_keystroke(&out.outcome, context);
Ok(JsValue::undefined())
}
);
af!(
af_date_format,
"AFDate_Format",
|args: &[JsValue], context: &mut Context, name: &str| {
exactly(args, 1, name)?;
let index = int(args, 0, context)?;
let now = now_ms(context);
let event = keystroke_of(context);
let out = settle(
pdfrum_script::af_date_format(&event.value, index, now),
context,
name,
)?;
report(&out.effects, context);
write_event_value(formatted(out.outcome), context);
Ok(JsValue::undefined())
}
);
af!(
af_date_format_ex,
"AFDate_FormatEx",
|args: &[JsValue], context: &mut Context, name: &str| {
exactly(args, 1, name)?;
let format = text(args, 0, context)?;
let now = now_ms(context);
let event = keystroke_of(context);
let out = settle(
pdfrum_script::af_date_format_ex(&event.value, &format, now),
context,
name,
)?;
report(&out.effects, context);
write_event_value(formatted(out.outcome), context);
Ok(JsValue::undefined())
}
);
af!(
af_date_keystroke,
"AFDate_Keystroke",
|args: &[JsValue], context: &mut Context, name: &str| {
exactly(args, 1, name)?;
let index = int(args, 0, context)?;
let now = now_ms(context);
let event = keystroke_of(context);
let out = pdfrum_script::af_date_keystroke(&event, index, now);
report(&out.effects, context);
apply_keystroke(&out.outcome, context);
Ok(JsValue::undefined())
}
);
af!(
af_date_keystroke_ex,
"AFDate_KeystrokeEx",
|args: &[JsValue], context: &mut Context, name: &str| {
if args.len() != 1 {
return Err(thrown(name, &pdfrum_script::Error::DateKeystrokeArity));
}
let format = text(args, 0, context)?;
let now = now_ms(context);
let event = keystroke_of(context);
let out = pdfrum_script::af_date_keystroke_ex(&event, &format, now);
report(&out.effects, context);
apply_keystroke(&out.outcome, context);
Ok(JsValue::undefined())
}
);
af!(
af_time_format,
"AFTime_Format",
|args: &[JsValue], context: &mut Context, name: &str| {
exactly(args, 1, name)?;
let index = int(args, 0, context)?;
let now = now_ms(context);
let event = keystroke_of(context);
let out = settle(
pdfrum_script::af_time_format(&event.value, index, now),
context,
name,
)?;
report(&out.effects, context);
write_event_value(formatted(out.outcome), context);
Ok(JsValue::undefined())
}
);
af!(
af_time_format_ex,
"AFTime_FormatEx",
|args: &[JsValue], context: &mut Context, name: &str| {
exactly(args, 1, name)?;
let format = text(args, 0, context)?;
let now = now_ms(context);
let event = keystroke_of(context);
let out = settle(
pdfrum_script::af_time_format_ex(&event.value, &format, now),
context,
name,
)?;
report(&out.effects, context);
write_event_value(formatted(out.outcome), context);
Ok(JsValue::undefined())
}
);
af!(
af_time_keystroke,
"AFTime_Keystroke",
|args: &[JsValue], context: &mut Context, name: &str| {
exactly(args, 1, name)?;
let index = int(args, 0, context)?;
let now = now_ms(context);
let event = keystroke_of(context);
let out = pdfrum_script::af_time_keystroke(&event, index, now);
report(&out.effects, context);
apply_keystroke(&out.outcome, context);
Ok(JsValue::undefined())
}
);
af!(
af_time_keystroke_ex,
"AFTime_KeystrokeEx",
|args: &[JsValue], context: &mut Context, name: &str| {
if args.len() != 1 {
return Err(thrown(name, &pdfrum_script::Error::DateKeystrokeArity));
}
let format = text(args, 0, context)?;
let now = now_ms(context);
let event = keystroke_of(context);
let out = pdfrum_script::af_time_keystroke_ex(&event, &format, now);
report(&out.effects, context);
apply_keystroke(&out.outcome, context);
Ok(JsValue::undefined())
}
);
af!(
af_special_format,
"AFSpecial_Format",
|args: &[JsValue], context: &mut Context, name: &str| {
exactly(args, 1, name)?;
let kind = int(args, 0, context)?;
let event = keystroke_of(context);
let out = pdfrum_script::af_special_format(&event.value, kind);
report(&out.effects, context);
write_event_value(formatted(out.outcome), context);
Ok(JsValue::undefined())
}
);
af!(
af_special_keystroke,
"AFSpecial_Keystroke",
|args: &[JsValue], context: &mut Context, name: &str| {
exactly(args, 1, name)?;
let kind = int(args, 0, context)?;
let event = keystroke_of(context);
let out = pdfrum_script::af_special_keystroke(&event, kind);
report(&out.effects, context);
apply_keystroke(&out.outcome, context);
Ok(JsValue::undefined())
}
);
af!(
af_special_keystroke_ex,
"AFSpecial_KeystrokeEx",
|args: &[JsValue], context: &mut Context, name: &str| {
at_least(args, 1, name)?;
let mask = text(args, 0, context)?;
let event = keystroke_of(context);
let out = pdfrum_script::af_special_keystroke_ex(&event, &mask);
report(&out.effects, context);
apply_keystroke(&out.outcome, context);
Ok(JsValue::undefined())
}
);
af!(
af_range_validate,
"AFRange_Validate",
|args: &[JsValue], context: &mut Context, name: &str| {
exactly(args, 4, name)?;
let check_min = truthy(args, 0);
let min = number(args, 1, context)?;
let check_max = truthy(args, 2);
let max = number(args, 3, context)?;
let min_label = text(args, 1, context)?;
let max_label = text(args, 3, context)?;
let event = keystroke_of(context);
let out = pdfrum_script::af_range_validate(
&event.value,
check_min,
min,
&min_label,
check_max,
max,
&max_label,
);
report(&out.effects, context);
if out.outcome == pdfrum_script::AfOutcome::Rejected {
set_event_rc(false, context);
}
Ok(JsValue::undefined())
}
);
af!(
af_merge_change,
"AFMergeChange",
|args: &[JsValue], context: &mut Context, name: &str| {
exactly(args, 1, name)?;
let event = keystroke_of(context);
Ok(JsValue::from(boa_engine::js_string!(
pdfrum_script::af_merge_change(&event)
)))
}
);
af!(
af_parse_date_ex,
"AFParseDateEx",
|args: &[JsValue], context: &mut Context, name: &str| {
exactly(args, 2, name)?;
let value = text(args, 0, context)?;
let format = text(args, 1, context)?;
let now = now_ms(context);
let millis = settle(
pdfrum_script::af_parse_date_ex(&value, &format, now),
context,
name,
)?;
Ok(JsValue::from(millis))
}
);
af!(
af_extract_nums,
"AFExtractNums",
|args: &[JsValue], context: &mut Context, name: &str| {
exactly(args, 1, name)?;
let value = text(args, 0, context)?;
let Some(parts) = pdfrum_script::af_extract_nums(&value) else {
return Ok(JsValue::from(false));
};
let array = boa_engine::object::builtins::JsArray::new(context)?;
for part in parts {
array.push(boa_engine::js_string!(part), context)?;
}
Ok(array.into())
}
);
af!(
af_make_number,
"AFMakeNumber",
|args: &[JsValue], context: &mut Context, name: &str| {
exactly(args, 1, name)?;
let value = text(args, 0, context)?;
Ok(JsValue::from(pdfrum_script::af_make_number(&value)))
}
);
af!(
af_simple,
"AFSimple",
|args: &[JsValue], context: &mut Context, name: &str| {
exactly(args, 3, name)?;
let op = text(args, 0, context)?;
let a = number(args, 1, context)?;
let b = number(args, 2, context)?;
let value = settle(pdfrum_script::af_simple(&op, a, b), context, name)?;
Ok(JsValue::from(value))
}
);
af!(
af_split_field_list,
"AFMakeArrayFromList",
|args: &[JsValue], context: &mut Context, name: &str| {
at_least(args, 1, name)?;
let value = text(args, 0, context)?;
let names = pdfrum_script::af_split_field_list(&value);
let array = boa_engine::object::builtins::JsArray::new(context)?;
for name in names {
array.push(boa_engine::js_string!(name), context)?;
}
Ok(array.into())
}
);
fn af_simple_calculate(
_this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
const NAME: &str = "AFSimple_Calculate";
exactly(args, 2, NAME)?;
let list = args.get_or_undefined(1).clone();
let is_array = list.as_object().is_some_and(|object| object.is_array());
if !is_array && !list.is_string() {
return Err(param_error(NAME));
}
let op = text(args, 0, context)?;
let names = field_name_list(&list, is_array, context)?;
let Some(host) = super::bind::host(context) else {
return Ok(JsValue::undefined());
};
let values: Vec<f64> = {
let state = host.borrow();
names
.iter()
.flat_map(|name| state.document.fields_named(name))
.map(contribution)
.collect()
};
let value = settle(
pdfrum_script::af_simple_calculate(&op, &values),
context,
NAME,
)?;
let rendered = JsValue::from(value)
.to_string(context)?
.to_std_string_lossy();
write_event_value(Some(rendered), context);
Ok(JsValue::undefined())
}
fn contribution(field: &super::model::FieldModel) -> f64 {
use super::model::FieldModelKind as Kind;
let text = match field.kind {
Kind::Text | Kind::ComboBox | Kind::CheckBox | Kind::RadioButton => field.value.as_str(),
Kind::ListBox if field.selected.len() <= 1 => field.value.as_str(),
_ => "",
};
pdfrum_script::string_to_double(text.trim())
}
fn field_name_list(list: &JsValue, is_array: bool, context: &mut Context) -> JsResult<Vec<String>> {
if !is_array {
let joined = super::bind::string_of(list, context)?;
return Ok(joined
.split(',')
.map(|name| name.trim().to_string())
.collect());
}
let Some(object) = list.as_object() else {
return Ok(Vec::new());
};
let length = object
.get(boa_engine::js_string!("length"), context)?
.to_length(context)?;
let mut names = Vec::new();
for index in 0..length {
let entry = object.get(index, context)?;
names.push(super::bind::string_of(&entry, context)?);
}
Ok(names)
}
pub(crate) type Bound = fn(&JsValue, &[JsValue], &mut Context) -> JsResult<JsValue>;
pub(crate) fn install(context: &mut Context) -> JsResult<()> {
let functions: &[(&str, Bound)] = &[
("AFNumber_Format", af_number_format),
("AFNumber_Keystroke", af_number_keystroke),
("AFPercent_Format", af_percent_format),
("AFPercent_Keystroke", af_percent_keystroke),
("AFDate_Format", af_date_format),
("AFDate_FormatEx", af_date_format_ex),
("AFDate_Keystroke", af_date_keystroke),
("AFDate_KeystrokeEx", af_date_keystroke_ex),
("AFTime_Format", af_time_format),
("AFTime_FormatEx", af_time_format_ex),
("AFTime_Keystroke", af_time_keystroke),
("AFTime_KeystrokeEx", af_time_keystroke_ex),
("AFSpecial_Format", af_special_format),
("AFSpecial_Keystroke", af_special_keystroke),
("AFSpecial_KeystrokeEx", af_special_keystroke_ex),
("AFRange_Validate", af_range_validate),
("AFMergeChange", af_merge_change),
("AFParseDateEx", af_parse_date_ex),
("AFExtractNums", af_extract_nums),
("AFMakeNumber", af_make_number),
("AFSimple", af_simple),
("AFSimple_Calculate", af_simple_calculate),
("AFMakeArrayFromList", af_split_field_list),
];
for (name, function) in functions {
context.register_global_callable(
boa_engine::js_string!(*name),
0,
NativeFunction::from_fn_ptr(*function),
)?;
}
let event = ObjectInitializer::new(context)
.property(
boa_engine::js_string!("value"),
boa_engine::js_string!(""),
Attribute::all(),
)
.property(
boa_engine::js_string!("change"),
boa_engine::js_string!(""),
Attribute::all(),
)
.property(
boa_engine::js_string!("rc"),
JsValue::from(true),
Attribute::all(),
)
.property(
boa_engine::js_string!("selStart"),
JsValue::from(0),
Attribute::all(),
)
.property(
boa_engine::js_string!("selEnd"),
JsValue::from(0),
Attribute::all(),
)
.property(
boa_engine::js_string!("willCommit"),
JsValue::from(false),
Attribute::all(),
)
.property(
boa_engine::js_string!("fieldFull"),
JsValue::from(false),
Attribute::all(),
)
.property(
boa_engine::js_string!("commitKey"),
JsValue::from(-1),
Attribute::all(),
)
.build();
context.register_global_property(boa_engine::js_string!("event"), event, Attribute::all())
}