Browse / github.com/storytold/pdfcraft / patch
M12: an XFA form's scripts can call the script objects of their subform
M12: an XFA form's scripts can call the script objects of their subform ## What this changes An XFA form can keep named JavaScripts under a subform's `<variables>`: `<script name="LOVUtils" contentType="application/x-javascript">`. Its scripts call them by name (`LOVUtils.check(x)`). The template parser ignored `<variables>`, so every such call failed with `ReferenceError: LOVUtils is not defined`, and with it the form's initialize and calculate scripts. In the pdf.js XFA test corpus the failing names include `LOVUtils`, `validator`, `FormInit`, `myfunctions`, `layoutUtils`, `messages`, `Block` and `ColorFieldsValidation`. - The parser now reads them (JavaScript
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- hikashop-nicolas (imported from pull request #4948, open)
- Base
1dd2737b47a9c1c9562f0af563053d1bf510e4bf- Size
- +420 −6 in 12 files
- Review
- pending
- Shared
- 2026-10-28
- Id
91e9de4ecfb6168d84d667af
How it went
No other operator has reported using it yet.
Diff
raw · crates/engine/src/tests.rs crates/engine/src/xfa.rs crates/js/src/run.rs crates/js/src/tests.rs crates/js/src/xfa.rs crates/js/src/xfa/run.rs crates/js/src/xfa_session.rs crates/xfa/src/fixtures.rs crates/xfa/src/lib.rs crates/xfa/src/model.rs crates/xfa/src/parse.rs crates/xfa/src/script.rs
@@ -6177,6 +6177,20 @@ fn repeated_xfa_print_buttons_run_their_click_script() { } } +/// A script object (`<variables><script name="Calc">`) is called by its name from the scripts of its subform and below,+/// from a deeper object to a shallower one, and only the ones a script uses are defined for it.+#[test]+fn xfa_scripts_call_the_script_objects_of_their_subform() {+ let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::script_objects_template()));+ let mut s = Session::new().with_clock(|| 1_700_000_000);+ let id = s.open("objects.pdf", None, bytes, None).expect("opens");+ let value = |n: &str| s.get(id).unwrap().form.iter().find(|f| f.name == n).map(|f| f.value.clone()).unwrap_or_else(|| panic!("no field {n}"));+ assert_eq!(value("total"), vec!["25".to_string()]);+ assert_eq!(value("label"), vec!["Hello Ada 7".to_string()]);+ let out = s.take_js_output(id);+ assert!(out.errors.is_empty(), "{:?}", out.errors);+}+ #[test] fn xfa_scripts_initialize_calculate_validate_toggle_and_add_rows() { let bytes = Arc::new(pdfcraft_xfa::fixtures::shell(&pdfcraft_xfa::fixtures::scripted_template()));@@ -661,7 +661,20 @@ impl<'a, 'b> Runner<'a, 'b> { self.check_live(); let event = XfaEvent { activity: ev.activity.clone(), target: ev.som.clone(), new_text: new_text.to_string(), prev_text: String::new() }; let root = to_js(&self.live().root);+ // a JavaScript event may call the script objects of its subform, which are defined in front of it+ let prelude = if ev.formcalc {+ String::new()+ } else {+ match pdfcraft_xfa::script_objects_for(self.tpl, &ev.som, &ev.script) {+ Ok(prelude) => prelude,+ Err(why) => {+ self.error(format!("{} ({}): {why}", ev.som, ev.activity));+ return Ok(None);+ }+ }+ }; let script = Script {+ prelude, source: ev.script.clone(), formcalc: ev.formcalc, event,@@ -36,6 +36,11 @@ pub(crate) fn refuse(source: &str) -> Option<String> { if source.len() > MAX_SCRIPT_BYTES { return Some(format!("the script is too long ({} KiB; the limit is {} KiB)", source.len() / 1024, MAX_SCRIPT_BYTES / 1024)); }+ refuse_nesting(source)+}++/// [`refuse`] for code with a length limit of its own: only its nesting is checked.+pub(crate) fn refuse_nesting(source: &str) -> Option<String> { let too_deep = || Some("the script is nested too deeply to run safely".to_string()); let b = source.as_bytes(); let (mut depth, mut prefix, mut conditionals) = (0usize, 0usize, 0usize);@@ -1412,6 +1412,7 @@ mod session { /// default limits and a 3 s timeout. fn script(source: &str, formcalc: bool, target: &str, activity: &str) -> Script { Script {+ prelude: String::new(), source: source.into(), formcalc, event: XfaEvent { activity: activity.into(), target: target.into(), ..Default::default() },@@ -1463,6 +1464,30 @@ mod session { assert!(started.elapsed() < std::time::Duration::from_secs(10), "{:?}", started.elapsed()); } + /// The script objects a form keeps are defined in front of the script, count against their own bound rather+ /// than the script's size, and a broken or oversized one fails only the scripts that use it.+ #[test]+ fn a_scripts_prelude_is_defined_before_it_and_bounded_on_its_own() {+ let mut s = XfaSession::new(XfaDoc { page_count: 1, ..Default::default() });+ let mut with = script("Calc.twice(21)", false, "form1[0].page1[0].total[0]", "calculate");+ with.prelude = "var Calc = { twice: function (x) { return x * 2; } };\n".into();+ assert_eq!(s.run(with).result.as_deref(), Some("42"));+ // far past the size a script may have, but within what script objects may add+ let mut big = script("Big.n", false, "form1[0].page1[0].total[0]", "calculate");+ big.prelude = format!("var Big = {{ n: 7 }};\n// {}\n", "x".repeat(300 * 1024));+ let o = s.run(big);+ assert_eq!(o.result.as_deref(), Some("7"), "{o:?}");+ let mut huge = script("1", false, "form1[0].page1[0].total[0]", "calculate");+ huge.prelude = " ".repeat(crate::xfa_session::MAX_PRELUDE_BYTES + 1);+ let o = s.run(huge);+ assert!(o.error.as_deref().is_some_and(|e| e.contains("script objects are too large")), "{o:?}");+ // a script object that does not parse fails the script, not the session+ let mut broken = script("1", false, "form1[0].page1[0].total[0]", "calculate");+ broken.prelude = "var Bad = (function () { ;return { ;\n".into();+ assert!(s.run(broken).error.is_some());+ assert_eq!(s.run(script("1 + 1", false, "form1[0].page1[0].total[0]", "calculate")).result.as_deref(), Some("2"));+ }+ /// A JavaScript or FormCalc script whose engine takes longer to be ready than the script's /// time limit still runs and returns its result, and the session stays alive. #[test]@@ -32,5 +32,5 @@ pub(crate) use nodes::{child_named, descendants_named, instances, resolve_som}; pub(crate) use run::run_within_limits; #[cfg(not(target_arch = "wasm32"))] pub(crate) use run::{ENGINE_START_LIMIT, warm_up};-pub(crate) use run::{run_here, run_within};+pub(crate) use run::{run_here_with, run_within}; pub use run::{run_xfa, run_xfa_within};@@ -126,6 +126,20 @@ pub(crate) fn warm_up() { /// `started` is called once the engine is ready: a time limit on the script runs from then. pub(crate) fn run_here(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNode, limits: Limits, started: &dyn Fn()) -> XfaOutcome {+ run_here_with(script, "", event, doc, root, limits, started)+}++/// [`run_here`] with `prelude`, the form's script objects, defined first. They are part of the form, so they are not held+/// to the size a script may have (their own bound is the caller's), only to its nesting.+pub(crate) fn run_here_with(+ script: &str,+ prelude: &str,+ event: &XfaEvent,+ doc: &XfaDoc,+ root: XfaNode,+ limits: Limits,+ started: &dyn Fn(),+) -> XfaOutcome { warm_up(); let mut ctx = match Context::builder().host_hooks(std::rc::Rc::new(crate::CheckedCodeFromStrings)).build() { Ok(ctx) => ctx,@@ -141,6 +155,12 @@ pub(crate) fn run_here(script: &str, event: &XfaEvent, doc: &XfaDoc, root: XfaNo install(&mut ctx, event, doc, current)?; // Ready for the script: its time starts now, the set-up above not charged to it. started();+ if !prelude.is_empty() {+ if let Some(why) = crate::run::refuse_nesting(prelude) {+ return Err(boa_engine::JsNativeError::range().with_message(why).into());+ }+ ctx.eval(Source::from_bytes(prelude.as_bytes()))?;+ } ctx.register_global_property(js_string!("__xfa_src"), s(script), Attribute::CONFIGURABLE)?; // Direct eval inside a function called on the node: `this` is the node and the // completion value is the script's last expression (what a calculate script yields).@@ -30,9 +30,15 @@ pub fn engines_started() -> u64 { ENGINES_STARTED.with(std::cell::Cell::get) } +/// The most a script's script objects may add to it.+pub const MAX_PRELUDE_BYTES: usize = 2 * 1024 * 1024;+ /// What one script run needs. #[derive(Clone, Debug)] pub struct Script {+ /// The script objects the source calls, defined in front of it (read for what they are: part of the form, not a+ /// script to size up on its own, though bounded by [`MAX_PRELUDE_BYTES`]).+ pub prelude: String, pub source: String, pub formcalc: bool, pub event: XfaEvent,@@ -64,7 +70,11 @@ fn run_script(s: Script, doc: &XfaDoc, started: &dyn Fn()) -> XfaOutcome { started(); return XfaOutcome { error: Some(why), ..Default::default() }; }- crate::xfa::run_here(&s.source, &s.event, doc, s.root, s.limits, started)+ if s.prelude.len() > MAX_PRELUDE_BYTES {+ started();+ return XfaOutcome { error: Some(format!("the form's script objects are too large ({} KiB)", s.prelude.len() / 1024)), ..Default::default() };+ }+ crate::xfa::run_here_with(&s.source, &s.prelude, &s.event, doc, s.root, s.limits, started) } #[cfg(not(target_arch = "wasm32"))]@@ -739,3 +739,39 @@ if (this.rawValue == "1") { "## .to_string() }++/// A form with script objects (`<variables><script name="...">`): `Calc` on the root subform, `Names` on the `page`+/// subform, and fields that call them from an initialize script. `total` uses `Calc.twice` and the variable `Calc.step`;+/// `label` uses `Names.greet`, which itself uses `Calc`.+pub fn script_objects_template() -> String {+ r##"<?xml version="1.0" encoding="UTF-8"?>+<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">+<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">+<subform name="form" layout="tb">+ <pageSet><pageArea name="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>+ <variables>+ <script name="Calc" contentType="application/x-javascript">+// a comment that says function not_a_function+var step = 5;+function twice(x) { return x * 2 + step; }+ </script>+ <script name="Unused" contentType="application/x-javascript">function never() { throw new Error("not defined unless used"); }</script>+ </variables>+ <subform name="page" layout="tb">+ <variables>+ <script name="Names" contentType="application/x-javascript">function greet(n) { return "Hello " + n + " " + Calc.twice(1); }</script>+ <script name="Formcalc" contentType="application/x-formcalc">$ = 1</script>+ </variables>+ <field name="total" w="2in" h="0.3in"><ui><textEdit/></ui>+ <event activity="initialize"><script contentType="application/x-javascript">this.rawValue = Calc.twice(10);</script></event>+ </field>+ <field name="label" w="2in" h="0.3in"><ui><textEdit/></ui>+ <event activity="initialize"><script contentType="application/x-javascript">this.rawValue = Names.greet("Ada");</script></event>+ </field>+ </subform>+</subform>+</template>+</xdp:xdp>+"##+ .to_string()+}@@ -103,7 +103,7 @@ pub use pdf::{CLICK_KEY, LAYOUT_KEY, NODE_KEY, SOM_KEY, Stripped, XfaShape, exis pub use picture::typed_date; pub use script::{ FormNode, LiveForm, MAX_FORM_NODES, NodeKind, OVERRIDES_KEY, Overrides, ScriptEvent, apply_overrides, data_of, field_grows_with_text,- fields_by_som, form_tree, has_scripts, overrides, refresh_value, rerender, set_overrides, template_of,+ fields_by_som, form_tree, has_scripts, overrides, refresh_value, rerender, script_objects_for, set_overrides, template_of, }; #[derive(Debug, thiserror::Error)]@@ -416,10 +416,20 @@ impl Default for Occur { } } +/// A named JavaScript the form keeps under a subform's `<variables>` (a "script object"): its functions are called+/// through its name, from any script of that subform or below.+#[derive(Clone, Debug, Default, PartialEq)]+pub struct ScriptObject {+ pub name: String,+ pub text: String,+}+ #[derive(Clone, Debug, Default, PartialEq)] pub struct Subform { pub common: Common, pub layout: Layout,+ /// `<variables><script name="…">`: JavaScript objects the subform's scripts call by name.+ pub script_objects: Vec<ScriptObject>, pub children: Vec<Node>, /// `<event activity="…"><script>` on the subform (initialize, calculate, …). pub scripts: Vec<Script>,@@ -7,8 +7,8 @@ use roxmltree::Node as XmlNode; use crate::XfaError; use crate::model::{ Access, Anchor, Area, Border, Caption, ChoiceList, Color, Common, Corner, Draw, Edge, ExclGroup, Field, FontSpec, HAlign, Layout, Margin, Node,- Occur, PageArea, PageRole, PageSet, ParaSpec, Paragraph, Placement, Presence, Rect, RichText, Run, Script, Stroke, Subform, Template, Ui, VAlign,- Value,+ Occur, PageArea, PageRole, PageSet, ParaSpec, Paragraph, Placement, Presence, Rect, RichText, Run, Script, ScriptObject, Stroke, Subform,+ Template, Ui, VAlign, Value, }; /// Deepest element nesting followed (templates are shallow; hostile ones are not).@@ -558,6 +558,29 @@ fn parse_script(s: XmlNode, activity: &str) -> Script { } /// `<event activity="…"><script>…</script></event>` children.+/// The JavaScript objects under `<variables>`: `<script name="LOVUtils" contentType="application/x-javascript">`.+fn parse_script_objects(n: XmlNode) -> Vec<ScriptObject> {+ let mut out = Vec::new();+ for vars in children(n, "variables").take(8) {+ for s in children(vars, "script").take(64) {+ let Some(name) = s.attribute("name").map(str::trim).filter(|v| is_js_identifier(v)) else { continue };+ if !s.attribute("contentType").unwrap_or("").to_ascii_lowercase().contains("javascript") {+ continue;+ }+ let text = text_of(s);+ if !text.trim().is_empty() {+ out.push(ScriptObject { name: name.to_string(), text });+ }+ }+ }+ out+}++fn is_js_identifier(s: &str) -> bool {+ let mut chars = s.chars();+ chars.next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == '$') && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')+}+ fn parse_events(n: XmlNode) -> Vec<Script> { let mut scripts = Vec::new(); for ev in children(n, "event").take(64) {@@ -640,6 +663,7 @@ fn parse_subform(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Subfor layout: Layout::parse(n.attribute("layout")), children: children_out, scripts: parse_events(n),+ script_objects: parse_script_objects(n), page_set, occur: parse_occur(child(n, "occur")), break_before_page: break_to_page("breakBefore")@@ -7,7 +7,7 @@ use std::collections::{BTreeMap, HashMap}; use pdfcraft_cos::{Dict, Document, Object, PdfString}; use crate::data::{DataNode, data_path, parse_datasets};-use crate::model::{Access, Field, Node, Presence, Script, SomIndices, Template};+use crate::model::{Access, Field, Node, Presence, Script, ScriptObject, SomIndices, Subform, Template}; use crate::{Report, XfaError}; /// Private key on the AcroForm: presence scripts set, by SOM path.@@ -737,3 +737,260 @@ pub fn refresh_value(live: &mut FormNode, tpl: &Template, data: Option<&DataNode pub fn data_of(doc: &Document) -> Option<DataNode> { crate::read_packets(doc).ok().flatten().and_then(|p| parse_datasets(&p.xdp)) }++/// JavaScript that defines the script objects a script at `som` calls by name: `LOVUtils.check(x)` finds the+/// `<variables><script name="LOVUtils">` of the subform the script sits in, or of one above it. Only the objects+/// the script uses (and those they use) are defined, since every script runs in a fresh context. Empty when it uses none.+/// `Err` when the objects it needs add up to more than [`MAX_SCRIPT_OBJECT_BYTES`].+pub fn script_objects_for(tpl: &Template, som: &str, script: &str) -> Result<String, String> {+ // The subforms from the root down to the one holding the script, as the SOM names them (unnamed ones in between too).+ let mut chain: Vec<&Subform> = vec![&tpl.root];+ for part in som.split('.').skip(1) {+ let name = part.split('[').next().unwrap_or(part);+ let Some(&last) = chain.last() else { break };+ let mut found = Vec::new();+ if !subform_path(&last.children, name, &mut found) {+ break;+ }+ chain.extend(found);+ }+ // Inner objects hide outer ones of the same name.+ let mut objects: Vec<&ScriptObject> = Vec::new();+ for sf in &chain {+ for o in &sf.script_objects {+ objects.retain(|x| x.name != o.name);+ objects.push(o);+ }+ }+ if objects.is_empty() {+ return Ok(String::new());+ }+ let mentions = |text: &str, name: &str| -> bool {+ text.match_indices(name).any(|(i, _)| {+ let before = text[..i].chars().next_back();+ let after = text[i + name.len()..].chars().next();+ let word = |c: Option<char>| c.is_some_and(|c| c.is_alphanumeric() || c == '_' || c == '$');+ !word(before) && !word(after)+ })+ };+ let mut used: Vec<&ScriptObject> = Vec::new();+ let mut bytes = 0usize;+ let mut texts = vec![script];+ while let Some(text) = texts.pop() {+ for o in &objects {+ if !used.iter().any(|u| std::ptr::eq(*u, *o)) && mentions(text, &o.name) {+ bytes = bytes.saturating_add(o.text.len());+ if bytes > MAX_SCRIPT_OBJECT_BYTES {+ return Err(format!(+ "the form's script objects are too large ({} KiB; the limit is {} KiB)",+ bytes / 1024,+ MAX_SCRIPT_OBJECT_BYTES / 1024+ ));+ }+ used.push(o);+ texts.push(&o.text);+ }+ }+ }+ // Defined in the order they were declared.+ used.sort_by_key(|u| objects.iter().position(|o| std::ptr::eq(*o, *u)));+ let mut out = String::new();+ for o in used {+ let names = declared_names(&o.text);+ let members: Vec<String> = names.iter().map(|n| format!("{n}: typeof {n} === 'undefined' ? undefined : {n}")).collect();+ out.push_str(&format!("var {} = (function () {{\n{}\n;return {{ {} }};\n}})();\n", o.name, o.text, members.join(", ")));+ }+ Ok(out)+}++/// The most the script objects a script needs may add up to.+pub const MAX_SCRIPT_OBJECT_BYTES: usize = 2 * 1024 * 1024;++/// The functions and variables a script declares at its top level or below (`function f`, `var v`, `let l`, `const c`), found by+/// reading the code without its comments and strings.+fn declared_names(text: &str) -> Vec<String> {+ const RESERVED: &[&str] = &[+ "function",+ "var",+ "let",+ "const",+ "if",+ "else",+ "for",+ "while",+ "do",+ "switch",+ "case",+ "default",+ "return",+ "break",+ "continue",+ "new",+ "delete",+ "typeof",+ "void",+ "in",+ "instanceof",+ "this",+ "null",+ "true",+ "false",+ "try",+ "catch",+ "finally",+ "throw",+ "with",+ "class",+ "extends",+ "super",+ "yield",+ "await",+ "undefined",+ "arguments",+ "eval",+ ];+ let b = text.as_bytes();+ let mut names: Vec<String> = Vec::new();+ let mut i = 0;+ let mut declaring = false;+ // inside a `var a = 1, b = f(2, 3);` list: the bracket depth it sits at, so a comma at that depth names the next one+ let (mut depth, mut list): (i32, Option<i32>) = (0, None);+ while i < b.len() && names.len() < 256 {+ let c = b[i];+ match c {+ b'/' if b.get(i + 1) == Some(&b'/') => {+ while i < b.len() && b[i] != b'\n' {+ i += 1;+ }+ }+ b'/' if b.get(i + 1) == Some(&b'*') => {+ i += 2;+ while i < b.len() && !b[i..].starts_with(b"*/") {+ i += 1;+ }+ i += 2;+ }+ b'\'' | b'"' | b'`' => {+ i += 1;+ while i < b.len() && b[i] != c {+ i += if b[i] == b'\\' { 2 } else { 1 };+ }+ i += 1;+ declaring = false;+ }+ c if c.is_ascii_alphabetic() || c == b'_' || c == b'$' => {+ let start = i;+ while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_' || b[i] == b'$') {+ i += 1;+ }+ let word = &text[start..i];+ // `a.function` or `x.var` is a property, not a declaration+ let after_dot = text[..start].trim_end().ends_with('.');+ if declaring && !RESERVED.contains(&word) && !after_dot {+ if !names.iter().any(|n| n == word) && word.len() < 64 {+ names.push(word.to_string());+ }+ declaring = false;+ } else {+ declaring = !after_dot && matches!(word, "function" | "var" | "let" | "const");+ if declaring {+ list = (word != "function").then_some(depth);+ }+ }+ }+ c if c.is_ascii_whitespace() || c == b'*' => i += 1,+ c => {+ declaring = false;+ match c {+ b'(' | b'[' | b'{' => depth = depth.saturating_add(1),+ b')' | b']' | b'}' => {+ depth = depth.saturating_sub(1);+ if list.is_some_and(|d| depth < d) {+ list = None;+ }+ }+ b',' if list == Some(depth) => declaring = true,+ b';' if list.is_some_and(|d| depth <= d) => list = None,+ _ => {}+ }+ i += 1;+ }+ }+ }+ names+}++/// The subforms down to the one named `name` (unnamed subforms and areas between are walked through, the unnamed subforms+/// kept in the path since their script objects reach what is below them); `false` leaves `path` as it was.+fn subform_path<'a>(nodes: &'a [Node], name: &str, path: &mut Vec<&'a Subform>) -> bool {+ for n in nodes {+ match n {+ Node::Subform(sf) if sf.common.name.as_deref() == Some(name) => {+ path.push(sf);+ return true;+ }+ Node::Subform(sf) if sf.common.name.is_none() => {+ path.push(sf);+ if subform_path(&sf.children, name, path) {+ return true;+ }+ path.pop();+ }+ Node::Area(a) if subform_path(&a.children, name, path) => return true,+ _ => {}+ }+ }+ false+}++#[cfg(test)]+mod script_object_tests {+ use super::{declared_names, script_objects_for};++ #[test]+ fn a_script_gets_the_objects_it_uses_from_its_subforms() {+ let (tpl, _) = crate::parse(&crate::fixtures::script_objects_template()).expect("parses");+ let prelude = script_objects_for(&tpl, "form[0].page[0].total[0]", "this.rawValue = Names.greet('x');").unwrap();+ // Names is used directly, Calc by Names; Unused and the FormCalc object are not defined+ assert!(prelude.contains("var Calc =") && prelude.contains("var Names ="), "{prelude}");+ assert!(prelude.find("var Calc =") < prelude.find("var Names ="), "declared outer first");+ assert!(!prelude.contains("var Unused") && !prelude.contains("Formcalc"), "{prelude}");+ // from the root subform the inner object is out of reach, and a script that names none gets nothing+ assert!(!script_objects_for(&tpl, "form[0]", "Names.greet()").unwrap().contains("var Names"));+ assert_eq!(script_objects_for(&tpl, "form[0].page[0].total[0]", "this.rawValue = 1;").unwrap(), "");+ // the objects of an unnamed subform between named ones reach the scripts below it+ let (mut tpl, _) = crate::parse(&crate::fixtures::script_objects_template()).expect("parses");+ let page = tpl+ .root+ .children+ .iter_mut()+ .find_map(|n| if let crate::model::Node::Subform(sf) = n { Some(sf) } else { None })+ .expect("the page subform");+ let inner = std::mem::take(page);+ page.children.push(crate::model::Node::Subform(Box::new(crate::model::Subform {+ script_objects: vec![crate::model::ScriptObject { name: "Mid".into(), text: "function m() { return 1; }".into() }],+ children: vec![crate::model::Node::Subform(inner)],+ ..Default::default()+ })));+ let prelude = script_objects_for(&tpl, "form[0].page[0].total[0]", "Mid.m()").unwrap();+ assert!(prelude.contains("var Mid ="), "{prelude}");+ // objects that add up to more than the bound are refused, not built+ let (mut tpl, _) = crate::parse(&crate::fixtures::script_objects_template()).expect("parses");+ tpl.root.script_objects.push(crate::model::ScriptObject { name: "Huge".into(), text: " ".repeat(super::MAX_SCRIPT_OBJECT_BYTES + 1) });+ let why = script_objects_for(&tpl, "form[0]", "Huge.x").unwrap_err();+ assert!(why.contains("too large"), "{why}");+ }++ #[test]+ fn declared_names_are_read_without_comments_strings_or_properties() {+ let text = "//==== function\n/* var hidden */ var counter = 0; function check(x) { return \"function fake\" + x.function; }\nfunction* gen() {}\nlet last;";+ assert_eq!(declared_names(text), ["counter", "check", "gen", "last"]);+ // every declarator of a list, but not the commas inside its calls, arrays and objects+ assert_eq!(+ declared_names("var a = 1, b = f(2, c), d = [3, 4], e = { x: 1, y: 2 }, g; let h = 5; function i() { var j = 1, k; }"),+ ["a", "b", "d", "e", "g", "h", "i", "j", "k"]+ );+ // words that are only keywords in some places are names+ assert_eq!(declared_names("var get = 1; function set() {} var of = 2, async = 3, static = 4;"), ["get", "set", "of", "async", "static"]);+ }+}