Merge remote-tracking branch 'upstream/main' into fix/exec-menu-attachments

# Conflicts:
#	crates/engine/src/tests.rs
#	crates/js/src/tests.rs
#	crates/ui-egui/tests/forms.rs
This commit is contained in:
Eric Kaden
2026-10-22 03:19:20 -04:00
354 changed files with 73358 additions and 3029 deletions
+3
View File
@@ -12,6 +12,9 @@ workspace = true
[dependencies]
boa_engine = { version = "0.22.0", default-features = false, features = ["annex-b"] }
# FormCalc Date/Num2Date calendar conversion goes through chrono (no hand-rolled calendar code);
# std-only, so it stays wasm-safe.
chrono = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
boa_engine = { version = "0.22.0", default-features = false, features = ["annex-b", "js"] }
+91 -28
View File
@@ -1097,10 +1097,16 @@ impl Interp<'_> {
self.charge(1)
}
/// Property `name` of form object `i` as FormCalc reads it: its value, attributes,
/// relatives and children by name.
fn node_member(&mut self, i: usize, name: &str) -> R<Val> {
let Some(n) = self.node(i) else { return Ok(Val::Null) };
Ok(match name {
"rawValue" | "value" | "formattedValue" => self.node_value(i),
"formattedValue" => match &n.formatted {
Some(f) => Val::Str(f.clone()),
None => self.node_value(i),
},
"rawValue" | "value" => self.node_value(i),
"name" => Val::Str(n.name.clone()),
"somExpression" => Val::Str(n.som.clone()),
"className" => Val::Str(n.kind.class_name().into()),
@@ -1838,30 +1844,50 @@ impl Interp<'_> {
// ── built-in functions ──────────────────────────────────────────────────────────────────────
/// `NaiveDate::num_days_from_ce` of 1900-01-01 minus one: a civil date's day count rebased so
/// that 1900-01-01 is FormCalc day 1 (and 1970-01-01 is day 25_568).
const FORMCALC_EPOCH_FROM_CE: i64 = 693_595;
/// Days in one 400-year Gregorian cycle; the calendar repeats after it.
const DAYS_PER_400_YEARS: i64 = 146_097;
/// Days from 1900-01-01 (day 1, as FormCalc counts) to a civil date.
///
/// chrono's `NaiveDate` stops near ±262143, but FormCalc accepts day numbers up to `MAX_DAYS`
/// (years past 270000). The calendar repeats every 400 years, so chrono converts the date
/// within its cycle and whole cycles are added back; the result matches the proleptic Gregorian
/// count for every year. Callers pass dates `parse_date`/`civil_from_days` already validated;
/// anything else reads as 0.
fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
// Howard Hinnant's algorithm, epoch 1970-01-01, shifted to FormCalc's 1900-01-01 = 1.
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let mp = (i64::from(m) + 9) % 12;
let doy = (153 * mp + 2) / 5 + i64::from(d) - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
let unix = era * 146_097 + doe - 719_468;
unix + 25_568
use chrono::Datelike;
let (cycles, year) = (i128::from(y.div_euclid(400)), y.rem_euclid(400));
i32::try_from(year)
.ok()
.and_then(|year| chrono::NaiveDate::from_ymd_opt(year, m, d))
.and_then(|date| {
let days = i128::from(date.num_days_from_ce()) - i128::from(FORMCALC_EPOCH_FROM_CE) + cycles * i128::from(DAYS_PER_400_YEARS);
i64::try_from(days).ok()
})
.unwrap_or(0)
}
/// The civil date (year, month, day) at FormCalc day number `n` (day 1 is 1900-01-01), for any
/// `n`: chrono converts the day within its 400-year cycle and the cycles are added back as years.
fn civil_from_days(n: i64) -> (i64, u32, u32) {
let z = n - 25_568 + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
(if m <= 2 { y + 1 } else { y }, m, d)
use chrono::Datelike;
// Days since CE day 0 (0000-12-31), in i128 so no `n` overflows.
let from_ce = i128::from(n) + i128::from(FORMCALC_EPOCH_FROM_CE);
let (cycles, rest) = (from_ce.div_euclid(i128::from(DAYS_PER_400_YEARS)), from_ce.rem_euclid(i128::from(DAYS_PER_400_YEARS)));
// `rest` is 0..146_097, so CE day `rest` lies in years 0..=400 (day 0 is 0000-12-31).
let date = i32::try_from(rest).ok().and_then(chrono::NaiveDate::from_num_days_from_ce_opt);
match date {
Some(date) => {
let y = i128::from(date.year()) + cycles * 400;
(i64::try_from(y).unwrap_or(if y < 0 { i64::MIN } else { i64::MAX }), date.month(), date.day())
}
// Unreachable: every `rest` in 0..146_097 is a valid CE day.
None => (1900, 1, 1),
}
}
/// Day numbers (and years) beyond this are refused rather than computed.
@@ -1925,10 +1951,15 @@ fn parse_date(text: &str, picture: &str) -> Option<(i64, u32, u32)> {
/// Number of days in month `m` (1-12) of year `y`, leap years included.
fn days_in(y: i64, m: u32) -> u32 {
match m {
2 if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 => 29,
2 => 28,
4 | 6 | 9 | 11 => 30,
// The length of the month from chrono (the first of the next month, less the first of this
// one). An invalid month keeps the old 31-day default.
// Month lengths repeat every 400 years, so the year within its cycle is enough (and always
// in chrono's range, whatever `y` is).
let y = y.rem_euclid(400);
let first = |y: i64, m: u32| i32::try_from(y).ok().and_then(|y| chrono::NaiveDate::from_ymd_opt(y, m, 1));
let next = if m == 12 { first(y + 1, 1) } else { first(y, m + 1) };
match (first(y, m), next) {
(Some(a), Some(b)) => (b - a).num_days() as u32,
_ => 31,
}
}
@@ -2027,6 +2058,10 @@ fn word_num(n: f64, cents: bool) -> String {
}
let cents_total = (n * 100.0).round() as u64;
let whole = if cents { cents_total / 100 } else { n.trunc() as u64 };
if whole >= 1_000_000_000_000 {
// Rounding to cents carried past the supported range.
return String::new();
}
let mut parts = Vec::new();
let groups = [(1_000_000_000u64, "Billion"), (1_000_000, "Million"), (1_000, "Thousand")];
let mut rest = whole;
@@ -2276,8 +2311,11 @@ impl Interp<'_> {
}
let num = self.to_num(&v);
let decimals = pic.split_once('.').map_or(0, |(_, f)| f.chars().filter(|c| matches!(c, '9' | 'z' | 'Z')).count()).min(20);
// `9` in the integer part forces a digit; `z` does not.
let min_int = pic.split('.').next().map_or(0, |i| i.chars().filter(|c| *c == '9').count()).min(20);
let width = min_int + if decimals > 0 { decimals + 1 } else { 0 };
let grouped = pic.contains(',');
let mut text = fixed(num.abs(), decimals);
let mut text = format!("{:0>width$}", fixed(num.abs(), decimals));
if grouped {
let (int, frac) = text.split_once('.').map_or((text.as_str(), ""), |(a, b)| (a, b));
let mut g = String::new();
@@ -2369,9 +2407,16 @@ impl Interp<'_> {
}
"time2num" | "isotime2num" => {
let s = self.arg_str(args, 0)?;
let parts: Vec<u64> = s.split(':').filter_map(|p| p.trim().parse::<u64>().ok()).map(|v| v.min(1_000_000)).collect();
let (h, m, sec) = (parts.first().copied().unwrap_or(0), parts.get(1).copied().unwrap_or(0), parts.get(2).copied().unwrap_or(0));
Val::Num(((h * 3600 + m * 60 + sec) * 1000) as f64)
// ISO 8601 allows a leading "T", a trailing "Z" and fractional seconds.
let t = s.trim();
let t = t.strip_prefix(['T', 't']).unwrap_or(t);
let t = t.strip_suffix(['Z', 'z']).unwrap_or(t);
let mut parts = t.split(':');
let mut whole = || parts.next().and_then(|p| p.trim().parse::<u64>().ok()).unwrap_or(0).min(1_000_000);
let (h, m) = (whole(), whole());
let sec =
parts.next().and_then(|p| p.trim().parse::<f64>().ok()).filter(|v| v.is_finite() && *v >= 0.0).unwrap_or(0.0).min(1_000_000.0);
Val::Num(((h * 3600 + m * 60) * 1000) as f64 + (sec * 1000.0).round())
}
// ── financial
"fv" => {
@@ -2591,11 +2636,29 @@ mod tests {
assert_eq!(days_from_civil(1900, 1, 1), 1);
assert_eq!(days_from_civil(1970, 1, 1), 25_568);
assert_eq!(civil_from_days(days_from_civil(2024, 2, 29)), (2024, 2, 29));
// Past chrono's ±262143 the 400-year cycles carry the year; values from the pre-chrono
// formula, both directions, both sides of the epoch.
for (n, date) in [
(99_000_000, (272_952, 10, 16)),
(99_999_999, (275_690, 9, 11)),
(-99_000_000, (-269_153, 3, 16)),
(-99_999_999, (-271_891, 4, 20)),
(72_354_905, (200_000, 12, 31)),
(-73_742_460, (-200_000, 1, 1)),
] {
assert_eq!(civil_from_days(n), date, "{n}");
assert_eq!(days_from_civil(date.0, date.1, date.2), n, "{date:?}");
}
assert_eq!(days_in(272_952, 2), 29, "272952 is a leap year");
assert_eq!(days_in(-269_153, 2), 28);
assert_eq!(days_in(i64::MAX, 12), 31);
let _ = (civil_from_days(i64::MAX), civil_from_days(i64::MIN), days_from_civil(i64::MAX, 12, 31), days_from_civil(i64::MIN, 1, 1));
assert_eq!(parse_date("2001-02-03", "YYYY-MM-DD"), Some((2001, 2, 3)));
assert_eq!(parse_date("Feb 3, 2001", "MMM D, YYYY"), Some((2001, 2, 3)));
assert_eq!(parse_date("2/3/01", "M/D/YY"), Some((2001, 2, 3)));
assert_eq!(format_date(2001, 2, 3, "MMMM D, YYYY"), "February 3, 2001");
assert_eq!(word_num(1234.5, true), "One Thousand Two Hundred Thirty-Four Dollars And 50 Cents");
assert_eq!(word_num(999_999_999_999.999, true), "");
assert_eq!(num_text(3.0), "3");
assert_eq!(num_text(2.5), "2.5");
assert_eq!(num_text(0.1 + 0.2), "0.3");
+13 -4
View File
@@ -25,6 +25,7 @@
use std::cell::RefCell;
use std::collections::BTreeSet;
use std::sync::Arc;
use boa_engine::object::builtins::{JsArray, JsFunction};
use boa_engine::object::{FunctionObjectBuilder, ObjectInitializer};
@@ -220,7 +221,7 @@ impl Default for Limits {
}
struct Host {
fields: Vec<FieldState>,
fields: Arc<Vec<FieldState>>,
changed: BTreeSet<usize>,
alerts: Vec<String>,
console: Vec<String>,
@@ -312,7 +313,8 @@ fn read(ctx: &Context, name: &JsString) -> JsResult<FieldState> {
fn write(ctx: &Context, name: &JsString, change: impl FnOnce(&mut FieldState)) -> JsResult<()> {
let i = field_index(ctx, name)?;
let mut h = host(ctx)?.borrow_mut();
change(&mut h.fields[i]);
let field = Arc::make_mut(&mut h.fields).get_mut(i).ok_or_else(|| error("the field is no longer available"))?;
change(field);
h.changed.insert(i);
Ok(())
}
@@ -1164,6 +1166,13 @@ fn refuse(source: &str) -> Option<String> {
/// Run `script` for `event`, after the document-level scripts (`doc_scripts`, which define the
/// functions field scripts call), with the form's `fields`.
pub fn run(script: &str, event: &Event, doc: &DocInfo, fields: &[FieldState], doc_scripts: &[String], limits: Limits) -> Outcome {
run_shared(script, event, doc, Arc::new(fields.to_vec()), doc_scripts, limits)
}
/// Like [`run`], sharing an immutable initial field snapshot. Scripts still get a fresh
/// context; the first field write copies the snapshot, so neither the caller nor another
/// script can observe uncommitted writes. Read-only calculations avoid copying the whole form.
pub fn run_shared(script: &str, event: &Event, doc: &DocInfo, fields: Arc<Vec<FieldState>>, doc_scripts: &[String], limits: Limits) -> Outcome {
let failed = |why: String| Outcome { rc: true, value: event.value.clone(), change: event.change.clone(), error: Some(why), ..Default::default() };
if let Some(why) = std::iter::once(script).chain(doc_scripts.iter().map(String::as_str)).find_map(refuse) {
return failed(why);
@@ -1185,12 +1194,12 @@ pub fn run(script: &str, event: &Event, doc: &DocInfo, fields: &[FieldState], do
run_here(script, event, doc, fields, doc_scripts, limits)
}
fn run_here(script: &str, event: &Event, doc: &DocInfo, fields: &[FieldState], doc_scripts: &[String], limits: Limits) -> Outcome {
fn run_here(script: &str, event: &Event, doc: &DocInfo, fields: Arc<Vec<FieldState>>, doc_scripts: &[String], limits: Limits) -> Outcome {
let mut ctx = Context::default();
ctx.runtime_limits_mut().set_loop_iteration_limit(limits.loop_iterations);
ctx.runtime_limits_mut().set_recursion_limit(limits.recursion);
ctx.insert_data(RefCell::new(Host {
fields: fields.to_vec(),
fields,
changed: BTreeSet::new(),
alerts: Vec::new(),
console: Vec::new(),
+109
View File
@@ -380,12 +380,15 @@ mod xfa_model {
}
}
/// Run the JavaScript `script` for `activity` on the test form, from the node at `target`.
fn go(script: &str, target: &str, activity: &str) -> XfaOutcome {
let ev = XfaEvent { activity: activity.into(), target: target.into(), ..Default::default() };
let doc = XfaDoc { file_name: "f.pdf".into(), page: 1, page_count: 3 };
run_xfa(script, &ev, &doc, &form(), Limits::default())
}
/// A calculate script finds its siblings by name and yields its last expression, and a
/// value it sets comes back as an effect.
#[test]
fn calculate_scripts_see_siblings_by_name_and_yield_their_last_expression() {
let o = go("qty.rawValue * price.rawValue", "form1[0].page1[0].total[0]", "calculate");
@@ -395,6 +398,39 @@ mod xfa_model {
assert_eq!(o.effects, vec![XfaEffect::SetValue { som: "form1[0].page1[0].total[0]".into(), value: "5".into() }]);
}
/// A form whose one field `d` holds the ISO date 2026-10-20, shown as 20261020.
pub(super) fn dated_form() -> XfaNode {
let d = XfaNode { formatted: Some("20261020".into()), ..field("d", "form1[0].page1[0].d[0]", "2026-10-20", false) };
XfaNode {
name: "form1".into(),
som: "form1[0]".into(),
kind: Some(XfaKind::Subform),
children: vec![XfaNode {
name: "page1".into(),
som: "form1[0].page1[0]".into(),
kind: Some(XfaKind::Subform),
children: vec![d],
..Default::default()
}],
..Default::default()
}
}
/// `formattedValue` reads the value as the field's picture shows it, and `rawValue` the
/// value itself; once the script sets the value, `formattedValue` gives that value.
#[test]
fn formatted_value_reads_the_shown_value_until_the_script_sets_one() {
let ev = XfaEvent { activity: "calculate".into(), target: "form1[0].page1[0].d[0]".into(), ..Default::default() };
let doc = XfaDoc::default();
let run = |script: &str| run_xfa(script, &ev, &doc, &dated_form(), Limits::default());
let o = run("this.formattedValue + ' ' + this.rawValue");
assert_eq!(o.error, None, "{o:?}");
assert_eq!(o.result.as_deref(), Some("20261020 2026-10-20"));
let o = run("this.rawValue = '2026-10-21'; this.formattedValue");
assert_eq!(o.result.as_deref(), Some("2026-10-21"));
}
/// Long values a script sets, and long results, are cut where they are stored, with a note.
#[test]
fn long_values_and_results_are_cut_where_they_are_stored() {
// The value kept for later reads is the one the effect saves.
@@ -607,6 +643,22 @@ mod formcalc {
o.result.unwrap_or_default()
}
/// FormCalc reads `formattedValue` as the field's picture shows it too, until the script
/// sets the value.
#[test]
fn formcalc_formatted_value_reads_the_shown_value_until_the_script_sets_one() {
let ev = XfaEvent { activity: "calculate".into(), target: "form1[0].page1[0].d[0]".into(), ..Default::default() };
let doc = XfaDoc::default();
let run = |script: &str| run_formcalc(script, &ev, &doc, &super::xfa_model::dated_form(), Limits::default());
let o = run("Concat($.formattedValue, \" \", $.rawValue)");
assert_eq!(o.error, None, "{o:?}");
assert_eq!(o.result.as_deref(), Some("20261020 2026-10-20"));
let o = run("$.rawValue = \"2026-10-21\"\n$.formattedValue");
assert_eq!(o.error, None, "{o:?}");
assert_eq!(o.result.as_deref(), Some("2026-10-21"));
}
/// FormCalc arithmetic, string conversion and comparisons follow FormCalc's own rules.
#[test]
fn arithmetic_strings_and_comparisons_follow_formcalc_rules() {
assert_eq!(calc("1 + 2 * 3"), "7");
@@ -651,6 +703,17 @@ mod formcalc {
assert_eq!(calc(r#"Num2Date(45350, "YYYY-MM-DD x")"#), "2024-02-29 x");
}
/// Accepted day numbers past chrono's own date range (about year 262143) keep the dates the
/// pre-chrono algorithm gave, on both sides of the epoch, through Num2Date and Format.
#[test]
fn date_numbers_beyond_chronos_range_keep_their_dates() {
assert_eq!(calc(r#"Num2Date(99000000, "YYYY-MM-DD")"#), "272952-10-16");
assert_eq!(calc(r#"Format("YYYY-MM-DD", 99000000)"#), "272952-10-16");
assert_eq!(calc(r#"Num2Date(99999999, "YYYY-MM-DD")"#), "275690-09-11");
assert_eq!(calc(r#"Num2Date(-99000000, "YYYY-MM-DD")"#), "-269153-03-16");
assert_eq!(calc(r#"Num2Date(-99999999, "YYYY-MM-DD")"#), "-271891-04-20");
}
#[test]
fn str_and_format_round_exact_ties_away_from_zero() {
assert_eq!(calc("Str(2.5, 3, 0)"), " 3");
@@ -708,6 +771,9 @@ mod formcalc {
assert_eq!(calc(r#"Num2Date(45350, "YYYY-MM-DD")"#), "2024-02-29");
assert_eq!(calc(r#"Num2Date(Date2Num("2024-02-29") + 1, "MMMM D, YYYY")"#), "March 1, 2024");
assert_eq!(calc(r#"Time2Num("01:00:00")"#), "3600000");
assert_eq!(calc(r#"IsoTime2Num("12:30:45Z")"#), "45045000");
assert_eq!(calc(r#"IsoTime2Num("12:30:45.250")"#), "45045250");
assert_eq!(calc(r#"IsoTime2Num("T12:30:45")"#), "45045000");
assert_eq!(calc(r#"Num2Time(3661000)"#), "01:01:01");
assert_eq!(calc("Round(Pmt(10000, 0.01, 12), 2)"), "888.49");
assert_eq!(calc("Round(FV(100, 0.01, 12), 2)"), "1268.25");
@@ -720,6 +786,15 @@ mod formcalc {
assert!(go("Nope(1)", "form1[0].page1[0].total[0]", "calculate").error.unwrap().contains("unknown function"));
}
#[test]
fn format_nine_pads_the_integer_part() {
assert_eq!(calc(r#"Format("99999", 42)"#), "00042");
assert_eq!(calc(r#"Format("9999", 5)"#), "0005");
assert_eq!(calc(r#"Format("999.99", 4.5)"#), "004.50");
assert_eq!(calc(r#"Format("zzz9", 42)"#), "42");
assert_eq!(calc(r#"Format("99", 12345)"#), "12345");
}
#[test]
fn date2num_rejects_days_past_the_end_of_the_month() {
assert_eq!(calc(r#"Date2Num("2023-02-30")"#), "0");
@@ -1143,6 +1218,40 @@ mod session {
}
}
#[test]
fn shared_field_snapshots_isolate_writes_errors_and_script_globals() {
let initial = Arc::new(fields());
let before = initial.as_ref().clone();
let event = Event::field("Calculate", "total", "");
let write = run_shared(
"var scriptLocal = 1; getField('qty').value = 9; getField('agree').checkThisBox(0, true); event.value = getField('qty').value;",
&event,
&doc(),
Arc::clone(&initial),
&[],
Limits::default(),
);
assert_eq!(write.error, None);
assert_eq!(write.value, "9");
assert_eq!(write.changed.iter().find(|f| f.name == "qty").unwrap().value, ["9"]);
assert_eq!(write.changed.iter().find(|f| f.name == "agree").unwrap().value, ["Yes"]);
assert_eq!(*initial, before, "script writes cannot mutate the caller's snapshot");
let failed = run_shared("getField('qty').value = 99; throw new Error('rejected');", &event, &doc(), Arc::clone(&initial), &[], Limits::default());
assert!(failed.error.is_some());
assert_eq!(*initial, before, "an exception cannot leak partial writes into the initial snapshot");
let read = run_shared(
"event.value = typeof scriptLocal + ':' + getField('qty').value + ':' + getField('agree').isBoxChecked(0);",
&event,
&doc(),
initial,
&[],
Limits::default(),
);
assert_eq!(read.error, None);
assert_eq!(read.value, "undefined:4:false");
assert!(read.changed.is_empty());
}
mod menu_items {
use super::*;
use crate::xfa::{XfaDoc, XfaEffect, XfaEvent, run_xfa};
+17 -1
View File
@@ -64,6 +64,9 @@ pub struct XfaNode {
/// This instance's index among its same-named siblings.
pub index: usize,
pub children: Vec<XfaNode>,
/// Fields: the value shown in the field's display picture, what `formattedValue` reads;
/// `None` when the picture doesn't change it (the raw value is read then).
pub formatted: Option<String>,
}
/// The event a script runs for.
@@ -207,6 +210,8 @@ pub(crate) struct HNode {
pub(crate) children: Vec<usize>,
/// Removed by `removeInstance`: skipped everywhere.
pub(crate) gone: bool,
/// See [`XfaNode::formatted`]; dropped when a script sets the value, which it no longer shows.
pub(crate) formatted: Option<String>,
}
/// The form as a script sees and changes it, with the effects it records.
@@ -348,6 +353,7 @@ impl XHost {
// the snapshot stays bounded however often a long value is written.
let (text, cut) = clip_value(text);
n.value = text.clone();
n.formatted = None;
let som = n.som.clone();
if cut {
self.clipped_values += 1;
@@ -359,10 +365,13 @@ impl XHost {
type Shared = RefCell<XHost>;
/// The form host of the script running in `ctx`.
fn host(ctx: &Context) -> JsResult<&Shared> {
ctx.get_data::<Shared>().ok_or_else(|| error("the XFA script host is not set up"))
}
/// The snapshot `n` and its descendants flattened into `out` under `parent` (to [`MAX_DEPTH`]
/// and [`MAX_NODES`]): the index of `n`'s node, `None` past the limits.
fn flatten(n: &XfaNode, parent: Option<usize>, out: &mut Vec<HNode>, depth: usize) -> Option<usize> {
if depth > MAX_DEPTH || out.len() >= MAX_NODES {
return None;
@@ -383,6 +392,7 @@ fn flatten(n: &XfaNode, parent: Option<usize>, out: &mut Vec<HNode>, depth: usiz
parent,
children: Vec::new(),
gone: false,
formatted: n.formatted.clone(),
});
let mut kids = Vec::new();
for c in &n.children {
@@ -620,6 +630,8 @@ fn clone_subtree(h: &mut XHost, src: usize, parent: Option<usize>, depth: usize)
parent,
children: Vec::new(),
gone: false,
// The copy holds no value, so nothing is shown in its picture either.
formatted: None,
},
n.children.clone(),
)
@@ -819,6 +831,8 @@ pub(crate) fn resolve_som(h: &XHost, from: usize, expr: &str) -> Vec<usize> {
cur
}
/// The live descendants of node `from` named `name` (`from..name` in a SOM path), in document
/// order, to [`MAX_DEPTH`].
pub(crate) fn descendants_named(h: &XHost, from: usize, name: &str, depth: usize) -> Vec<usize> {
let mut out = Vec::new();
if depth > MAX_DEPTH {
@@ -839,6 +853,8 @@ pub(crate) fn descendants_named(h: &XHost, from: usize, name: &str, depth: usize
out
}
/// A property read on a form node's Proxy: its value (and `formattedValue` as its picture shows
/// it), attributes, relatives, methods, instance managers and children by name.
fn node_get(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
let idx = node_index(&arg(args, 0), ctx)?;
let key = arg(args, 1);
@@ -855,7 +871,7 @@ fn node_get(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValu
Ok(match key.as_str() {
IDX => JsValue::from(idx as f64),
"rawValue" | "value" => host(ctx)?.borrow().nodes.get(idx).map_or(JsValue::null(), value_js),
"formattedValue" => s(&value),
"formattedValue" => s(&host(ctx)?.borrow().nodes.get(idx).and_then(|n| n.formatted.clone()).unwrap_or(value)),
"name" => s(&name),
"somExpression" => s(&som),
"className" => s(kind.class_name()),
+3 -2
View File
@@ -1,8 +1,9 @@
//! One script engine for a whole form event. Every script of the event runs on one thread
//! (with the script stack), each in a fresh JavaScript context on a fresh copy of the form as
//! the caller holds it, so scripts can't see each other's variables or objects and a script
//! that failed halfway leaves nothing behind; what a script changed reaches the next one only
//! through the caller, which applies the effects and sends the form as it then is. Before,
//! that failed halfway leaves no state behind in the engine; what a script changed (up to its
//! error, when it failed) reaches the next one only through the caller, which applies the
//! effects and sends the form as it then is. Before,
//! every script also got a thread of its own and the caller rebuilt the form for each: a form
//! with hundreds of calculations spent its open-time budget on that.
//!