#![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::{Context, JsArgs, JsError, JsObject, JsResult, JsValue};
use super::bind::{host, native, param_error, qualified, string_of};
use super::doc::{BAD_OBJECT, NOT_SUPPORTED, OBJECT_TYPE, VALUE_ERROR};
use super::model::{FieldModel, FieldModelKind};
const CLASS: &str = "Field";
const INDEX_KEY: &str = "__pdfrum_field_index";
const NAME_KEY: &str = "__pdfrum_field_name";
fn err(member: &str, message: &str) -> JsError {
qualified(&format!("{CLASS}.{member}"), message)
}
fn params(member: &str) -> JsError {
param_error(&format!("{CLASS}.{member}"))
}
fn index_of(this: &JsValue, context: &mut Context) -> Option<usize> {
let object = this.as_object()?;
let value = object
.get(boa_engine::js_string!(INDEX_KEY), context)
.ok()?;
usize::try_from(value.to_i32(context).ok()?).ok()
}
fn with_field<T>(
this: &JsValue,
context: &mut Context,
body: impl FnOnce(&FieldModel) -> T,
) -> Option<T> {
let index = index_of(this, context)?;
let host = host(context)?;
let state = host.borrow();
state.document.field_at(index).map(body)
}
fn read<T>(
this: &JsValue,
context: &mut Context,
member: &str,
body: impl FnOnce(&FieldModel) -> T,
) -> JsResult<T> {
with_field(this, context, body).ok_or_else(|| err(member, BAD_OBJECT))
}
fn ignoring_setter(_this: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::undefined())
}
fn noop(_this: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::undefined())
}
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!(throw_default_style, "defaultStyle");
declined!(throw_doc, "doc");
declined!(throw_name, "name");
declined!(throw_num_items, "numItems");
declined!(throw_type, "type");
declined!(throw_value_as_string, "valueAsString");
declined!(method_button_set_caption, "buttonSetCaption");
declined!(method_button_set_icon, "buttonSetIcon");
declined!(method_get_lock, "getLock");
declined!(method_set_lock, "setLock");
declined!(
method_signature_get_modifications,
"signatureGetModifications"
);
declined!(method_signature_get_seed_value, "signatureGetSeedValue");
declined!(method_signature_info, "signatureInfo");
declined!(method_signature_set_seed_value, "signatureSetSeedValue");
declined!(method_signature_sign, "signatureSign");
declined!(method_signature_validate, "signatureValidate");
fn throw_page(_t: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Err(err("page", "Cannot assign to readonly property."))
}
fn get_value(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let (kind, value, selected, options) = read(this, context, "value", |field| {
(
field.kind,
field.value.clone(),
field.selected.clone(),
field.options.clone(),
)
})?;
if kind == FieldModelKind::Button {
return Err(err("value", "Object is of the wrong type."));
}
if kind == FieldModelKind::ListBox && selected.len() > 1 {
let values: Vec<JsValue> = selected
.iter()
.filter_map(|index| options.get(usize::try_from(*index).ok()?))
.map(|(export, label)| {
let text = if export.is_empty() { label } else { export };
JsValue::from(boa_engine::js_string!(text.clone()))
})
.collect();
return Ok(JsValue::from(
boa_engine::object::builtins::JsArray::from_iter(values, context),
));
}
Ok(maybe_number(&value, context))
}
fn maybe_number(value: &str, context: &mut Context) -> JsValue {
if value.is_empty() {
return JsValue::from(boa_engine::js_string!(""));
}
let text = JsValue::from(boa_engine::js_string!(value.to_string()));
let Ok(number) = text.to_number(context) else {
return text;
};
if number.is_nan() && value != "NaN" {
return text;
}
JsValue::from(number)
}
fn set_value(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(index) = index_of(this, context) else {
return Err(err("value", BAD_OBJECT));
};
let value = args.get_or_undefined(0).clone();
let offered: Vec<String> = 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 out = Vec::new();
for at in 0..length {
let element = array.get(at, context)?;
out.push(string_of(&element, context)?);
}
out
} else {
vec![string_of(&value, context)?]
};
if offered.is_empty() {
return Ok(JsValue::undefined());
}
let delayed = this
.as_object()
.and_then(|object| object.get(boa_engine::js_string!(DELAY_KEY), context).ok())
.is_some_and(|value| value.to_boolean());
if delayed {
if let Some(host) = host(context) {
host.borrow_mut()
.delayed_writes
.push((u32::try_from(index).unwrap_or(u32::MAX), offered));
}
return Ok(JsValue::undefined());
}
if let Some(host) = host(context) {
let mut state = host.borrow_mut();
let Some(field) = state.document.fields.get_mut(index) else {
return Err(err("value", BAD_OBJECT));
};
let accepted = apply_value(field, &offered);
state
.field_writes
.push((u32::try_from(index).unwrap_or(u32::MAX), accepted));
}
Ok(JsValue::undefined())
}
pub(crate) fn apply_value(field: &mut FieldModel, offered: &[String]) -> String {
let first = offered.first().cloned().unwrap_or_default();
if field.kind.is_toggle() {
let mut hit = false;
for (at, export) in field.export_values.iter().enumerate() {
let matched = *export == first;
if let Some(checked) = field.checked.get_mut(at) {
*checked = matched;
}
if matched {
hit = true;
break;
}
}
let value = if hit { first } else { "Off".to_string() };
field.value.clone_from(&value);
if field.kind == FieldModelKind::CheckBox {
field.value_as_string = Some(if hit { "Yes" } else { "Off" }.to_string());
}
return value;
}
if !field.kind.is_choice() {
field.value.clone_from(&first);
return first;
}
let option_of = |field: &FieldModel, text: &str| -> Option<usize> {
field.options.iter().position(|(export, _)| export == text)
};
if field.kind == FieldModelKind::ComboBox {
field.value.clone_from(&first);
return first;
}
field.selected.clear();
for text in offered {
let Some(at) = option_of(field, text) else {
continue;
};
let at = u32::try_from(at).unwrap_or(u32::MAX);
if field.flags.multiple_selection {
if !field.selected.contains(&at) {
field.selected.push(at);
}
} else {
field.selected = vec![at];
}
}
let value = field
.selected
.first()
.and_then(|at| field.options.get(usize::try_from(*at).ok()?))
.map(|(export, label)| {
if export.is_empty() {
label.clone()
} else {
export.clone()
}
})
.unwrap_or_default();
field.value.clone_from(&value);
value
}
fn get_value_as_string(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let text = read(this, context, "valueAsString", |field| {
field
.value_as_string
.clone()
.unwrap_or_else(|| field.value.clone())
})?;
Ok(JsValue::from(boa_engine::js_string!(text)))
}
fn get_name(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(object) = this.as_object() else {
return Err(err("name", BAD_OBJECT));
};
object.get(boa_engine::js_string!(NAME_KEY), context)
}
fn get_type(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let kind = read(this, context, "type", |field| field.kind)?;
Ok(JsValue::from(boa_engine::js_string!(kind.as_str())))
}
fn get_doc(_this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from(context.global_object()))
}
fn get_default_value(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let (kind, value) = read(this, context, "defaultValue", |field| {
(field.kind, field.default_value.clone())
})?;
if matches!(kind, FieldModelKind::Button | FieldModelKind::Signature) {
return Err(err("defaultValue", "Object is of the wrong type."));
}
Ok(JsValue::from(boa_engine::js_string!(value)))
}
fn get_display(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let display = read(this, context, "display", |field| field.display)?;
Ok(JsValue::from(display))
}
fn set_display(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let wanted = args.get_or_undefined(0).to_i32(context)?;
let Ok(wanted) = u32::try_from(wanted) else {
return Ok(JsValue::undefined());
};
if wanted > 3 {
return Ok(JsValue::undefined());
}
write_field(this, context, |field| field.display = wanted);
Ok(JsValue::undefined())
}
fn get_hidden(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let display = read(this, context, "hidden", |field| field.display)?;
Ok(JsValue::from(display == 1))
}
fn set_hidden(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let hidden = args.get_or_undefined(0).to_boolean();
write_field(this, context, |field| {
field.display = u32::from(hidden);
});
Ok(JsValue::undefined())
}
fn get_readonly(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let flag = read(this, context, "readonly", |field| field.flags.read_only)?;
Ok(JsValue::from(flag))
}
fn set_readonly(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let readonly = args.get_or_undefined(0).to_boolean();
write_field(this, context, |field| field.flags.read_only = readonly);
Ok(JsValue::undefined())
}
fn get_rect(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let rect = read(this, context, "rect", |field| field.rect)?;
let values: Vec<JsValue> = rect
.iter()
.map(|side| JsValue::from(side.trunc()))
.collect();
Ok(JsValue::from(
boa_engine::object::builtins::JsArray::from_iter(values, context),
))
}
fn set_rect(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let value = args.get_or_undefined(0).clone();
let Some(array) = value.as_object().filter(|o| o.is_array()) else {
return Err(err("rect", VALUE_ERROR));
};
let length = array
.get(boa_engine::js_string!("length"), context)?
.to_length(context)?;
if length < 4 {
return Err(err("rect", VALUE_ERROR));
}
let mut sides = [0.0_f64; 4];
for (index, side) in sides.iter_mut().enumerate() {
*side = array.get(index as u64, context)?.to_number(context)?;
}
write_field(this, context, |field| {
let (left, right) = (sides[0].min(sides[2]), sides[0].max(sides[2]));
let (bottom, top) = (sides[1].min(sides[3]), sides[1].max(sides[3]));
field.rect = [left, top, right, bottom];
});
Ok(JsValue::undefined())
}
fn get_page(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let pages = read(this, context, "page", |field| field.pages.clone())?;
if pages.is_empty() {
return Ok(JsValue::from(-1));
}
let array = boa_engine::object::builtins::JsArray::new(context)?;
for (index, page) in pages.iter().enumerate() {
let index = u64::try_from(index).unwrap_or(u64::MAX);
array.set(index, JsValue::from(*page), false, context)?;
}
Ok(JsValue::from(array))
}
fn get_num_items(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let count = read(this, context, "numItems", |field| field.options.len())?;
Ok(JsValue::from(i32::try_from(count).unwrap_or(i32::MAX)))
}
fn get_current_value_indices(this: &JsValue, _a: &[JsValue], c: &mut Context) -> JsResult<JsValue> {
let selected = read(this, c, "currentValueIndices", |field| {
field.selected.clone()
})?;
match selected.len() {
0 => Ok(JsValue::from(-1)),
1 => Ok(selected
.first()
.map_or_else(JsValue::undefined, |at| JsValue::from(*at))),
_ => {
let values: Vec<JsValue> = selected.iter().map(|index| JsValue::from(*index)).collect();
Ok(JsValue::from(
boa_engine::object::builtins::JsArray::from_iter(values, c),
))
}
}
}
fn set_current_value_indices(
this: &JsValue,
args: &[JsValue],
c: &mut Context,
) -> JsResult<JsValue> {
let value = args.get_or_undefined(0).clone();
let mut wanted: Vec<u32> = Vec::new();
if let Some(array) = value.as_object().filter(|o| o.is_array()) {
let length = array
.get(boa_engine::js_string!("length"), c)?
.to_length(c)?;
for index in 0..length {
let element = array.get(index, c)?;
if let Ok(number) = element.to_i32(c)
&& let Ok(number) = u32::try_from(number)
{
wanted.push(number);
}
}
} else if let Ok(number) = value.to_i32(c)
&& let Ok(number) = u32::try_from(number)
{
wanted.push(number);
}
write_field(this, c, |field| {
if !field.flags.multiple_selection {
wanted.truncate(1);
}
field.selected = wanted;
});
Ok(JsValue::undefined())
}
fn get_item_at(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let (kind, options) = read(this, context, "getItemAt", |field| {
(field.kind, field.options.clone())
})?;
if !kind.is_choice() {
return Err(err("getItemAt", "Object is of the wrong type."));
}
let wanted = match args.first() {
None => -1,
Some(value) => {
let number = value.clone().to_number(context)?;
if number.is_nan() {
0
} else {
value.clone().to_i32(context)?
}
}
};
let count = i32::try_from(options.len()).unwrap_or(i32::MAX);
let index = if wanted == -1 || wanted > count {
count - 1
} else {
wanted
};
let Some(option) = usize::try_from(index).ok().and_then(|at| options.get(at)) else {
return Ok(JsValue::from(boa_engine::js_string!("")));
};
let export = args.get(1).is_none_or(JsValue::to_boolean);
let (value, label) = option;
let text = if export {
if value.is_empty() { label } else { value }
} else {
label
};
Ok(JsValue::from(boa_engine::js_string!(text.clone())))
}
fn check_this_box(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if args.is_empty() {
return Err(params("checkThisBox"));
}
let widget = args.get_or_undefined(0).to_i32(context)?;
let (kind, controls) = read(this, context, "checkThisBox", |field| {
(field.kind, field.checked.len())
})?;
if !kind.is_toggle() {
return Err(err("checkThisBox", "Object is of the wrong type."));
}
let Some(widget) = usize::try_from(widget).ok().filter(|at| *at < controls) else {
return Err(err("checkThisBox", VALUE_ERROR));
};
let check = args.get(1).is_none_or(JsValue::to_boolean);
write_field(this, context, |field| {
if let Some(slot) = field.checked.get_mut(widget) {
*slot = check;
}
});
Ok(JsValue::undefined())
}
fn is_box_checked(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let checked = read(this, context, "isBoxChecked", |field| field.checked.clone())?;
let widget = match args.first() {
Some(value) => value.clone().to_i32(context)?,
None => return Err(err("isBoxChecked", VALUE_ERROR)),
};
let Some(widget) = usize::try_from(widget).ok() else {
return Err(err("isBoxChecked", VALUE_ERROR));
};
let Some(state) = checked.get(widget) else {
return Err(err("isBoxChecked", VALUE_ERROR));
};
Ok(JsValue::from(*state))
}
fn is_default_checked(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let checked = read(this, context, "isDefaultChecked", |field| {
field.default_checked.clone()
})?;
let widget = match args.first() {
Some(value) => value.clone().to_i32(context)?,
None => return Err(err("isDefaultChecked", VALUE_ERROR)),
};
let Some(state) = usize::try_from(widget).ok().and_then(|at| checked.get(at)) else {
return Err(err("isDefaultChecked", VALUE_ERROR));
};
Ok(JsValue::from(*state))
}
fn default_is_checked(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
if args.is_empty() {
return Err(params("defaultIsChecked"));
}
let (kind, controls) = read(this, context, "defaultIsChecked", |field| {
(field.kind, field.default_checked.len())
})?;
let widget = args.get_or_undefined(0).to_i32(context)?;
if usize::try_from(widget).ok().is_none_or(|at| at >= controls) {
return Err(err("defaultIsChecked", VALUE_ERROR));
}
Ok(JsValue::from(kind.is_toggle()))
}
fn button_get_caption(
this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let (kind, captions) = read(this, context, "buttonGetCaption", |field| {
(field.kind, field.captions.clone())
})?;
if kind != FieldModelKind::Button {
return Err(err("buttonGetCaption", "Object is of the wrong type."));
}
let face = match args.first() {
Some(value) => value.clone().to_i32(context)?,
None => 0,
};
let Some(caption) = usize::try_from(face).ok().and_then(|at| captions.get(at)) else {
return Err(err("buttonGetCaption", VALUE_ERROR));
};
Ok(JsValue::from(boa_engine::js_string!(caption.clone())))
}
fn button_get_icon(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let kind = read(this, context, "buttonGetIcon", |field| field.kind)?;
if kind != FieldModelKind::Button {
return Err(err("buttonGetIcon", "Object is of the wrong type."));
}
let face = match args.first() {
Some(value) => value.clone().to_i32(context)?,
None => 0,
};
if !(0..=2).contains(&face) {
return Err(err("buttonGetIcon", VALUE_ERROR));
}
super::doc::icon_object(None, context).map(JsValue::from)
}
fn browse_for_file(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ok = read(this, context, "browseForFileToSubmit", |field| {
field.kind == FieldModelKind::Text && field.flags.file_select
})?;
if !ok {
return Err(err("browseForFileToSubmit", "Object is of the wrong type."));
}
Ok(JsValue::undefined())
}
fn set_focus(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(index) = index_of(this, context) else {
return Ok(JsValue::undefined());
};
if let Some(host) = host(context) {
host.borrow_mut().focus_requested = Some(u32::try_from(index).unwrap_or(u32::MAX));
}
Ok(JsValue::undefined())
}
fn get_array(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(index) = index_of(this, context) else {
return Err(err("getArray", BAD_OBJECT));
};
let Some(host) = host(context) else {
return Err(err("getArray", BAD_OBJECT));
};
let prefix = {
let Some(object) = this.as_object() else {
return Err(err("getArray", BAD_OBJECT));
};
let name = object.get(boa_engine::js_string!(NAME_KEY), context)?;
string_of(&name, context)?
};
let _ = index;
let mut children: Vec<(String, usize)> = {
let state = host.borrow();
state
.document
.fields
.iter()
.enumerate()
.filter(|(_, field)| {
prefix.is_empty()
|| field.name == prefix
|| field
.name
.strip_prefix(prefix.as_str())
.is_some_and(|rest| rest.starts_with('.'))
})
.map(|(at, field)| (field.name.clone(), at))
.collect()
};
children.sort_by(|left, right| left.0.cmp(&right.0));
let mut values = Vec::with_capacity(children.len());
for (name, at) in children {
values.push(JsValue::from(build(at, &name, context)?));
}
Ok(JsValue::from(
boa_engine::object::builtins::JsArray::from_iter(values, context),
))
}
fn write_field(this: &JsValue, context: &mut Context, body: impl FnOnce(&mut FieldModel)) {
let Some(index) = index_of(this, context) else {
return;
};
let Some(host) = host(context) else { return };
let mut state = host.borrow_mut();
if let Some(field) = state.document.fields.get_mut(index) {
body(field);
}
}
macro_rules! flag {
($fn_name:ident, $member:literal, $read:expr) => {
fn $fn_name(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let flag = read(this, context, $member, $read)?;
Ok(JsValue::from(flag))
}
};
}
flag!(get_comb, "comb", |f: &FieldModel| f.flags.comb);
flag!(get_multiline, "multiline", |f: &FieldModel| f
.flags
.multiline);
flag!(get_password, "password", |f: &FieldModel| f.flags.password);
flag!(get_rich_text, "richText", |f: &FieldModel| f
.flags
.rich_text);
flag!(get_required, "required", |f: &FieldModel| f.flags.required);
flag!(get_do_not_scroll, "doNotScroll", |f: &FieldModel| f
.flags
.do_not_scroll);
flag!(
get_do_not_spell_check,
"doNotSpellCheck",
|f: &FieldModel| f.flags.do_not_spell_check
);
flag!(get_file_select, "fileSelect", |f: &FieldModel| f
.flags
.file_select);
flag!(get_editable, "editable", |f: &FieldModel| f.flags.editable);
flag!(
get_multiple_selection,
"multipleSelection",
|f: &FieldModel| f.flags.multiple_selection
);
fn get_user_name(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let name = read(this, context, "userName", |field| field.user_name.clone())?;
Ok(JsValue::from(boa_engine::js_string!(name)))
}
fn set_export_values(_t: &JsValue, args: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
let is_array = args
.first()
.and_then(JsValue::as_object)
.is_some_and(|object| object.is_array());
if !is_array {
return Err(err("exportValues", BAD_OBJECT));
}
Ok(JsValue::undefined())
}
fn get_export_values(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let (kind, values) = read(this, context, "exportValues", |field| {
(field.kind, field.export_values.clone())
})?;
if !kind.is_toggle() {
return Err(err("exportValues", "Object is of the wrong type."));
}
let values: Vec<JsValue> = values
.into_iter()
.map(|value| JsValue::from(boa_engine::js_string!(value)))
.collect();
Ok(JsValue::from(
boa_engine::object::builtins::JsArray::from_iter(values, context),
))
}
macro_rules! fixed {
($fn_name:ident, $value:expr) => {
fn $fn_name(_t: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from($value))
}
};
}
fixed!(get_calc_order_index, -1);
fixed!(get_char_limit, 0);
fixed!(get_text_size, 0);
fixed!(get_rotation, 0);
const LINE_WIDTH_KEY: &str = "__pdfrum_field_line_width";
const PRINT_KEY: &str = "__pdfrum_field_print";
macro_rules! per_object {
($get:ident, $set:ident, $key:ident, $default:expr, $coerce:ident) => {
fn $get(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(object) = this.as_object() else {
return Ok(JsValue::from($default));
};
object.get(boa_engine::js_string!($key), context)
}
fn $set(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let value = args.get_or_undefined(0).clone().$coerce(context)?;
if let Some(object) = this.as_object() {
object.set(
boa_engine::js_string!($key),
JsValue::from(value),
false,
context,
)?;
}
Ok(JsValue::undefined())
}
};
}
per_object!(get_line_width, set_line_width, LINE_WIDTH_KEY, 1, to_i32);
fn get_print(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(object) = this.as_object() else {
return Ok(JsValue::from(true));
};
object.get(boa_engine::js_string!(PRINT_KEY), context)
}
fn set_print(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let value = args.get_or_undefined(0).to_boolean();
if let Some(object) = this.as_object() {
object.set(
boa_engine::js_string!(PRINT_KEY),
JsValue::from(value),
false,
context,
)?;
}
Ok(JsValue::undefined())
}
fn get_button_position(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let position = read(this, context, "buttonPosition", |field| {
field.button_position
})?;
Ok(JsValue::from(position))
}
fixed!(get_button_align_x, 0);
fixed!(get_button_align_y, 0);
fixed!(get_button_fit_bounds, false);
fixed!(get_button_scale_how, false);
fixed!(get_button_scale_when, 0);
fixed!(get_radios_in_unison, false);
macro_rules! fixed_string {
($fn_name:ident, $value:literal) => {
fn $fn_name(_t: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from(boa_engine::js_string!($value)))
}
};
}
fixed_string!(get_alignment, "left");
fixed_string!(get_highlight, "invert");
fn get_style(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let kind = read(this, context, "style", |field| field.kind)?;
if !kind.is_toggle() {
return Err(err("style", OBJECT_TYPE));
}
let caption = read(this, context, "style", |field| field.captions[0].clone())?;
let selector = caption.chars().next().unwrap_or(
if kind == super::model::FieldModelKind::RadioButton {
'l'
} else {
'4'
},
);
let style = match selector {
'l' => "circle",
'8' => "cross",
'u' => "diamond",
'n' => "square",
'H' => "star",
_ => "check",
};
Ok(JsValue::from(boa_engine::js_string!(style)))
}
fixed_string!(get_text_font, "Helv");
fn get_color(_t: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::from(
boa_engine::object::builtins::JsArray::from_iter(
[JsValue::from(boa_engine::js_string!("T"))],
context,
),
))
}
fn get_undefined(_t: &JsValue, _a: &[JsValue], _c: &mut Context) -> JsResult<JsValue> {
Ok(JsValue::undefined())
}
const BORDER_STYLES: [&str; 5] = ["solid", "dashed", "beveled", "inset", "underline"];
fn get_border_style(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(object) = this.as_object() else {
return Ok(JsValue::from(boa_engine::js_string!("solid")));
};
object.get(boa_engine::js_string!(BORDER_KEY), context)
}
fn set_border_style(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let text = string_of(&args.get_or_undefined(0).clone(), context)?;
if !BORDER_STYLES.contains(&text.as_str()) {
return Ok(JsValue::undefined());
}
if let Some(object) = this.as_object() {
object.set(
boa_engine::js_string!(BORDER_KEY),
JsValue::from(boa_engine::js_string!(text)),
false,
context,
)?;
}
Ok(JsValue::undefined())
}
const BORDER_KEY: &str = "__pdfrum_field_border";
const DELAY_KEY: &str = "__pdfrum_field_delay";
fn get_delay(this: &JsValue, _a: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let Some(object) = this.as_object() else {
return Ok(JsValue::from(false));
};
object.get(boa_engine::js_string!(DELAY_KEY), context)
}
fn set_delay(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let delay = args.get_or_undefined(0).to_boolean();
let Some(object) = this.as_object() else {
return Ok(JsValue::undefined());
};
object.set(
boa_engine::js_string!(DELAY_KEY),
JsValue::from(delay),
false,
context,
)?;
if delay {
return Ok(JsValue::undefined());
}
let Some(index) = index_of(this, context) else {
return Ok(JsValue::undefined());
};
let index = u32::try_from(index).unwrap_or(u32::MAX);
let Some(host) = host(context) else {
return Ok(JsValue::undefined());
};
let mut state = host.borrow_mut();
let mut mine = Vec::new();
state.delayed_writes.retain(|(at, offered)| {
if *at == index {
mine.push(offered.clone());
return false;
}
true
});
for offered in mine {
let Ok(at) = usize::try_from(index) else {
continue;
};
let Some(field) = state.document.fields.get_mut(at) else {
continue;
};
let accepted = apply_value(field, &offered);
state.field_writes.push((index, accepted));
}
Ok(JsValue::undefined())
}
fn define(
object: &JsObject,
context: &mut Context,
name: &str,
get: super::af::Bound,
set: super::af::Bound,
) -> JsResult<()> {
super::bind::define_accessor(object, context, name, get, set)
}
#[allow(
clippy::too_many_lines,
reason = "the function is the table; see the doc comment"
)]
pub(crate) fn build(index: usize, name: &str, context: &mut Context) -> JsResult<JsObject> {
let object = ObjectInitializer::new(context).build();
object.create_data_property_or_throw(
boa_engine::js_string!(INDEX_KEY),
JsValue::from(u32::try_from(index).unwrap_or(u32::MAX)),
context,
)?;
object.create_data_property_or_throw(
boa_engine::js_string!(NAME_KEY),
JsValue::from(boa_engine::js_string!(name.to_string())),
context,
)?;
object.create_data_property_or_throw(
boa_engine::js_string!(DELAY_KEY),
JsValue::from(false),
context,
)?;
object.create_data_property_or_throw(
boa_engine::js_string!(BORDER_KEY),
JsValue::from(boa_engine::js_string!("solid")),
context,
)?;
object.create_data_property_or_throw(
boa_engine::js_string!(LINE_WIDTH_KEY),
JsValue::from(1),
context,
)?;
object.create_data_property_or_throw(
boa_engine::js_string!(PRINT_KEY),
JsValue::from(true),
context,
)?;
let writable: [(&str, super::af::Bound, super::af::Bound); 11] = [
("value", get_value, set_value),
("display", get_display, set_display),
("hidden", get_hidden, set_hidden),
("readonly", get_readonly, set_readonly),
("rect", get_rect, set_rect),
(
"currentValueIndices",
get_current_value_indices,
set_current_value_indices,
),
("delay", get_delay, set_delay),
("borderStyle", get_border_style, set_border_style),
("lineWidth", get_line_width, set_line_width),
("print", get_print, set_print),
("exportValues", get_export_values, set_export_values),
];
for (name, getter, setter) in writable {
define(&object, context, name, getter, setter)?;
}
let discarding: [(&str, super::af::Bound); 27] = [
("alignment", get_alignment),
("buttonAlignX", get_button_align_x),
("buttonAlignY", get_button_align_y),
("buttonFitBounds", get_button_fit_bounds),
("buttonPosition", get_button_position),
("buttonScaleHow", get_button_scale_how),
("buttonScaleWhen", get_button_scale_when),
("calcOrderIndex", get_calc_order_index),
("charLimit", get_char_limit),
("comb", get_comb),
("defaultValue", get_default_value),
("doNotScroll", get_do_not_scroll),
("doNotSpellCheck", get_do_not_spell_check),
("editable", get_editable),
("fileSelect", get_file_select),
("fillColor", get_color),
("highlight", get_highlight),
("multiline", get_multiline),
("multipleSelection", get_multiple_selection),
("password", get_password),
("radiosInUnison", get_radios_in_unison),
("required", get_required),
("richText", get_rich_text),
("rotation", get_rotation),
("strokeColor", get_color),
("style", get_style),
("textColor", get_color),
];
for (name, getter) in discarding {
define(&object, context, name, getter, ignoring_setter)?;
}
for (name, getter) in [
("textFont", get_text_font as super::af::Bound),
("textSize", get_text_size),
("userName", get_user_name),
("richValue", get_undefined),
("source", get_undefined),
("submitName", get_undefined),
] {
define(&object, context, name, getter, ignoring_setter)?;
}
let refusing: [(&str, super::af::Bound, super::af::Bound); 7] = [
("name", get_name, throw_name),
("type", get_type, throw_type),
("valueAsString", get_value_as_string, throw_value_as_string),
("numItems", get_num_items, throw_num_items),
("doc", get_doc, throw_doc),
("page", get_page, throw_page),
("defaultStyle", throw_default_style, throw_default_style),
];
for (name, getter, setter) in refusing {
define(&object, context, name, getter, setter)?;
}
let methods: [(&str, usize, super::af::Bound); 26] = [
("browseForFileToSubmit", 0, browse_for_file),
("buttonGetCaption", 1, button_get_caption),
("buttonGetIcon", 1, button_get_icon),
("buttonImportIcon", 0, noop),
("buttonSetCaption", 0, method_button_set_caption),
("buttonSetIcon", 0, method_button_set_icon),
("checkThisBox", 2, check_this_box),
("clearItems", 0, noop),
("defaultIsChecked", 1, default_is_checked),
("deleteItemAt", 0, noop),
("getArray", 0, get_array),
("getItemAt", 2, get_item_at),
("getLock", 0, method_get_lock),
("insertItemAt", 0, noop),
("isBoxChecked", 1, is_box_checked),
("isDefaultChecked", 1, is_default_checked),
("setAction", 0, noop),
("setFocus", 0, set_focus),
("setItems", 0, noop),
("setLock", 0, method_set_lock),
(
"signatureGetModifications",
0,
method_signature_get_modifications,
),
("signatureGetSeedValue", 0, method_signature_get_seed_value),
("signatureInfo", 0, method_signature_info),
("signatureSetSeedValue", 0, method_signature_set_seed_value),
("signatureSign", 0, method_signature_sign),
("signatureValidate", 0, method_signature_validate),
];
for (name, length, function) in methods {
let bound = ObjectInitializer::new(context)
.function(native(function), boa_engine::js_string!("f"), length)
.build()
.get(boa_engine::js_string!("f"), context)?;
object.create_data_property_or_throw(
boa_engine::js_string!(name.to_string()),
bound,
context,
)?;
}
Ok(object)
}