use boa_engine::object::ObjectInitializer;
use boa_engine::property::Attribute;
use boa_engine::{Context, JsArgs, JsError, JsResult, JsValue, NativeFunction};
use super::host::{Host, HostState};
use super::transcript::{DEFAULT_ALERT_TITLE, TranscriptLine};
pub(crate) const NOT_SUPPORTED: &str = "Operation not supported.";
pub(crate) const PARAM_ERROR: &str = "Incorrect number of parameters passed to function.";
pub(crate) const INVALID_INPUT: &str = "The input value is invalid.";
pub(crate) fn qualified(name: &str, message: &str) -> JsError {
JsError::from_opaque(JsValue::from(boa_engine::js_string!(format!(
"{name}: {message}"
))))
}
fn unsupported(name: &str) -> JsError {
qualified(name, NOT_SUPPORTED)
}
pub(crate) fn param_error(name: &str) -> JsError {
qualified(name, PARAM_ERROR)
}
pub(crate) fn host(context: &Context) -> Option<Host> {
context.get_data::<Host>().cloned()
}
pub(crate) fn say(context: &Context, line: TranscriptLine) {
if let Some(host) = host(context) {
host.borrow_mut().transcript.push(line);
}
}
pub(crate) fn string_of(value: &JsValue, context: &mut Context) -> JsResult<String> {
Ok(value.to_string(context)?.to_std_string_lossy())
}
fn app_alert(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let expanded = expand_keywords(args, &["cMsg", "nIcon", "nType", "cTitle"], context)?;
let Some(message_value) = expanded.first().cloned().flatten() else {
return Err(param_error("app.alert"));
};
let message = if let Some(array) = message_value.as_object().filter(|o| o.is_array()) {
let length = array
.get(boa_engine::js_string!("length"), context)?
.to_length(context)?;
let mut parts = Vec::new();
for index in 0..length {
let element = array.get(index, context)?;
parts.push(string_of(&element, context)?);
}
format!("[{}]", parts.join(", "))
} else {
string_of(&message_value, context)?
};
let known = |index: usize| -> Option<JsValue> { expanded.get(index).cloned().flatten() };
let icon = match known(1) {
Some(value) => value.to_i32(context)?,
None => 0,
};
let button = match known(2) {
Some(value) => value.to_i32(context)?,
None => 0,
};
let title = match known(3) {
Some(value) => string_of(&value, context)?,
None => DEFAULT_ALERT_TITLE.to_string(),
};
say(
context,
TranscriptLine::Alert {
title,
message,
icon,
button,
},
);
Ok(JsValue::from(0))
}
pub(crate) fn expand_keywords(
args: &[JsValue],
keywords: &[&str],
context: &mut Context,
) -> JsResult<Vec<Option<JsValue>>> {
let mut out: Vec<Option<JsValue>> = vec![None; keywords.len()];
for (slot, value) in out.iter_mut().zip(args.iter()) {
*slot = Some(value.clone());
}
let single_object = args
.first()
.filter(|_| args.len() == 1)
.and_then(JsValue::as_object)
.filter(|object| !object.is_array());
let Some(object) = single_object else {
return Ok(out);
};
if let Some(first) = out.first_mut() {
*first = None;
}
for (index, keyword) in keywords.iter().enumerate() {
let value = object.get(boa_engine::js_string!(*keyword), context)?;
if !value.is_undefined()
&& let Some(slot) = out.get_mut(index)
{
*slot = Some(value);
}
}
Ok(out)
}
fn app_beep(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if args.len() != 1 {
return Err(param_error("app.beep"));
}
let kind = args.get_or_undefined(0).to_i32(context)?;
say(context, TranscriptLine::Beep(kind));
Ok(JsValue::undefined())
}
fn app_response(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let expanded = expand_keywords(
args,
&["cQuestion", "cTitle", "cDefault", "bPassword", "cLabel"],
context,
)?;
if expanded.first().is_none_or(Option::is_none) {
return Err(param_error("app.response"));
}
let text = |index: usize, context: &mut Context| -> JsResult<String> {
match expanded.get(index).cloned().flatten() {
Some(value) => string_of(&value, context),
None => Ok(String::new()),
}
};
let question = text(0, context)?;
let title = match expanded.get(1).cloned().flatten() {
Some(value) => string_of(&value, context)?,
None => "PDF".to_string(),
};
let default_value = text(2, context)?;
let password = expanded
.get(3)
.and_then(Clone::clone)
.is_some_and(|value| value.to_boolean());
let label = text(4, context)?;
say(
context,
TranscriptLine::Response {
question,
title,
default_value,
label,
password,
},
);
Ok(JsValue::from(boa_engine::js_string!(GOLDEN_RESPONSE)))
}
pub(crate) const GOLDEN_RESPONSE: &str = "No";
#[allow(clippy::unnecessary_wraps)]
fn app_noop(_this: &JsValue, _args: &[JsValue], _context: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::undefined())
}
macro_rules! declined {
($fn_name:ident, $acrobat:literal) => {
fn $fn_name(
_this: &JsValue,
_args: &[JsValue],
_context: &mut Context,
) -> JsResult<JsValue> {
Err(unsupported($acrobat))
}
};
}
declined!(app_exec_menu_item, "app.execMenuItem");
declined!(app_new_doc, "app.newDoc");
declined!(app_open_doc, "app.openDoc");
declined!(app_popup_menu, "app.popUpMenu");
declined!(app_popup_menu_ex, "app.popUpMenuEx");
macro_rules! set_timer {
($fn_name:ident, $kind:expr, $acrobat:literal) => {
fn $fn_name(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if args.is_empty() || args.len() > 2 {
return Err(param_error($acrobat));
}
let script = string_of(&args.get_or_undefined(0).clone(), context)?;
if script.is_empty() {
return Err(qualified($acrobat, INVALID_INPUT));
}
let interval = match args.get(1) {
Some(value) => value.clone().to_i32(context)?,
None => 1000,
};
let id = match host(context) {
Some(host) => host.borrow_mut().timers.set($kind, script, interval),
None => 0,
};
let timer = ObjectInitializer::new(context)
.property(
boa_engine::js_string!("timeOut"),
JsValue::from(id),
Attribute::all(),
)
.build();
allow_construction(&timer, context)?;
Ok(JsValue::from(timer))
}
};
}
set_timer!(
app_set_time_out,
super::timer::TimerKind::OneShot,
"app.setTimeOut"
);
set_timer!(
app_set_interval,
super::timer::TimerKind::Repeating,
"app.setInterval"
);
macro_rules! clear_timer {
($fn_name:ident, $acrobat:literal) => {
fn $fn_name(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if args.len() != 1 {
return Err(param_error($acrobat));
}
let Some(object) = args.get_or_undefined(0).as_object() else {
return Ok(JsValue::undefined());
};
let id = object.get(boa_engine::js_string!("timeOut"), context)?;
if id.is_undefined() {
return Ok(JsValue::undefined());
}
let id = id.to_i32(context)?;
if let Some(host) = host(context) {
host.borrow_mut().timers.cancel(id);
}
Ok(JsValue::undefined())
}
};
}
clear_timer!(app_clear_time_out, "app.clearTimeOut");
clear_timer!(app_clear_interval, "app.clearInterval");
fn console_println(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let text = match args.first() {
Some(value) => string_of(&value.clone(), context)?,
None => String::new(),
};
say(context, TranscriptLine::ConsolePrintln(text));
Ok(JsValue::undefined())
}
fn util_printf(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(format) = args.first().cloned() else {
return Err(param_error("util.printf"));
};
let format = string_of(&format, context)?;
let mut values = Vec::new();
for value in args.iter().skip(1) {
values.push(printf_arg(value, context)?);
}
match pdfrum_script::util_printf(&format, &values) {
Ok(text) => Ok(JsValue::from(boa_engine::js_string!(text))),
Err(error) => Err(thrown("util.printf", &error)),
}
}
fn printf_arg(value: &JsValue, context: &mut Context) -> JsResult<pdfrum_script::PrintfArg> {
if let Some(number) = value.as_number() {
return Ok(pdfrum_script::PrintfArg::Double(number));
}
Ok(pdfrum_script::PrintfArg::String(string_of(value, context)?))
}
fn util_printd(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
const NAME: &str = "util.printd";
if args.len() < 2 {
return Err(param_error(NAME));
}
let format = args.get_or_undefined(0).clone();
let date = args.get_or_undefined(1).clone();
let Some(object) = date.as_object().filter(is_date) else {
return Err(thrown(NAME, &pdfrum_script::Error::NotADate));
};
let millis = {
let time = object.get(boa_engine::js_string!("getTime"), context)?;
let Some(callable) = time.as_callable() else {
return Err(thrown(NAME, &pdfrum_script::Error::NotADate));
};
callable.call(&date, &[], context)?.to_number(context)?
};
if millis.is_nan() {
return Err(thrown(NAME, &pdfrum_script::Error::InvalidDate));
}
let millis = to_local_time(millis, context);
if let Some(style) = format.as_number() {
#[allow(clippy::cast_possible_truncation)]
let style = style as i32;
return match pdfrum_script::util_printd_style(style, millis) {
Ok(text) => Ok(JsValue::from(boa_engine::js_string!(text))),
Err(error) => Err(thrown(NAME, &error)),
};
}
if !format.is_string() {
return Err(thrown(NAME, &pdfrum_script::Error::Type));
}
if args.len() > 2 && args.get_or_undefined(2).to_boolean() {
return Err(thrown(NAME, &pdfrum_script::Error::NotSupported));
}
let format = string_of(&format, context)?;
match pdfrum_script::util_printd(&format, millis) {
Ok(text) => Ok(JsValue::from(boa_engine::js_string!(text))),
Err(error) => Err(thrown(NAME, &error)),
}
}
fn is_date(object: &boa_engine::JsObject) -> bool {
object.is::<boa_engine::builtins::date::Date>()
}
fn util_printx(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if args.len() < 2 {
return Err(param_error("util.printx"));
}
let mask = string_of(&args.get_or_undefined(0).clone(), context)?;
let source = string_of(&args.get_or_undefined(1).clone(), context)?;
Ok(JsValue::from(boa_engine::js_string!(
pdfrum_script::util_printx(&mask, &source)
)))
}
fn util_scand(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if args.len() < 2 {
return Err(param_error("util.scand"));
}
let format = string_of(&args.get_or_undefined(0).clone(), context)?;
let text = string_of(&args.get_or_undefined(1).clone(), context)?;
let now = now_ms(context);
match pdfrum_script::util_scand(&format, &text, now) {
Some(millis) => new_date(millis, context),
None => Ok(JsValue::undefined()),
}
}
fn util_byte_to_char(
_this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
if args.len() != 1 {
return Err(param_error("util.byteToChar"));
}
let code = args.get_or_undefined(0).to_i32(context)?;
let Ok(byte) = u8::try_from(code) else {
return Err(thrown("util.byteToChar", &pdfrum_script::Error::Value));
};
let text = char::from(byte).to_string();
Ok(JsValue::from(boa_engine::js_string!(text)))
}
pub(crate) fn thrown(name: &str, error: &pdfrum_script::Error) -> JsError {
qualified(name, &error.to_string())
}
fn to_local_time(millis: f64, context: &Context) -> f64 {
let offset = context.get_data::<PrintdOffset>().map_or(0, |o| o.0);
millis + f64::from(offset) * 1000.0
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct PrintdOffset(pub(crate) i32);
fn new_date(millis: f64, context: &mut Context) -> JsResult<JsValue> {
let constructor = context
.global_object()
.get(boa_engine::js_string!("Date"), context)?;
let Some(constructor) = constructor.as_constructor() else {
return Ok(JsValue::undefined());
};
constructor
.construct(&[JsValue::from(millis)], None, context)
.map(JsValue::from)
}
pub(crate) fn now_ms(context: &Context) -> f64 {
#[allow(clippy::cast_precision_loss)]
{
context.clock().system_time_millis() as f64
}
}
pub(crate) fn refuse_construction(
object: &boa_engine::JsObject,
context: &mut Context,
) -> JsResult<()> {
define_constructor(object, DYNAMIC_REFUSES, context)
}
pub(crate) fn allow_construction(
object: &boa_engine::JsObject,
context: &mut Context,
) -> JsResult<()> {
define_constructor(object, DYNAMIC_ALLOWS, context)
}
const DYNAMIC_REFUSES: &[u8] = b"(function () {\n\
throw new.target === undefined\n\
? 'illegal constructor'\n\
: 'not a dynamic object';\n\
})";
const DYNAMIC_ALLOWS: &[u8] = b"(function () {\n\
if (new.target === undefined) { throw 'illegal constructor'; }\n\
})";
fn define_constructor(
object: &boa_engine::JsObject,
source: &[u8],
context: &mut Context,
) -> JsResult<()> {
let function = context.eval(boa_engine::Source::from_bytes(source))?;
object.define_property_or_throw(
boa_engine::js_string!("constructor"),
boa_engine::property::PropertyDescriptor::builder()
.value(function)
.writable(true)
.enumerable(false)
.configurable(true),
context,
)?;
Ok(())
}
pub(crate) fn install(context: &mut Context, host: Host) -> JsResult<()> {
context.insert_data(host);
install_app(context)?;
install_console(context)?;
install_util(context)?;
super::af::install(context)?;
super::doc::install(context)?;
super::event::install(context)?;
super::color::install(context)?;
super::consts::install(context)?;
super::global::install(context)?;
refuse_static_construction(context)
}
fn refuse_static_construction(context: &mut Context) -> JsResult<()> {
let global = context.global_object();
refuse_construction(&global, context)?;
for name in [
"app",
"border",
"color",
"console",
"display",
"event",
"font",
"global",
"highlight",
"position",
"scaleHow",
"scaleWhen",
"style",
"util",
"zoomtype",
] {
let value = global.get(boa_engine::js_string!(name), context)?;
if let Some(object) = value.as_object() {
refuse_construction(&object, context)?;
}
}
Ok(())
}
pub(crate) fn native(function: super::af::Bound) -> NativeFunction {
NativeFunction::from_fn_ptr(function)
}
fn install_app(context: &mut Context) -> JsResult<()> {
let app = {
let mut init = ObjectInitializer::new(context);
init.function(native(app_alert), boa_engine::js_string!("alert"), 4)
.function(native(app_beep), boa_engine::js_string!("beep"), 1)
.function(native(app_response), boa_engine::js_string!("response"), 5)
.function(
native(app_set_time_out),
boa_engine::js_string!("setTimeOut"),
2,
)
.function(
native(app_set_interval),
boa_engine::js_string!("setInterval"),
2,
)
.function(
native(app_clear_time_out),
boa_engine::js_string!("clearTimeOut"),
1,
)
.function(
native(app_clear_interval),
boa_engine::js_string!("clearInterval"),
1,
);
init.function(
native(super::doc::app_mail_msg),
boa_engine::js_string!("mailMsg"),
6,
);
for name in [
"browseForDoc",
"execDialog",
"findComponent",
"goBack",
"goForward",
"launchURL",
"newFDF",
"openFDF",
] {
init.function(native(app_noop), boa_engine::js_string!(name), 0);
}
for (name, function) in [
("execMenuItem", app_exec_menu_item as super::af::Bound),
("newDoc", app_new_doc),
("openDoc", app_open_doc),
("popUpMenu", app_popup_menu),
("popUpMenuEx", app_popup_menu_ex),
] {
init.function(native(function), boa_engine::js_string!(name), 0);
}
init.build()
};
context.register_global_property(
boa_engine::js_string!("app"),
app.clone(),
Attribute::all(),
)?;
install_app_properties(&app, context)
}
fn install_app_properties(app: &boa_engine::JsObject, context: &mut Context) -> JsResult<()> {
let properties: [(&str, super::af::Bound, super::af::Bound); 12] = [
("formsVersion", app_forms_version, app_no_forms_version),
("language", app_language, app_no_language),
("platform", app_platform, app_no_platform),
("viewerType", app_viewer_type, app_no_viewer_type),
(
"viewerVariation",
app_viewer_variation,
app_no_viewer_variation,
),
("viewerVersion", app_viewer_version, app_no_viewer_version),
("activeDocs", app_active_docs, app_no_active_docs),
("calculate", app_get_calculate, app_set_calculate),
(
"runtimeHighlight",
app_get_runtime_highlight,
app_set_runtime_highlight,
),
("fs", app_no_fs, app_no_fs),
("fullscreen", app_no_fullscreen, app_no_fullscreen),
("media", app_no_media, app_no_media),
];
for (name, get, set) in properties {
define_accessor(app, context, name, get, set)?;
}
Ok(())
}
macro_rules! app_constant {
($fn_name:ident, $value:expr) => {
#[allow(clippy::unnecessary_wraps)]
fn $fn_name(_t: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from($value))
}
};
}
app_constant!(app_forms_version, 7);
app_constant!(app_language, boa_engine::js_string!("ENU"));
app_constant!(app_platform, boa_engine::js_string!("WIN"));
app_constant!(app_viewer_type, boa_engine::js_string!("pdfium"));
app_constant!(app_viewer_variation, boa_engine::js_string!("Full"));
app_constant!(app_viewer_version, 8);
fn accessor_function(
function: super::af::Bound,
context: &mut Context,
) -> JsResult<boa_engine::JsObject> {
let object = ObjectInitializer::new(context)
.function(native(function), boa_engine::js_string!("f"), 1)
.build();
object
.get(boa_engine::js_string!("f"), context)?
.as_object()
.ok_or_else(|| JsError::from_opaque(JsValue::undefined()))
}
pub(crate) fn define_accessor(
object: &boa_engine::JsObject,
context: &mut Context,
name: &str,
get: super::af::Bound,
set: super::af::Bound,
) -> JsResult<()> {
let getter = accessor_function(get, context)?;
let setter = accessor_function(set, context)?;
object.define_property_or_throw(
boa_engine::js_string!(name.to_string()),
boa_engine::property::PropertyDescriptor::builder()
.get(JsValue::from(getter))
.set(JsValue::from(setter))
.enumerable(true)
.configurable(true),
context,
)?;
Ok(())
}
#[allow(
clippy::unnecessary_wraps,
reason = "the bound-function signature, which every entry in the table shares"
)]
fn app_active_docs(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let global = JsValue::from(context.global_object());
Ok(JsValue::from(
boa_engine::object::builtins::JsArray::from_iter([global], context),
))
}
macro_rules! app_declined {
($fn_name:ident, $member:literal) => {
fn $fn_name(_t: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Err(unsupported(concat!("app.", $member)))
}
};
}
app_declined!(app_no_active_docs, "activeDocs");
app_declined!(app_no_forms_version, "formsVersion");
app_declined!(app_no_language, "language");
app_declined!(app_no_platform, "platform");
app_declined!(app_no_viewer_type, "viewerType");
app_declined!(app_no_viewer_variation, "viewerVariation");
app_declined!(app_no_viewer_version, "viewerVersion");
app_declined!(app_no_fs, "fs");
app_declined!(app_no_fullscreen, "fullscreen");
app_declined!(app_no_media, "media");
macro_rules! app_flag {
($get:ident, $set:ident, $slot:ident, $default:literal) => {
fn $get(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from(
host(context).map_or($default, |host| host.borrow().$slot),
))
}
fn $set(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let value = args.get_or_undefined(0).to_boolean();
if let Some(host) = host(context) {
host.borrow_mut().$slot = value;
}
Ok(JsValue::undefined())
}
};
}
app_flag!(app_get_calculate, app_set_calculate, app_calculate, true);
app_flag!(
app_get_runtime_highlight,
app_set_runtime_highlight,
app_runtime_highlight,
false
);
fn install_console(context: &mut Context) -> JsResult<()> {
let console = {
let mut init = ObjectInitializer::new(context);
init.function(
native(console_println),
boa_engine::js_string!("println"),
1,
);
for name in ["clear", "hide", "show"] {
init.function(native(app_noop), boa_engine::js_string!(name), 0);
}
init.build()
};
context.register_global_property(boa_engine::js_string!("console"), console, Attribute::all())
}
fn install_util(context: &mut Context) -> JsResult<()> {
let util = {
let mut init = ObjectInitializer::new(context);
init.function(native(util_printf), boa_engine::js_string!("printf"), 1)
.function(native(util_printd), boa_engine::js_string!("printd"), 3)
.function(native(util_printx), boa_engine::js_string!("printx"), 2)
.function(native(util_scand), boa_engine::js_string!("scand"), 2)
.function(
native(util_byte_to_char),
boa_engine::js_string!("byteToChar"),
1,
);
init.build()
};
context.register_global_property(boa_engine::js_string!("util"), util, Attribute::all())
}
pub(crate) fn new_host() -> Host {
std::rc::Rc::new(std::cell::RefCell::new(HostState::default()))
}