#![allow(
clippy::unnecessary_wraps,
reason = "every bound function has one signature — \
`fn(&JsValue, &[JsValue], &mut Context) -> JsResult<JsValue>` — \
because that is what `NativeFunction::from_fn_ptr` takes and \
what makes the tables tables. A getter that cannot fail still \
has to return a `Result`, and narrowing the ones that happen \
not to today would break the next one that grows a throw."
)]
use boa_engine::object::ObjectInitializer;
use boa_engine::property::{Attribute, PropertyDescriptor};
use boa_engine::{Context, JsArgs, JsError, JsObject, JsResult, JsValue};
use super::bind::{host, native, param_error, qualified, say, string_of};
use super::model::DocumentModel;
use super::transcript::TranscriptLine;
pub(crate) const CLASS: &str = "Document";
const READ_ONLY: &str = "Cannot assign to readonly property.";
pub(crate) const VALUE_ERROR: &str = "Incorrect parameter value.";
pub(crate) const TYPE_ERROR: &str = "Incorrect parameter type.";
pub(crate) const BAD_OBJECT: &str = "Object no longer exists.";
pub(crate) const OBJECT_TYPE: &str = "Object is of the wrong type.";
const USER_GESTURE: &str = "User gesture required.";
pub(crate) const NOT_SUPPORTED: &str = super::bind::NOT_SUPPORTED;
fn err(member: &str, message: &str) -> JsError {
qualified(&format!("{CLASS}.{member}"), message)
}
fn params(member: &str) -> JsError {
param_error(&format!("{CLASS}.{member}"))
}
fn with_model<T>(context: &Context, body: impl FnOnce(&DocumentModel) -> T) -> Option<T> {
let host = host(context)?;
let state = host.borrow();
Some(body(&state.document))
}
fn noop(_this: &JsValue, _args: &[JsValue], _context: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::undefined())
}
fn undefined_getter(_this: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::undefined())
}
fn ignoring_setter(_this: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::undefined())
}
fn info_entry(context: &Context, member: &str, key: &str) -> JsResult<JsValue> {
let Some(found) = with_model(context, |model| {
model.has_info.then(|| {
model
.info
.iter()
.find(|(name, _)| name == key)
.map_or_else(String::new, |(_, value)| value.clone())
})
}) else {
return Ok(JsValue::undefined());
};
match found {
Some(text) => Ok(JsValue::from(boa_engine::js_string!(text))),
None => Err(err(member, BAD_OBJECT)),
}
}
macro_rules! metadata {
($fn_name:ident, $member:literal, $key:literal) => {
fn $fn_name(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
info_entry(context, $member, $key)
}
};
}
metadata!(get_author, "author", "Author");
metadata!(get_title, "title", "Title");
metadata!(get_subject, "subject", "Subject");
metadata!(get_keywords, "keywords", "Keywords");
metadata!(get_creator, "creator", "Creator");
metadata!(get_producer, "producer", "Producer");
metadata!(get_creation_date, "creationDate", "CreationDate");
metadata!(get_mod_date, "modDate", "ModDate");
fn get_info(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
const FIXED: [&str; 9] = [
"Author",
"Title",
"Subject",
"Keywords",
"Creator",
"Producer",
"CreationDate",
"ModDate",
"Trapped",
];
let Some(entries) = with_model(context, |model| model.has_info.then(|| model.info.clone()))
else {
return Ok(JsValue::undefined());
};
let Some(entries) = entries else {
return Err(err("info", BAD_OBJECT));
};
let mut init = ObjectInitializer::new(context);
for key in FIXED {
let value = entries
.iter()
.find(|(name, _)| name == key)
.map_or_else(String::new, |(_, value)| value.clone());
init.property(
boa_engine::js_string!(key),
boa_engine::js_string!(value),
Attribute::all(),
);
}
for (key, value) in &entries {
if FIXED.contains(&key.as_str()) {
continue;
}
init.property(
boa_engine::js_string!(key.clone()),
boa_engine::js_string!(value.clone()),
Attribute::all(),
);
}
Ok(JsValue::from(init.build()))
}
fn get_num_pages(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from(
with_model(context, |model| model.page_count).unwrap_or(0),
))
}
fn get_num_fields(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let count = with_model(context, |model| model.fields.len()).unwrap_or(0);
Ok(JsValue::from(i32::try_from(count).unwrap_or(i32::MAX)))
}
fn get_path(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let path = with_model(context, |model| model.path.clone()).unwrap_or_default();
Ok(JsValue::from(boa_engine::js_string!(path)))
}
fn get_url(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let url = with_model(context, |model| model.url.clone()).unwrap_or_default();
Ok(JsValue::from(boa_engine::js_string!(url)))
}
fn get_document_file_name(_t: &JsValue, _a: &[JsValue], c: &mut Context) -> JsResult<JsValue> {
let path = with_model(c, |model| model.url.clone()).unwrap_or_default();
let name = path
.rfind(['/', '\\'])
.map_or(String::new(), |at| path[at + 1..].to_string());
Ok(JsValue::from(boa_engine::js_string!(name)))
}
fn get_calculate(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from(
with_model(context, |model| model.calculate).unwrap_or(true),
))
}
fn set_calculate(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let enabled = args.get_or_undefined(0).to_boolean();
if let Some(host) = host(context) {
host.borrow_mut().document.calculate = enabled;
}
Ok(JsValue::undefined())
}
fn get_filesize(_t: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from(0))
}
fn get_external(_t: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from(true))
}
fn get_icons(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(host) = host(context) else {
return Ok(JsValue::undefined());
};
let names = host.borrow().icon_names.clone();
if names.is_empty() {
return Ok(JsValue::undefined());
}
let icons: Vec<JsValue> = names.into_iter().map(|name| icon(&name, context)).collect();
Ok(JsValue::from(
boa_engine::object::builtins::JsArray::from_iter(icons, context),
))
}
const ICON_KEY: &str = "__pdfrum_icon_name";
fn icon_get_name(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(object) = this.as_object() else {
return Ok(JsValue::undefined());
};
object.get(boa_engine::js_string!(ICON_KEY), context)
}
fn icon_set_name(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let bound = this.as_object().is_some_and(|object| {
object
.has_own_property(boa_engine::js_string!(ICON_KEY), context)
.unwrap_or(false)
});
if bound {
return Err(qualified("Icon.name", READ_ONLY));
}
Ok(JsValue::undefined())
}
const ICON_PROTOTYPE: &str = "__pdfrum_icon_prototype";
pub(crate) fn icon_object(name: Option<&str>, context: &mut Context) -> JsResult<JsObject> {
let prototype = icon_prototype(context)?;
let object = JsObject::with_object_proto(context.intrinsics());
object.set_prototype(Some(prototype));
let value = match name {
Some(name) => JsValue::from(boa_engine::js_string!(name.to_string())),
None => JsValue::undefined(),
};
object.create_data_property_or_throw(boa_engine::js_string!(ICON_KEY), value, context)?;
Ok(object)
}
fn icon_prototype(context: &mut Context) -> JsResult<JsObject> {
let global = context.global_object();
let existing = global.get(boa_engine::js_string!(ICON_PROTOTYPE), context)?;
if let Some(object) = existing.as_object() {
return Ok(object);
}
let prototype = ObjectInitializer::new(context).build();
super::bind::define_accessor(&prototype, context, "name", icon_get_name, icon_set_name)?;
super::bind::allow_construction(&prototype, context)?;
let constructor = prototype.get(boa_engine::js_string!("constructor"), context)?;
if let Some(constructor) = constructor.as_object() {
constructor.define_property_or_throw(
boa_engine::js_string!("prototype"),
PropertyDescriptor::builder()
.value(prototype.clone())
.writable(false)
.enumerable(false)
.configurable(false),
context,
)?;
}
global.define_property_or_throw(
boa_engine::js_string!(ICON_PROTOTYPE),
PropertyDescriptor::builder()
.value(prototype.clone())
.writable(false)
.enumerable(false)
.configurable(false),
context,
)?;
Ok(prototype)
}
fn icon(name: &str, context: &mut Context) -> JsValue {
icon_object(Some(name), context).map_or_else(|_| JsValue::undefined(), JsValue::from)
}
macro_rules! readonly {
($fn_name:ident, $member:literal) => {
fn $fn_name(_t: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Err(err($member, READ_ONLY))
}
};
}
readonly!(set_document_file_name, "documentFileName");
readonly!(set_filesize, "filesize");
readonly!(set_icons, "icons");
readonly!(set_info, "info");
readonly!(set_num_fields, "numFields");
readonly!(set_num_pages, "numPages");
readonly!(set_path, "path");
readonly!(set_url, "URL");
fn get_page_num(_t: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::undefined())
}
fn set_page_num(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let wanted = args.get_or_undefined(0).to_i32(context)?;
let count = i32::try_from(with_model(context, |model| model.page_count).unwrap_or(0))
.unwrap_or(i32::MAX);
let target = if wanted >= count {
count - 1
} else if wanted < 0 {
0
} else {
wanted
};
say(context, TranscriptLine::GotoPage(target));
Ok(JsValue::undefined())
}
fn get_nth_field_name(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if args.len() != 1 {
return Err(params("getNthFieldName"));
}
let index = args.get_or_undefined(0).to_i32(context)?;
if index < 0 {
return Err(err("getNthFieldName", VALUE_ERROR));
}
let name = with_model(context, |model| {
usize::try_from(index)
.ok()
.and_then(|index| model.field_at(index))
.map(|field| field.name.clone())
})
.flatten();
match name {
Some(name) => Ok(JsValue::from(boa_engine::js_string!(name))),
None => Err(err("getNthFieldName", BAD_OBJECT)),
}
}
fn get_field(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if args.is_empty() {
return Err(params("getField"));
}
let asked = string_of(&args.get_or_undefined(0).clone(), context)?;
let reachable = with_model(context, |model| model.count_fields(&asked)).unwrap_or(0);
if reachable == 0 {
return Ok(JsValue::undefined());
}
let collapsed = collapse_dots_once(&asked);
let reachable = with_model(context, |model| model.count_fields(&collapsed)).unwrap_or(0);
let name = if reachable > 0 {
collapsed
} else {
parse_field_name(&collapsed).unwrap_or_default()
};
let index = with_model(context, |model| model.field_named(&name))
.flatten()
.unwrap_or(0);
super::field::build(index, &name, context).map(JsValue::from)
}
fn collapse_dots_once(name: &str) -> String {
name.replace("..", ".")
}
fn parse_field_name(name: &str) -> Option<String> {
let at = name.rfind('.')?;
let (head, suffix) = name.split_at(at);
let suffix = &suffix[1..];
let index: i32 = suffix.trim_start().trim_end().parse().unwrap_or(0);
if index == 0 && suffix.trim_end() != "0" {
return None;
}
Some(head.to_string())
}
fn calculate_now(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if let Some(host) = host(context) {
host.borrow_mut().calculate_requested = true;
}
Ok(JsValue::undefined())
}
fn reset_form(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let wanted: Option<Vec<String>> = match args.first() {
None => None,
Some(value) if value.is_undefined() => None,
Some(value) => {
let value = value.clone();
if let Some(array) = value.as_object().filter(|o| o.is_array()) {
let length = array
.get(boa_engine::js_string!("length"), context)?
.to_length(context)?;
let mut names = Vec::new();
for index in 0..length {
let element = array.get(index, context)?;
names.push(string_of(&element, context)?);
}
Some(names)
} else {
Some(vec![string_of(&value, context)?])
}
}
};
if let Some(host) = host(context) {
let mut state = host.borrow_mut();
let targets: Vec<usize> = match &wanted {
None => (0..state.document.fields.len()).collect(),
Some(asked) => asked
.iter()
.filter_map(|name| state.document.field_named(name))
.collect(),
};
for index in targets {
let Some(field) = state.document.fields.get_mut(index) else {
continue;
};
field.value.clone_from(&field.default_value);
let value = field.value.clone();
state
.field_writes
.push((u32::try_from(index).unwrap_or(u32::MAX), value));
}
}
Ok(JsValue::undefined())
}
fn get_annot(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if args.len() != 2 {
return Err(params("getAnnot"));
}
let page = args.get_or_undefined(0).to_i32(context)?;
let name = string_of(&args.get_or_undefined(1).clone(), context)?;
let found = with_model(context, |model| {
model
.annotations
.iter()
.position(|annot| i64::from(annot.page) == i64::from(page) && annot.name == name)
})
.flatten();
let Some(index) = found else {
return Err(err("getAnnot", BAD_OBJECT));
};
annot_object(index, context).map(JsValue::from)
}
fn get_annots(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let count = with_model(context, |model| model.annotations.len()).unwrap_or(0);
let mut values = Vec::with_capacity(count);
for index in 0..count {
values.push(JsValue::from(annot_object(index, context)?));
}
Ok(JsValue::from(
boa_engine::object::builtins::JsArray::from_iter(values, context),
))
}
const ANNOT_KEY: &str = "__pdfrum_annot_index";
fn annot_index(this: &JsValue, context: &mut Context) -> Option<usize> {
let object = this.as_object()?;
let value = object
.get(boa_engine::js_string!(ANNOT_KEY), context)
.ok()?;
usize::try_from(value.to_i32(context).ok()?).ok()
}
fn annot_get_name(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(index) = annot_index(this, context) else {
return Ok(JsValue::undefined());
};
let name = with_model(context, |model| {
model.annotations.get(index).map(|annot| annot.name.clone())
})
.flatten()
.unwrap_or_default();
Ok(JsValue::from(boa_engine::js_string!(name)))
}
fn annot_set_name(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(index) = annot_index(this, context) else {
return Ok(JsValue::undefined());
};
let name = string_of(&args.get_or_undefined(0).clone(), context)?;
if let Some(host) = host(context)
&& let Some(annot) = host.borrow_mut().document.annotations.get_mut(index)
{
annot.name = name;
}
Ok(JsValue::undefined())
}
fn annot_get_hidden(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(index) = annot_index(this, context) else {
return Ok(JsValue::undefined());
};
let hidden = with_model(context, |model| {
model.annotations.get(index).map(|annot| annot.hidden)
})
.flatten()
.unwrap_or(false);
Ok(JsValue::from(hidden))
}
fn annot_set_hidden(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(index) = annot_index(this, context) else {
return Ok(JsValue::undefined());
};
let hidden = args.get_or_undefined(0).to_boolean();
if let Some(host) = host(context)
&& let Some(annot) = host.borrow_mut().document.annotations.get_mut(index)
{
annot.hidden = hidden;
}
Ok(JsValue::undefined())
}
fn annot_get_type(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(index) = annot_index(this, context) else {
return Ok(JsValue::undefined());
};
let kind = with_model(context, |model| {
model.annotations.get(index).map(|annot| annot.kind.clone())
})
.flatten()
.unwrap_or_default();
Ok(JsValue::from(boa_engine::js_string!(kind)))
}
fn annot_set_type(_t: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Err(qualified("Annot.type", READ_ONLY))
}
fn annot_object(index: usize, context: &mut Context) -> JsResult<JsObject> {
let object = ObjectInitializer::new(context).build();
object.create_data_property_or_throw(
boa_engine::js_string!(ANNOT_KEY),
JsValue::from(u32::try_from(index).unwrap_or(u32::MAX)),
context,
)?;
for (name, get, set) in [
(
"name",
annot_get_name as super::af::Bound,
annot_set_name as super::af::Bound,
),
("hidden", annot_get_hidden, annot_set_hidden),
("type", annot_get_type, annot_set_type),
] {
super::bind::define_accessor(&object, context, name, get, set)?;
}
Ok(object)
}
fn goto_named_dest(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if args.len() != 1 {
return Err(params("gotoNamedDest"));
}
let name = string_of(&args.get_or_undefined(0).clone(), context)?;
let page = with_model(context, |model| {
model
.named_destinations
.iter()
.find(|(dest, _)| *dest == name)
.map(|(_, page)| *page)
})
.flatten();
if page.is_none() {
return Err(err("gotoNamedDest", BAD_OBJECT));
}
Ok(JsValue::undefined())
}
fn add_icon(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if args.len() != 2 {
return Err(params("addIcon"));
}
let name = string_of(&args.get_or_undefined(0).clone(), context)?;
let is_icon = args.get_or_undefined(1).as_object().is_some_and(|object| {
object
.has_own_property(boa_engine::js_string!(ICON_KEY), context)
.unwrap_or(false)
});
if !is_icon {
return Err(err("addIcon", TYPE_ERROR));
}
if let Some(host) = host(context) {
host.borrow_mut().icon_names.push(name);
}
Ok(JsValue::undefined())
}
fn get_icon(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if args.len() != 1 {
return Err(params("getIcon"));
}
let name = string_of(&args.get_or_undefined(0).clone(), context)?;
let known = host(context).is_some_and(|host| host.borrow().icon_names.contains(&name));
if !known {
return Err(err("getIcon", BAD_OBJECT));
}
Ok(icon(&name, context))
}
fn mail_msg(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
mail(args, context, None)
}
pub(crate) fn app_mail_msg(
_t: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
mail(args, context, Some("app.mailMsg"))
}
fn mail(args: &[JsValue], context: &mut Context, required: Option<&str>) -> JsResult<JsValue> {
let expanded = super::bind::expand_keywords(
args,
&["bUI", "cTo", "cCc", "cBcc", "cSubject", "cMsg"],
context,
)?;
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 known_ui = expanded.first().and_then(Clone::clone);
if let Some(member) = required {
let Some(ui) = known_ui.clone() else {
return Err(param_error(member));
};
if !ui.to_boolean() && expanded.get(1).and_then(Clone::clone).is_none() {
return Err(param_error(member));
}
}
let ui = known_ui.is_none_or(|value| value.to_boolean());
let to = text(1, context)?;
let cc = text(2, context)?;
let bcc = text(3, context)?;
let subject = text(4, context)?;
let body = text(5, context)?;
say(
context,
TranscriptLine::MailMsg {
ui,
to,
cc,
bcc,
subject,
body,
},
);
Ok(JsValue::undefined())
}
fn submit_form(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if args.is_empty() {
return Err(params("submitForm"));
}
if !is_user_gesture(context) {
return Err(err("submitForm", USER_GESTURE));
}
let request = submit_request(args, context)?;
let data = super::submit::serialize(&request, context);
say(
context,
TranscriptLine::SubmitForm {
url: request.url,
data,
},
);
Ok(JsValue::undefined())
}
pub(super) struct SubmitRequest {
pub(super) url: String,
pub(super) fdf: bool,
pub(super) empty: bool,
pub(super) fields: Vec<String>,
}
fn submit_request(args: &[JsValue], context: &mut Context) -> JsResult<SubmitRequest> {
let first = args.get_or_undefined(0).clone();
if first.is_string() {
return Ok(SubmitRequest {
url: string_of(&first, context)?,
fdf: args.get(1).is_none_or(JsValue::to_boolean),
empty: args.get(2).is_some_and(JsValue::to_boolean),
fields: name_list(args.get(3), context)?,
});
}
let Some(object) = first.as_object() else {
return Ok(SubmitRequest {
url: String::new(),
fdf: false,
empty: false,
fields: Vec::new(),
});
};
let url = object.get(boa_engine::js_string!("cURL"), context)?;
let url = if url.is_undefined() {
String::new()
} else {
string_of(&url, context)?
};
let fdf = object
.get(boa_engine::js_string!("bFDF"), context)?
.to_boolean();
let empty = object
.get(boa_engine::js_string!("bEmpty"), context)?
.to_boolean();
let fields = object.get(boa_engine::js_string!("aFields"), context)?;
Ok(SubmitRequest {
url,
fdf,
empty,
fields: name_list(Some(&fields), context)?,
})
}
fn name_list(value: Option<&JsValue>, context: &mut Context) -> JsResult<Vec<String>> {
let Some(object) = value.and_then(JsValue::as_object) else {
return Ok(Vec::new());
};
if !object.is_array() {
return Ok(Vec::new());
}
let length = object.get(boa_engine::js_string!("length"), context)?;
let length = length.to_length(context)?;
let mut names = Vec::new();
for index in 0..length {
let entry = object.get(index, context)?;
names.push(string_of(&entry, context)?);
}
Ok(names)
}
fn print(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if !is_user_gesture(context) {
return Err(err("print", USER_GESTURE));
}
let expanded = expand_keyword_params(
args,
&[
"bUI",
"nStart",
"nEnd",
"bSilent",
"bShrinkToFit",
"bPrintAsImage",
"bReverse",
"bAnnotations",
],
context,
)?;
let flag = |index: usize, default: bool| -> bool {
expanded
.get(index)
.and_then(Option::as_ref)
.map_or(default, JsValue::to_boolean)
};
let page = |index: usize, context: &mut Context| -> JsResult<i32> {
match expanded.get(index).and_then(Option::as_ref) {
Some(value) => value.to_i32(context),
None => Ok(0),
}
};
let ui = flag(0, true);
let start = page(1, context)?;
let end = page(2, context)?;
say(
context,
TranscriptLine::Print {
ui,
start,
end,
silent: flag(3, false),
shrink_to_fit: flag(4, false),
print_as_image: flag(5, false),
reverse: flag(6, false),
annotations: flag(7, false),
},
);
Ok(JsValue::undefined())
}
fn expand_keyword_params(
args: &[JsValue],
keywords: &[&str],
context: &mut Context,
) -> JsResult<Vec<Option<JsValue>>> {
let mut result: Vec<Option<JsValue>> = keywords
.iter()
.enumerate()
.map(|(index, _)| args.get(index).cloned())
.collect();
if args.len() != 1 {
return Ok(result);
}
let Some(object) = args.first().and_then(JsValue::as_object) else {
return Ok(result);
};
if object.is_array() {
return Ok(result);
}
if let Some(first) = result.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) = result.get_mut(index)
{
*slot = Some(value);
}
}
Ok(result)
}
fn is_user_gesture(context: &Context) -> bool {
super::bind::host(context).is_some_and(|host| host.borrow().event.kind.is_user_gesture())
}
fn remove_field(_t: &JsValue, args: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
if args.is_empty() {
return Err(params("removeField"));
}
Ok(JsValue::undefined())
}
fn word_page(member: &str, args: &[JsValue], context: &mut Context) -> JsResult<usize> {
let page = match args.first() {
Some(value) => value.clone().to_i32(context)?,
None => 0,
};
let count = i32::try_from(with_model(context, |model| model.page_count).unwrap_or(0))
.unwrap_or(i32::MAX);
if page < 0 || page >= count {
return Err(err(member, VALUE_ERROR));
}
usize::try_from(page).map_err(|_| err(member, VALUE_ERROR))
}
fn get_page_nth_word(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let page = word_page("getPageNthWord", args, context)?;
let index = match args.get(1) {
Some(value) => value.clone().to_i32(context)?,
None => 0,
};
let strip = args.get(2).is_none_or(JsValue::to_boolean);
let word = with_model(context, |model| {
model
.page_words
.get(page)
.and_then(|words| {
usize::try_from(index)
.ok()
.and_then(|at| words.get(at).or_else(|| words.last()))
})
.cloned()
.unwrap_or_default()
})
.unwrap_or_default();
let word = if strip { word.trim().to_owned() } else { word };
Ok(JsValue::from(boa_engine::js_string!(word)))
}
fn get_page_num_words(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let page = word_page("getPageNumWords", args, context)?;
let count = with_model(context, |model| {
model.page_words.get(page).map_or(0, Vec::len)
})
.unwrap_or(0);
Ok(JsValue::from(i32::try_from(count).unwrap_or(i32::MAX)))
}
macro_rules! declined {
($fn_name:ident, $member:literal) => {
fn $fn_name(_t: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Err(err($member, NOT_SUPPORTED))
}
};
}
declined!(get_print_params, "getPrintParams");
declined!(get_page_nth_word_quads, "getPageNthWordQuads");
fn define(
context: &mut Context,
name: &str,
get: super::af::Bound,
set: super::af::Bound,
) -> JsResult<()> {
let global = context.global_object();
super::bind::define_accessor(&global, context, name, get, set)
}
#[allow(
clippy::too_many_lines,
reason = "the function is the table; see the doc comment"
)]
pub(crate) fn install(context: &mut Context) -> JsResult<()> {
let global = context.global_object();
global.define_property_or_throw(
boa_engine::JsSymbol::to_string_tag(),
PropertyDescriptor::builder()
.value(boa_engine::js_string!("global"))
.writable(false)
.enumerable(false)
.configurable(true),
context,
)?;
for name in [
"ADBE",
"bookmarkRoot",
"Collab",
"layout",
"media",
"mouseX",
"mouseY",
"pageWindowRect",
"zoom",
"zoomType",
] {
define(context, name, undefined_getter, ignoring_setter)?;
}
define(context, "delay", get_delay, set_delay)?;
define(context, "dirty", get_dirty, set_dirty)?;
let metadata: [(&str, super::af::Bound); 8] = [
("author", get_author),
("title", get_title),
("subject", get_subject),
("keywords", get_keywords),
("creator", get_creator),
("producer", get_producer),
("creationDate", get_creation_date),
("modDate", get_mod_date),
];
for (name, getter) in metadata {
define(context, name, getter, ignoring_setter)?;
}
let read_only: [(&str, super::af::Bound, super::af::Bound); 8] = [
(
"documentFileName",
get_document_file_name,
set_document_file_name,
),
("filesize", get_filesize, set_filesize),
("icons", get_icons, set_icons),
("info", get_info, set_info),
("numFields", get_num_fields, set_num_fields),
("numPages", get_num_pages, set_num_pages),
("path", get_path, set_path),
("URL", get_url, set_url),
];
for (name, getter, setter) in read_only {
define(context, name, getter, setter)?;
}
define(context, "baseURL", get_base_url, set_base_url)?;
define(context, "calculate", get_calculate, set_calculate)?;
define(context, "external", get_external, ignoring_setter)?;
define(context, "pageNum", get_page_num, set_page_num)?;
for name in [
"addAnnot",
"addField",
"addLink",
"closeDoc",
"createDataObject",
"deletePages",
"exportAsFDF",
"exportAsText",
"exportAsXFDF",
"extractPages",
"getAnnot3D",
"getAnnots3D",
"getLinks",
"getOCGs",
"getPageBox",
"getURL",
"importAnFDF",
"importAnXFDF",
"importTextData",
"insertPages",
"removeIcon",
"replacePages",
"saveAs",
"syncAnnotScan",
] {
context.register_global_builtin_callable(boa_engine::js_string!(name), 0, native(noop))?;
}
let methods: [(&str, usize, super::af::Bound); 15] = [
("addIcon", 2, add_icon),
("calculateNow", 0, calculate_now),
("getAnnot", 2, get_annot),
("getAnnots", 0, get_annots),
("getField", 1, get_field),
("getIcon", 1, get_icon),
("getNthFieldName", 1, get_nth_field_name),
("getPrintParams", 0, get_print_params),
("getPageNthWordQuads", 0, get_page_nth_word_quads),
("gotoNamedDest", 1, goto_named_dest),
("mailDoc", 6, mail_msg),
("mailForm", 6, mail_msg),
("print", 8, print),
("removeField", 1, remove_field),
("resetForm", 1, reset_form),
];
for (name, length, function) in methods {
context.register_global_builtin_callable(
boa_engine::js_string!(name),
length,
native(function),
)?;
}
context.register_global_builtin_callable(
boa_engine::js_string!("submitForm"),
1,
native(submit_form),
)?;
context.register_global_builtin_callable(
boa_engine::js_string!("getPageNthWord"),
3,
native(get_page_nth_word),
)?;
context.register_global_builtin_callable(
boa_engine::js_string!("getPageNumWords"),
1,
native(get_page_num_words),
)?;
Ok(())
}
macro_rules! flag {
($get:ident, $set:ident, $slot:ident) => {
fn $get(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from(
host(context).is_some_and(|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())
}
};
}
flag!(get_delay, set_delay_flag, delay);
flag!(get_dirty, set_dirty, dirty);
fn set_delay(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let delay = args.get_or_undefined(0).to_boolean();
set_delay_flag(this, args, context)?;
let Some(host) = host(context) else {
return Ok(JsValue::undefined());
};
let mut state = host.borrow_mut();
let queued = std::mem::take(&mut state.delayed_writes);
if delay {
return Ok(JsValue::undefined());
}
for (index, offered) in queued {
let Ok(at) = usize::try_from(index) else {
continue;
};
let Some(field) = state.document.fields.get_mut(at) else {
continue;
};
let accepted = super::field::apply_value(field, &offered);
state.field_writes.push((index, accepted));
}
Ok(JsValue::undefined())
}
fn get_base_url(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let text = host(context).map(|host| host.borrow().base_url.clone());
Ok(JsValue::from(boa_engine::js_string!(
text.unwrap_or_default()
)))
}
fn set_base_url(_t: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let text = string_of(&args.get_or_undefined(0).clone(), context)?;
if let Some(host) = host(context) {
host.borrow_mut().base_url = text;
}
Ok(JsValue::undefined())
}