mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
M12: XFA scripts read a field's border, ui, validate and other properties (XFA 3.3 ch. 3, ch. 17)
Scripts got an object that takes anything for `this.border`, `this.ui`,
`this.validate` and the like (XFA 3.3 ch. 3, "Properties vs. Children",
p. 78), so `ui.oneOfChild.className == "choiceList"` was never true,
`this.validate.nullTest == "error"` never matched, and
`border.getElement("edge", 0).color.value` threw a TypeError.
The parser now keeps each object's own attributes and its property
elements (border, font, para, margin, ui, validate, occur, break,
breakBefore, breakAfter, assist, format, value, bind, desc, extras, keep,
caption, items, calculate) as a generic tree, bounded per object: 256
elements, 16 KiB of text, 8 levels, 64 attributes an element; past that
the rest is left out with one warning. An element keeps only its own
text, not that of children left out (an extras, a script); a validation
or a calculation keeps its first script with its text outside the
budget, as forms read it, within the script engine's 256 KiB limit on a
script. A field's first two `<items>` (a field has two at most, p. 726)
are kept as lists: each counts against the budget as one element, and
its entries, outside it, are bounded by the list's own limits (1,000
entries), as the field's lists are; a further `<items>` (one public form
has a third, empty one) is an ordinary property, every entry charged.
The tree is shared through an Arc by the template, every instance of the
object and every script's snapshot, and the engine keeps one converted
copy of each tree in the form's template, so it is made once per opened
form rather than per event or per script, behind the form's own lock,
and goes when the form is closed. On a 10,000-field form with 17
property elements each, a script's snapshot takes 3.24 ms against
1.46 ms on main (median of 15 interleaved runs); the property trees are
converted once per form, and each later snapshot adds one walk over the
form and a lookup per tree.
In JavaScript a property object is a Proxy naming its node and path. It
reads what the template writes, or the default XFA 3.3 gives for the
properties the 367 inventoried public forms read: presence of border,
edge, corner, fill (p. 629, 687, 663, 734); colours (a fill white,
p. 734; a font fill, edges and corners black, p. 734, 687, 663); font
typeface/size/weight (pp. 741-744); para hAlign/vAlign (p. 800); margin
insets (p. 776); validate nullTest/scriptTest/formatTest (pp. 880-881);
occur min/max/initial (p. 786); break before/after (p. 632); bind ref
(p. 623); keep next/previous none and intact by container (pp. 762-763);
a comb's numberOfCells (p. 657); a gradient's type (p. 770, 806, 820); a
fill's type solid (p. 734) and a field's widget textEdit (p. 877); a
calculation's override, "disabled" where the template has no calculate
element and "error" where it has one that doesn't say (pp. 641-642).
Where XFA 3.3 leaves the outcome open, or where every reader tested
contradicts its text, reads follow the readers. A border's presence
reads "hidden" when the object has no border element ("visible" with
one that doesn't say, p. 629). For the edges and corners a border
doesn't list, p. 38 says the last one listed is reused; readers draw
such a side from the edge or corner on the same side instead (two
listed alternate, three reuse the second for the fourth, one serves
every side), and reads follow the readers: a side past those listed
reads as the one drawn there (with none listed, the defaults: black,
0.5pt, solid, a square cap, a corner's radius 0in, square join, not
inverted, p. 687, p. 663), and is made as a copy of it (with what a
script already set on it) when a script reaches it, so a write then
changes only that side. A border has four sides: a fifth or later edge
or corner, even one the template lists, is out of reach, as the layout
draws four. The copies have their own budget (8 MiB a run), apart from
the run's text budget, so reaching sides never crowds out a script's
writes; past it a side still reads as the one drawn there. A fill type
the fill doesn't have (its pattern when it is solid, p. 734) can't be
read: the read raises an error the script can catch (GeneralError,
"operation failed"), the fill keeps its type, and `"pattern" in fill`
is false. Anything else unspecified reads as an empty value that
compares as "" and never throws; further defaults are added as forms
need them.
Property objects have className, oneOfChild, children by name
(desc.vMandatory, message.scriptTest) and by class (ui["#textEdit"]),
getElement(name, index), isPropertySpecified, getAttribute,
setAttribute, rawValue (a desc's first text), a validate's or
calculate's script text and message (p. 827), resolveNode(s),
somExpression and nodes (length/item/namedItem); nodes get getAttribute,
getElement, isPropertySpecified, and mandatory, validationMessage,
formatMessage and mandatoryMessage read from their validate's nullTest
and message texts (the text of that name, as the template or a script
named it, else a single unnamed one, which serves every test: p. 782,
p. 880). Setting a test's message sets its named text, else overwrites a
single unnamed one (so every test then reads it), else adds a text of
that name, as readers do; reading validate.message.<test> where no such
text exists raises a script error, as in readers. fillColor, fontColor
and borderColor read from the border fill, font fill and first edge
colours; borderColor set colours all four edges. A child of the same
name comes before a property, an attribute or a member, on nodes and on
property objects alike, for reads and writes (a write to a member a
child shadows leaves the property alone), as XFA SOM resolves name
clashes in favour of the explicitly named object, and `#name` reaches
the property (p. 99). A SOM expression from a node, or from
xfa.resolveNode, that runs into objects' properties
(resolveNode("ui.#textEdit"), "border.edge[1]",
"row[*].border.fill.color", "extras.arrayGroup[*].groupName", "ui.*")
gives those property objects, each instance's, 4,096 at most (about 30
of the forms do this); `value.#text` on a draw stays null, as before,
until a node's value is its value element.
Writes (border.fill.color.value = ..., validate.nullTest = ...,
rawValue, setAttribute, mandatory, validationMessage = ...) are kept for
the rest of the script run and read back through the same properties,
bounded by the run's text budget and 64 attributes a property, but have
no effect on the form yet.
The caption (XFA 3.3 p. 643) and a field's items (p. 758) are property
objects too. A caption's value holds its label as text or rich text,
read by name, by class (`['#text']`), through resolveNode and
oneOfChild; rich text reads as its words; the content a value doesn't
hold reads as null (one of them, p. 598). Items list their entries
through `nodes`, each with the text the choice list or check button
shows. A field's `items` is its first `<items>` (the shown list), a
second is reached by index (`items[1]`), and a field with none reads an
items object with no entries. Items reads follow the template:
`HNode::items` is the hook PdfCraft's choice-list methods (addItem,
clearItems; not on main yet) will set, so that reads agree with them
once they land. exData.loadXML and saveXML do nothing yet. Over the 163
dynamic forms (118,208 objects) no object reaches the budget: elements
p99 30, max 41 (limit 256); bytes p99 773, max 8,500 (limit
16 KiB); item entries per list p99 51, max 342. The choice-list
methods (selectedIndex reads still give an object that takes anything),
execEvent and script objects are handled separately. The one known gap
is value.text and value.exData (about 15 of the 367 forms): a node's
value stays its raw value, and changing what value returns is left to a
follow-up with a full-corpus regression check. FormCalc does not read
these properties yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK
This commit is contained in:
1 parent
3109bb2359
commit
34f3716f5e
14 files changed
+3229
-39
No files matched your search
+173
-6
@@ -8,9 +8,9 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use pdfcraft_js::xfa::{XfaDoc, XfaEffect, XfaEvent, XfaKind, XfaNode};
|
||||
use pdfcraft_js::xfa::{XfaDoc, XfaEffect, XfaEvent, XfaKind, XfaNode, XfaProp, XfaProps};
|
||||
use pdfcraft_js::xfa_session::{Script, XfaSession};
|
||||
use pdfcraft_xfa::model::Template;
|
||||
use pdfcraft_xfa::model::{Prop, Props, Template};
|
||||
use pdfcraft_xfa::{FormNode, LiveForm, NodeKind, ScriptEvent};
|
||||
|
||||
use crate::js::JsOutput;
|
||||
@@ -134,8 +134,94 @@ pub(crate) struct XfaRun<'a> {
|
||||
pub time_scale: u32,
|
||||
}
|
||||
|
||||
/// The live form node `n`, with its descendants, as the script engine's snapshot of it.
|
||||
fn to_js(n: &FormNode) -> XfaNode {
|
||||
/// The script engine's copies of a form's property trees, by the address of the tree each was
|
||||
/// made from: made the first time a script of the form needs one, then shared by every later
|
||||
/// script and event of that form. They are kept in the form's template
|
||||
/// ([`pdfcraft_xfa::model::TemplateCache`]), so they go with it when the form is closed, and
|
||||
/// the lock is the form's own. Each entry holds a weak reference to its tree, which keeps the
|
||||
/// address from being reused while the entry exists; entries whose tree is gone (a tree of a
|
||||
/// copy of the template, such as one with a script's overrides applied) are dropped each time
|
||||
/// the map is used.
|
||||
type PropsCopies = HashMap<usize, (std::sync::Weak<Props>, Arc<XfaProps>)>;
|
||||
type PropsCache = std::sync::Mutex<PropsCopies>;
|
||||
|
||||
/// The form's map of copies, with the entries of trees that are gone dropped; `None` only when
|
||||
/// the template's cache holds something else. A poisoned lock only means a panic elsewhere
|
||||
/// while it was held: the map is still a valid memo.
|
||||
fn props_copies(tpl: &Template) -> Option<std::sync::MutexGuard<'_, PropsCopies>> {
|
||||
let mut copies = tpl.cache.get_or_init(PropsCache::default)?.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
copies.retain(|_, (tree, _)| tree.strong_count() > 0);
|
||||
Some(copies)
|
||||
}
|
||||
|
||||
/// The copy made of `p`, if there is one.
|
||||
fn copy_of(copies: &PropsCopies, p: &Arc<Props>) -> Option<Arc<XfaProps>> {
|
||||
let (tree, copy) = copies.get(&(Arc::as_ptr(p) as usize))?;
|
||||
(std::sync::Weak::as_ptr(tree) == Arc::as_ptr(p) && tree.strong_count() > 0).then(|| Arc::clone(copy))
|
||||
}
|
||||
|
||||
/// The script engine's copy of the template's property tree `p`.
|
||||
fn convert_props(p: &Props) -> XfaProps {
|
||||
fn prop(p: &Prop, depth: usize) -> XfaProp {
|
||||
XfaProp {
|
||||
tag: p.tag.clone(),
|
||||
attrs: p.attrs.clone(),
|
||||
// Shared, not copied: a validation's script can be long.
|
||||
text: p.text.clone(),
|
||||
// The parser keeps 8 levels; this bounds the walk whatever builds the tree.
|
||||
children: if depth > 64 { Vec::new() } else { p.children.iter().map(|c| prop(c, depth + 1)).collect() },
|
||||
}
|
||||
}
|
||||
XfaProps { attrs: p.attrs.clone(), elements: p.elements.iter().map(|e| prop(e, 0)).collect() }
|
||||
}
|
||||
|
||||
/// The live form node `n` of the form whose template is `tpl`, with its descendants, as the
|
||||
/// script engine's snapshot of it; the property trees are the form's shared copies
|
||||
/// ([`PropsCopies`]), not copied again. The map is locked only to look copies up and to add the
|
||||
/// ones made (outside the lock) for trees met for the first time.
|
||||
fn to_js(n: &FormNode, tpl: &Template) -> XfaNode {
|
||||
fn trees<'a>(n: &'a FormNode, out: &mut HashMap<usize, &'a Arc<Props>>, depth: usize) {
|
||||
out.entry(Arc::as_ptr(&n.props) as usize).or_insert(&n.props);
|
||||
if depth <= 256 {
|
||||
n.children.iter().for_each(|c| trees(c, out, depth + 1));
|
||||
}
|
||||
}
|
||||
let mut needed = HashMap::new();
|
||||
trees(n, &mut needed, 0);
|
||||
let mut found: HashMap<usize, Arc<XfaProps>> = HashMap::with_capacity(needed.len());
|
||||
let missing: Vec<&Arc<Props>> = {
|
||||
let copies = props_copies(tpl);
|
||||
needed
|
||||
.iter()
|
||||
.filter_map(|(&key, p)| match copies.as_ref().and_then(|c| copy_of(c, p)) {
|
||||
Some(copy) => {
|
||||
found.insert(key, copy);
|
||||
None
|
||||
}
|
||||
None => Some(*p),
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
if !missing.is_empty() {
|
||||
let made: Vec<(&Arc<Props>, Arc<XfaProps>)> = missing.into_iter().map(|p| (p, Arc::new(convert_props(p)))).collect();
|
||||
let mut copies = props_copies(tpl);
|
||||
for (p, copy) in made {
|
||||
let key = Arc::as_ptr(p) as usize;
|
||||
// Another thread may have made one meanwhile: keep the first.
|
||||
let copy = match copies.as_mut() {
|
||||
Some(c) => copy_of(c, p).unwrap_or_else(|| {
|
||||
c.insert(key, (Arc::downgrade(p), Arc::clone(©)));
|
||||
copy
|
||||
}),
|
||||
None => copy,
|
||||
};
|
||||
found.insert(key, copy);
|
||||
}
|
||||
}
|
||||
to_js_with(n, &found, 0)
|
||||
}
|
||||
|
||||
fn to_js_with(n: &FormNode, copies: &HashMap<usize, Arc<XfaProps>>, depth: usize) -> XfaNode {
|
||||
XfaNode {
|
||||
name: n.name.clone(),
|
||||
som: n.som.clone(),
|
||||
@@ -154,7 +240,8 @@ fn to_js(n: &FormNode) -> XfaNode {
|
||||
occur_min: n.occur_min,
|
||||
occur_max: n.occur_max,
|
||||
index: n.index,
|
||||
children: n.children.iter().map(to_js).collect(),
|
||||
children: if depth > 256 { Vec::new() } else { n.children.iter().map(|c| to_js_with(c, copies, depth + 1)).collect() },
|
||||
props: copies.get(&(Arc::as_ptr(&n.props) as usize)).cloned().unwrap_or_else(|| Arc::new(convert_props(&n.props))),
|
||||
formatted: n.display_picture.as_deref().and_then(|p| pdfcraft_xfa::formatted_value(p, &n.value)),
|
||||
}
|
||||
}
|
||||
@@ -662,7 +749,8 @@ impl<'a, 'b> Runner<'a, 'b> {
|
||||
#[cfg(test)]
|
||||
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);
|
||||
let tpl = self.tpl;
|
||||
let root = to_js(&self.live().root, tpl);
|
||||
let script = Script {
|
||||
source: ev.script.clone(),
|
||||
formcalc: ev.formcalc,
|
||||
@@ -954,4 +1042,83 @@ mod tests {
|
||||
Some((2, "form[0].table[0]".into(), "row".into(), 2))
|
||||
);
|
||||
}
|
||||
|
||||
/// The scripts' snapshot shares the template's property trees: the next script's, and the
|
||||
/// next event's (a live form built again, from a copy of the template as overrides make
|
||||
/// one), point at the trees the first converted rather than converting them again.
|
||||
#[test]
|
||||
fn snapshots_share_property_trees_across_runs_and_events() {
|
||||
let fields: String = (0..2_000)
|
||||
.map(|i| format!(r#"<field name="f{i}"><ui><choiceList/></ui><border><edge/><fill><color value="1,2,3"/></fill></border></field>"#))
|
||||
.collect();
|
||||
let xml = format!(
|
||||
r#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="form" layout="tb">{fields}</subform></template>"#
|
||||
);
|
||||
let (tpl, _) = pdfcraft_xfa::parse(&xml).unwrap();
|
||||
let live = pdfcraft_xfa::form_tree(&tpl, None, &Default::default());
|
||||
let a = to_js(&live.root, &tpl);
|
||||
let b = to_js(&live.root, &tpl);
|
||||
// A tree built again (with a script's overrides) is still the form's: same copies.
|
||||
let copy = pdfcraft_xfa::apply_overrides(&tpl, &Default::default());
|
||||
let c = to_js(&pdfcraft_xfa::form_tree(©, None, &Default::default()).root, &tpl);
|
||||
assert_eq!(a.children.len(), 2_000);
|
||||
for ((x, y), z) in a.children.iter().zip(&b.children).zip(&c.children) {
|
||||
assert!(Arc::ptr_eq(&x.props, &y.props) && Arc::ptr_eq(&x.props, &z.props));
|
||||
}
|
||||
assert_eq!(a.children[0].props.elements[1].children[1].children[0].attrs, [("value".to_string(), "1,2,3".to_string())]);
|
||||
}
|
||||
|
||||
/// A form's copies of its property trees are its own: kept for its later snapshots, and
|
||||
/// dropped with its template when the form is closed, whatever other forms do.
|
||||
#[test]
|
||||
fn a_closed_forms_property_copies_go_with_its_template() {
|
||||
let xml = |name: &str| {
|
||||
format!(
|
||||
r#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="form" layout="tb"><field name="{name}"><border><edge/></border></field></subform></template>"#
|
||||
)
|
||||
};
|
||||
let (closed, _) = pdfcraft_xfa::parse(&xml("a")).unwrap();
|
||||
let (open, _) = pdfcraft_xfa::parse(&xml("b")).unwrap();
|
||||
let live = pdfcraft_xfa::form_tree(&closed, None, &Default::default());
|
||||
let snapshot = to_js(&live.root, &closed);
|
||||
let copy = Arc::downgrade(&snapshot.children[0].props);
|
||||
drop(snapshot);
|
||||
assert!(copy.upgrade().is_some(), "kept for the form's next snapshot");
|
||||
// Another form's snapshot neither keeps nor needs this form's copies.
|
||||
let other = to_js(&pdfcraft_xfa::form_tree(&open, None, &Default::default()).root, &open);
|
||||
assert!(!Arc::ptr_eq(&other.children[0].props, ©.upgrade().unwrap_or_default()));
|
||||
drop(live);
|
||||
drop(closed);
|
||||
assert!(copy.upgrade().is_none(), "closing the form drops its copies at once");
|
||||
}
|
||||
|
||||
/// A field's click script reads its own and other objects' properties through a laid-out
|
||||
/// dynamic form: template values, the spec's defaults, and a write read back, which changes
|
||||
/// nothing on the form.
|
||||
#[test]
|
||||
fn a_click_script_reads_field_properties() {
|
||||
let template = pdfcraft_xfa::fixtures::scripted_template()
|
||||
.replace(
|
||||
r#"<field name="hello" w="1in" h="0.3in"><ui><button/></ui>"#,
|
||||
r#"<field name="hello" w="1in" h="0.3in"><ui><button/></ui><border><fill><color value="200,200,255"/></fill></border>"#,
|
||||
)
|
||||
.replace(
|
||||
r#"xfa.host.messageBox("Hello " + qty.rawValue);"#,
|
||||
r#"qty.validate.nullTest = "error"; xfa.host.messageBox([this.ui.oneOfChild.className, this.border.fill.color.value, this.border.presence, qty.ui.oneOfChild.className, qty.validate.message.text.value, qty.validate.nullTest, table.row.occur.max, more.ui.oneOfChild.className, this.isPropertySpecified("border"), qty.isPropertySpecified("border")].join("|"));"#,
|
||||
);
|
||||
let mut s = crate::Session::new();
|
||||
let id = s.open("props.pdf", None, Arc::new(pdfcraft_xfa::fixtures::shell(&template)), None).unwrap();
|
||||
let _ = s.take_js_output(id);
|
||||
let fields =
|
||||
|s: &crate::Session| s.get(id).unwrap().form.iter().map(|f| (f.name.clone(), f.value.clone(), f.read_only())).collect::<Vec<_>>();
|
||||
let before = fields(&s);
|
||||
let out = s.run_javascript(id, "", Some("hello")).unwrap();
|
||||
assert!(out.error.is_none(), "{out:?}");
|
||||
assert_eq!(
|
||||
out.alerts,
|
||||
[pdfcraft_js::Message::plain("button|200,200,255|visible|numericEdit|Quantity must be 100 or less|error|-1|checkButton|true|false")]
|
||||
);
|
||||
assert!(s.take_js_output(id).errors.is_empty());
|
||||
assert_eq!(fields(&s), before, "a property write changes nothing yet");
|
||||
}
|
||||
}
|
||||
@@ -22,12 +22,14 @@ mod host;
|
||||
mod instances;
|
||||
mod model;
|
||||
mod nodes;
|
||||
mod props;
|
||||
mod run;
|
||||
|
||||
pub(crate) use host::{HNode, XHost};
|
||||
pub use model::{MAX_ALERTS, MAX_CONSOLE, MAX_EFFECTS, XfaDoc, XfaEffect, XfaEvent, XfaKind, XfaNode, XfaOutcome};
|
||||
pub(crate) use model::{MAX_LINE, MAX_PAYLOAD, clip, clip_result, reset_names};
|
||||
pub(crate) use nodes::{child_named, descendants_named, instances, resolve_som};
|
||||
pub use props::{XfaProp, XfaProps};
|
||||
#[cfg(test)]
|
||||
pub(crate) use run::run_within_limits;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
@@ -23,6 +23,14 @@ pub(crate) struct HNode {
|
||||
pub(crate) gone: bool,
|
||||
/// See [`XfaNode::formatted`]; dropped when a script sets the value, which it no longer shows.
|
||||
pub(crate) formatted: Option<String>,
|
||||
/// See [`XfaNode::props`]; instances a script adds share their first instance's.
|
||||
pub(crate) props: std::sync::Arc<super::XfaProps>,
|
||||
/// A choice list's entries as a script's list methods leave them, one list per `<items>`;
|
||||
/// `None` while they are as the template lists them. The hook for PdfCraft's choice-list
|
||||
/// methods (`addItem`, `clearItems`; the Series 1 work, not on main yet), which will set it so
|
||||
/// that the `items` property reads (see [`super::props::list_entries`]) agree with them.
|
||||
/// Nothing sets it yet, so reads follow the template.
|
||||
pub(crate) items: Option<Vec<super::props::ItemList>>,
|
||||
}
|
||||
|
||||
/// The form as a script sees and changes it, with the effects it records.
|
||||
@@ -48,6 +56,14 @@ pub(crate) struct XHost {
|
||||
/// Bytes of text kept in effects, console lines and values written, against [`MAX_PAYLOAD`].
|
||||
kept: usize,
|
||||
dropped_bytes: usize,
|
||||
/// What the script wrote to property objects, by (node, property path): read back later in
|
||||
/// the run, applied nowhere yet. Charged against [`MAX_PAYLOAD`].
|
||||
pub(crate) prop_writes: super::props::PropWrites,
|
||||
/// Attributes written to a property past [`super::props::MAX_WRITTEN_ATTRS`], left out.
|
||||
pub(super) dropped_prop_writes: usize,
|
||||
/// Bytes of edges and corners copied when a script reached sides a border doesn't list,
|
||||
/// against [`super::props::MAX_COPIED`].
|
||||
pub(super) copied: usize,
|
||||
}
|
||||
|
||||
impl XHost {
|
||||
@@ -74,11 +90,14 @@ impl XHost {
|
||||
dropped_console: 0,
|
||||
kept: 0,
|
||||
dropped_bytes: 0,
|
||||
prop_writes: super::props::PropWrites::new(),
|
||||
dropped_prop_writes: 0,
|
||||
copied: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Room for `bytes` more bytes of kept text: charged when there is, a drop counted when not.
|
||||
fn room(&mut self, bytes: usize) -> bool {
|
||||
pub(super) fn room(&mut self, bytes: usize) -> bool {
|
||||
match self.kept.checked_add(bytes).filter(|k| *k <= MAX_PAYLOAD) {
|
||||
Some(k) => {
|
||||
self.kept = k;
|
||||
@@ -209,6 +228,13 @@ impl XHost {
|
||||
if self.clipped_values > 0 {
|
||||
out.push(format!("the script set {} value(s) longer than {MAX_VALUE} characters; they were cut", self.clipped_values));
|
||||
}
|
||||
if self.dropped_prop_writes > 0 {
|
||||
out.push(format!(
|
||||
"the script wrote more than {} attributes to one property; {} more were left out",
|
||||
super::props::MAX_WRITTEN_ATTRS,
|
||||
self.dropped_prop_writes
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -272,6 +298,8 @@ fn flatten(n: &XfaNode, parent: Option<usize>, out: &mut Vec<HNode>, depth: usiz
|
||||
children: Vec::new(),
|
||||
gone: false,
|
||||
formatted: n.formatted.clone(),
|
||||
props: std::sync::Arc::clone(&n.props),
|
||||
items: None,
|
||||
});
|
||||
let mut kids = Vec::new();
|
||||
for c in &n.children {
|
||||
|
||||
@@ -160,6 +160,9 @@ fn clone_subtree(h: &mut XHost, src: usize, parent: Option<usize>, depth: usize)
|
||||
gone: false,
|
||||
// The copy holds no value, so nothing is shown in its picture either.
|
||||
formatted: None,
|
||||
props: std::sync::Arc::clone(&n.props),
|
||||
// A new instance lists what the template lists.
|
||||
items: None,
|
||||
},
|
||||
n.children.clone(),
|
||||
)
|
||||
|
||||
@@ -50,6 +50,9 @@ pub struct 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 template's attributes and property elements of the object (border, ui, validate, …),
|
||||
/// shared: snapshots taken for each script point at the same tree.
|
||||
pub props: std::sync::Arc<super::XfaProps>,
|
||||
}
|
||||
|
||||
/// The event a script runs for.
|
||||
|
||||
+84
-18
@@ -2,8 +2,9 @@ use super::globals::{recalculate, relayout};
|
||||
use super::host::{HNode, XHost, host};
|
||||
use super::instances::{instance_manager, list_object};
|
||||
use super::model::{MAX_DEPTH, XfaEffect};
|
||||
use super::props;
|
||||
use super::{
|
||||
Attribute, Context, JsObject, JsProxy, JsResult, JsString, JsValue, NativeFunction, ObjectInitializer, arg, as_number, error, function,
|
||||
Attribute, Context, JsObject, JsProxy, JsResult, JsString, JsValue, NativeFunction, ObjectInitializer, arg, as_number, clip, error, function,
|
||||
js_string, s, text,
|
||||
};
|
||||
|
||||
@@ -208,6 +209,12 @@ fn node_get(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValu
|
||||
(n.name.clone(), n.som.clone(), n.kind, n.value.clone(), n.presence.clone(), n.access.clone(), n.index, n.parent, n.children.clone())
|
||||
};
|
||||
let (name, som, kind, value, presence, access, index, parent, children) = snapshot;
|
||||
// A child named like one of the members read from the properties comes first, as for the
|
||||
// properties themselves (XFA 3.3 p. 99).
|
||||
let shadowing = if props::NODE_MEMBERS.contains(&key.as_str()) { child_named(&host(ctx)?.borrow(), idx, &key) } else { None };
|
||||
if let Some(c) = shadowing {
|
||||
return node_object(ctx, c);
|
||||
}
|
||||
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),
|
||||
@@ -219,7 +226,15 @@ fn node_get(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValu
|
||||
"presence" => s(&presence),
|
||||
"access" => s(&access),
|
||||
"isNull" => JsValue::from(value.is_empty()),
|
||||
"mandatory" => s("disabled"),
|
||||
"mandatory" => s(&props::node_mandatory(&host(ctx)?.borrow(), idx)),
|
||||
"fillColor" | "fontColor" | "borderColor" => {
|
||||
let path = props::shorthand_color(&key).unwrap_or_default();
|
||||
s(&props::node_color(&host(ctx)?.borrow(), idx, path))
|
||||
}
|
||||
"validationMessage" | "formatMessage" | "mandatoryMessage" => {
|
||||
let test = props::message_test(&key).unwrap_or_default();
|
||||
s(&props::node_message(&host(ctx)?.borrow(), idx, test))
|
||||
}
|
||||
"parent" => match parent {
|
||||
Some(p) => node_object(ctx, p)?,
|
||||
None => JsValue::null(),
|
||||
@@ -246,32 +261,33 @@ fn node_get(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValu
|
||||
"resolveNodes" => resolver(ctx, idx, true)?,
|
||||
"recalculate" => function(ctx, NativeFunction::from_fn_ptr(recalculate)).into(),
|
||||
"remerge" => function(ctx, NativeFunction::from_fn_ptr(relayout)).into(),
|
||||
"getAttribute"
|
||||
| "setAttribute"
|
||||
| "execEvent"
|
||||
| "execInitialize"
|
||||
| "execCalculate"
|
||||
| "execValidate"
|
||||
| "getElement"
|
||||
| "setElement"
|
||||
| "isPropertySpecified"
|
||||
| "loadXML"
|
||||
| "saveXML"
|
||||
| "clone" => function(ctx, NativeFunction::from_fn_ptr(noop)).into(),
|
||||
"getAttribute" => node_method(ctx, idx, props::get_attribute),
|
||||
"getElement" => node_method(ctx, idx, props::get_element),
|
||||
"isPropertySpecified" => node_method(ctx, idx, props::is_property_specified),
|
||||
"setAttribute" | "execEvent" | "execInitialize" | "execCalculate" | "execValidate" | "setElement" | "loadXML" | "saveXML" | "clone" => {
|
||||
function(ctx, NativeFunction::from_fn_ptr(noop)).into()
|
||||
}
|
||||
"toString" | "valueOf" | "toJSON" => function(ctx, NativeFunction::from_fn_ptr(node_to_string)).into(),
|
||||
"length" => JsValue::undefined(),
|
||||
"border" | "ui" | "font" | "caption" | "fillColor" | "borderColor" | "fontColor" | "margin" | "para" | "assist" | "validate" | "format"
|
||||
| "items" | "bind" | "keep" | "h" | "w" | "x" | "y" | "minH" | "maxH" | "minW" | "maxW" | "layout" | "model" | "oneOfChild" | "locale"
|
||||
| "relevant" | "colSpan" | "dataNode" | "defaultValue" | "editValue" | "selectedIndex" | "event" | "calculate" | "traversal" | "extras"
|
||||
| "desc" => sink(ctx)?,
|
||||
other => {
|
||||
if let Some(child) = other.strip_prefix('_') {
|
||||
let exists = child_named(&host(ctx)?.borrow(), idx, child).is_some();
|
||||
return if exists { instance_manager(ctx, idx, child) } else { Ok(JsValue::undefined()) };
|
||||
}
|
||||
// `#border`: the property, whatever the children are called.
|
||||
if let Some(prop) = other.strip_prefix('#').filter(|p| props::NODE_PROPS.contains(p)) {
|
||||
return props::prop_object(ctx, idx, &props::node_prop_path(prop));
|
||||
}
|
||||
let found = child_named(&host(ctx)?.borrow(), idx, other);
|
||||
match found {
|
||||
Some(c) => node_object(ctx, c)?,
|
||||
// A child of the name comes first, for every name below: "XFA SOM resolves the
|
||||
// name clash in favor of the explicit naming" (XFA 3.3 p. 99).
|
||||
// Property objects (p. 78): read with the spec's defaults, written for this run
|
||||
// only.
|
||||
None if props::NODE_PROPS.contains(&other) => props::prop_object(ctx, idx, &props::node_prop_path(other))?,
|
||||
// Members not modelled yet take any read or write without complaint.
|
||||
None if SINK_MEMBERS.contains(&other) => sink(ctx)?,
|
||||
None => JsValue::undefined(),
|
||||
}
|
||||
}
|
||||
@@ -309,6 +325,12 @@ fn node_set(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValu
|
||||
};
|
||||
h.set_state(idx, XfaEffect::SetAccess { som, access: a });
|
||||
}
|
||||
// A child of the same name is what a read of the member gives (see `node_get`), so a
|
||||
// write names that child too, and doesn't reach the property; writes to a child by name
|
||||
// are not modelled, as for every other child name.
|
||||
k if props::NODE_MEMBERS.contains(&k) && child_named(&h, idx, k).is_some() => {}
|
||||
"mandatory" | "validationMessage" | "formatMessage" | "mandatoryMessage" => h.set_validation(idx, &key, clip(value)),
|
||||
"fillColor" | "fontColor" | "borderColor" => h.set_color(idx, &key, clip(value)),
|
||||
_ => {}
|
||||
}
|
||||
Ok(JsValue::from(true))
|
||||
@@ -319,6 +341,37 @@ fn node_to_string(this: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<
|
||||
Ok(s(&host(ctx)?.borrow().nodes.get(idx).map(|n| n.value.clone()).unwrap_or_default()))
|
||||
}
|
||||
|
||||
/// Node members not modelled yet: a read gives an object that takes any read or write
|
||||
/// ([`sink`]) unless a child has the name.
|
||||
const SINK_MEMBERS: &[&str] = &[
|
||||
"h",
|
||||
"w",
|
||||
"x",
|
||||
"y",
|
||||
"minH",
|
||||
"maxH",
|
||||
"minW",
|
||||
"maxW",
|
||||
"layout",
|
||||
"model",
|
||||
"oneOfChild",
|
||||
"locale",
|
||||
"relevant",
|
||||
"colSpan",
|
||||
"dataNode",
|
||||
"defaultValue",
|
||||
"editValue",
|
||||
"selectedIndex",
|
||||
"event",
|
||||
"traversal",
|
||||
];
|
||||
|
||||
/// The node method `f` bound to node `idx`.
|
||||
fn node_method(ctx: &mut Context, idx: usize, f: fn(&JsValue, &[JsValue], &JsObject, &mut Context) -> JsResult<JsValue>) -> JsValue {
|
||||
let r = props::node_ref(ctx, idx);
|
||||
function(ctx, NativeFunction::from_copy_closure_with_captures(f, r)).into()
|
||||
}
|
||||
|
||||
pub(super) fn noop(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
@@ -334,6 +387,19 @@ pub(super) fn resolver(ctx: &mut Context, from: usize, many: bool) -> JsResult<J
|
||||
let many = key.get(js_string!("many"), ctx)?.to_boolean();
|
||||
let expr = text(&arg(args, 0), ctx)?;
|
||||
let found = resolve_som(&host(ctx)?.borrow(), from, &expr);
|
||||
// An expression that runs into an object's properties (`ui.#textEdit`, `border.edge[1]`).
|
||||
if found.is_empty() {
|
||||
let props = props::resolve_into_props(&host(ctx)?.borrow(), from, &expr);
|
||||
if let Some((node, path)) = props.first().cloned() {
|
||||
{
|
||||
// Edges and corners reached past those a border lists come into being.
|
||||
let mut h = host(ctx)?.borrow_mut();
|
||||
let reached = if many { props.as_slice() } else { props.get(..1).unwrap_or_default() };
|
||||
reached.iter().for_each(|(n, p)| h.instantiate(*n, p));
|
||||
}
|
||||
return if many { props::list_object(ctx, props) } else { props::prop_object(ctx, node, &path) };
|
||||
}
|
||||
}
|
||||
if many {
|
||||
return list_object(ctx, &found);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,925 @@
|
||||
//! Property objects as scripts see them: the spec's defaults, the template's values,
|
||||
//! `getElement`, `oneOfChild`, `isPropertySpecified` and writes read back in the same run.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::super::{XfaDoc, XfaEvent, XfaKind, XfaNode, XfaOutcome, run_xfa};
|
||||
use super::{XfaProp, XfaProps};
|
||||
use crate::Limits;
|
||||
|
||||
fn p(tag: &str, attrs: &[(&str, &str)], children: Vec<XfaProp>) -> XfaProp {
|
||||
XfaProp { tag: tag.into(), attrs: attrs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(), text: None, children }
|
||||
}
|
||||
|
||||
fn t(tag: &str, attrs: &[(&str, &str)], text: &str) -> XfaProp {
|
||||
XfaProp { text: Some(text.into()), ..p(tag, attrs, Vec::new()) }
|
||||
}
|
||||
|
||||
fn node(name: &str, kind: XfaKind, attrs: &[(&str, &str)], elements: Vec<XfaProp>) -> XfaNode {
|
||||
XfaNode {
|
||||
name: name.into(),
|
||||
som: format!("form1[0].{name}[0]"),
|
||||
kind: Some(kind),
|
||||
presence: "visible".into(),
|
||||
props: Arc::new(XfaProps { attrs: attrs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(), elements }),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// form1 > { styled (a field with most properties written), plain (a field with none), label (a
|
||||
/// draw with a text edit), rows (a repeating subform) }
|
||||
fn form() -> XfaNode {
|
||||
let edge = |rgb: &str| p("edge", &[("thickness", "2pt")], vec![p("color", &[("value", rgb)], vec![])]);
|
||||
let styled = node(
|
||||
"styled",
|
||||
XfaKind::Field,
|
||||
&[("name", "styled"), ("w", "2in")],
|
||||
vec![
|
||||
p(
|
||||
"border",
|
||||
&[("presence", "hidden")],
|
||||
vec![
|
||||
edge("255,0,0"),
|
||||
edge("0,0,255"),
|
||||
p("fill", &[], vec![p("color", &[("value", "1,2,3")], vec![]), p("linear", &[("type", "toTop")], vec![])]),
|
||||
],
|
||||
),
|
||||
p("ui", &[], vec![p("choiceList", &[("open", "always")], vec![p("border", &[], vec![edge("9,9,9")])])]),
|
||||
p(
|
||||
"validate",
|
||||
&[("nullTest", "error")],
|
||||
vec![
|
||||
p("message", &[], vec![t("text", &[("name", "nullTest")], "Required"), t("text", &[("name", "scriptTest")], "Too big")]),
|
||||
t("script", &[("contentType", "application/x-javascript")], "this.rawValue > 0"),
|
||||
],
|
||||
),
|
||||
p("font", &[("typeface", "Arial")], vec![]),
|
||||
p("assist", &[], vec![t("toolTip", &[], "Pick one")]),
|
||||
p("format", &[], vec![t("picture", &[], "num{z9}")]),
|
||||
p("value", &[], vec![p("decimal", &[], vec![])]),
|
||||
p("desc", &[], vec![t("text", &[("name", "vMandatory")], "1"), t("text", &[("name", "title")], "Title")]),
|
||||
p(
|
||||
"extras",
|
||||
&[],
|
||||
vec![
|
||||
t("text", &[("name", "note")], "N"),
|
||||
p("extras", &[("name", "arrayGroup")], vec![t("text", &[("name", "groupName")], "A")]),
|
||||
p("extras", &[("name", "arrayGroup")], vec![t("text", &[("name", "groupName")], "B")]),
|
||||
],
|
||||
),
|
||||
p("bind", &[("ref", "$.a")], vec![]),
|
||||
],
|
||||
);
|
||||
let plain = node("plain", XfaKind::Field, &[("name", "plain")], vec![]);
|
||||
let label = node("label", XfaKind::Draw, &[], vec![p("ui", &[], vec![p("textEdit", &[], vec![])])]);
|
||||
let rows = XfaNode {
|
||||
repeatable: true,
|
||||
occur_max: None,
|
||||
..node(
|
||||
"rows",
|
||||
XfaKind::Subform,
|
||||
&[],
|
||||
vec![p("occur", &[("min", "0"), ("max", "-1")], vec![]), p("breakBefore", &[("targetType", "pageArea")], vec![])],
|
||||
)
|
||||
};
|
||||
XfaNode {
|
||||
name: "form1".into(),
|
||||
som: "form1[0]".into(),
|
||||
kind: Some(XfaKind::Subform),
|
||||
children: vec![styled, plain, label, rows],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn go_on(root: &XfaNode, script: &str, target: &str) -> XfaOutcome {
|
||||
let ev = XfaEvent { activity: "click".into(), target: format!("form1[0].{target}[0]"), ..Default::default() };
|
||||
run_xfa(script, &ev, &XfaDoc::default(), root, Limits::default())
|
||||
}
|
||||
|
||||
/// The script's result, failing the test on a script error.
|
||||
fn eval(script: &str, target: &str) -> String {
|
||||
let o = go_on(&form(), script, target);
|
||||
assert_eq!(o.error, None, "{script}: {o:?}");
|
||||
o.result.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Each property the template leaves out reads as the default XFA 3.3 ch. 17 gives it, and is
|
||||
/// not specified.
|
||||
#[test]
|
||||
fn unspecified_properties_read_as_the_spec_defaults() {
|
||||
for (expr, want) in [
|
||||
("this.border.presence", "hidden"),
|
||||
("this.border.edge.presence", "visible"),
|
||||
("this.border.edge.color.value", "0,0,0"),
|
||||
("this.border.getElement('corner', 0).presence", "visible"),
|
||||
("this.border.corner.color.value", "0,0,0"),
|
||||
("this.border.fill.presence", "visible"),
|
||||
("this.border.fill.color.value", "255,255,255"),
|
||||
("this.border.fill.oneOfChild.className", "solid"),
|
||||
("this.font.fill.color.value", "0,0,0"),
|
||||
("this.font.typeface", "Courier"),
|
||||
("this.font.weight", "normal"),
|
||||
("this.para.hAlign", "left"),
|
||||
("this.para.vAlign", "top"),
|
||||
("this.ui.oneOfChild.className", "textEdit"),
|
||||
("this.validate.nullTest", "disabled"),
|
||||
("this.validate.formatTest", "warning"),
|
||||
("this.validate.scriptTest", "error"),
|
||||
("this.occur.min", "1"),
|
||||
("this.occur.max", "1"),
|
||||
("this.occur.initial", "1"),
|
||||
("this.assist.toolTip.value", ""),
|
||||
("this.format.picture.value", ""),
|
||||
("String(this.isPropertySpecified('border'))", "false"),
|
||||
("String(this.border.isPropertySpecified('presence'))", "false"),
|
||||
("String(this.validate.isPropertySpecified('nullTest'))", "false"),
|
||||
// Attributes without a default here read as an empty value that never throws.
|
||||
("this.border.edge.thickness", "0.5pt"),
|
||||
("String(this.para.radixOffset)", ""),
|
||||
("String(this.para.radixOffset == '0in')", "false"),
|
||||
("String(this.para.radixOffset.isPropertySpecified('x'))", "false"),
|
||||
("String(this.breakBefore.targetType) + this.font.posture.value", ""),
|
||||
] {
|
||||
assert_eq!(eval(expr, "plain"), want, "{expr}");
|
||||
}
|
||||
}
|
||||
|
||||
/// What the template writes is what scripts read, and is specified.
|
||||
#[test]
|
||||
fn specified_properties_read_as_the_template_writes_them() {
|
||||
for (expr, want) in [
|
||||
("this.border.presence", "hidden"),
|
||||
("this.border.edge.color.value", "255,0,0"),
|
||||
("this.border.edge.thickness", "2pt"),
|
||||
("this.border.fill.color.value", "1,2,3"),
|
||||
("this.border.fill.oneOfChild.className", "linear"),
|
||||
("this.border.fill.oneOfChild.type", "toTop"),
|
||||
("this.ui.oneOfChild.className", "choiceList"),
|
||||
("this.ui.oneOfChild.open", "always"),
|
||||
("this.validate.nullTest", "error"),
|
||||
("this.validate.message.text.value", "Required"),
|
||||
("this.validate.message.getElement('text', 1).value", "Too big"),
|
||||
("this.validate.message.nodes.length", "2"),
|
||||
("this.validate.message.nodes.item(1).name", "scriptTest"),
|
||||
("this.validate.message.nodes.namedItem('text').value", "Required"),
|
||||
("this.font.typeface", "Arial"),
|
||||
("this.assist.toolTip.value", "Pick one"),
|
||||
("this.format.picture.value", "num{z9}"),
|
||||
("String(this.format.picture)", "num{z9}"),
|
||||
("this.getElement('value', 0).oneOfChild.className", "decimal"),
|
||||
("String(this.isPropertySpecified('border'))", "true"),
|
||||
("String(this.isPropertySpecified('caption'))", "false"),
|
||||
("String(this.isPropertySpecified('w'))", "true"),
|
||||
("String(this.border.isPropertySpecified('presence'))", "true"),
|
||||
("String(this.border.isPropertySpecified('break'))", "false"),
|
||||
("String(this.border.isPropertySpecified('fill'))", "true"),
|
||||
("String(this.border.isPropertySpecified('corner'))", "false"),
|
||||
("this.getAttribute('w')", "2in"),
|
||||
("this.getAttribute('name')", "styled"),
|
||||
("this.getAttribute('presence')", "visible"),
|
||||
("this.getAttribute('nothing')", ""),
|
||||
("this.border.getAttribute('presence')", "hidden"),
|
||||
("this.border.parent.className", "field"),
|
||||
("this.border.edge.parent.className", "border"),
|
||||
] {
|
||||
assert_eq!(eval(expr, "styled"), want, "{expr}");
|
||||
}
|
||||
assert_eq!(eval("this.occur.min + ',' + this.occur.max", "rows"), "0,-1");
|
||||
assert_eq!(eval("this.breakBefore.targetType", "rows"), "pageArea");
|
||||
}
|
||||
|
||||
/// `getElement(name, index)` reaches each occurrence the template lists, up to what the spec
|
||||
/// allows (four edges), and nothing beyond. An edge or corner past those listed reads as the
|
||||
/// one drawn on that side (as readers draw two edges 0, 1, 0, 1, three 0, 1, 2, 1 and one on
|
||||
/// every side), comes into being as a copy of it when reached, and a write then changes only
|
||||
/// it; with none listed, each is a default one (edge p. 687, corner p. 663).
|
||||
#[test]
|
||||
fn get_element_reaches_occurrences_by_index() {
|
||||
assert_eq!(eval("this.border.getElement('edge', 0).color.value", "styled"), "255,0,0");
|
||||
assert_eq!(eval("this.border.getElement('edge', 1).color.value", "styled"), "0,0,255");
|
||||
assert_eq!(eval("this.border.getElement('edge').color.value", "styled"), "255,0,0");
|
||||
let edge3 = "var e = this.border.getElement('edge', 3); [e.color.value, e.thickness, e.presence, e.stroke, e.cap].join('|')";
|
||||
assert_eq!(eval(edge3, "styled"), "0,0,255|2pt|visible|solid|square");
|
||||
assert_eq!(eval("this.border.resolveNode('edge[2]').color.value", "styled"), "255,0,0");
|
||||
assert_eq!(eval("this.border.getElement('edge', 3).somExpression", "styled"), "form1[0].styled[0].border[0].edge[3]");
|
||||
let corner2 = "var c = this.border.getElement('corner', 2); \
|
||||
[c.color.value, c.thickness, c.presence, c.stroke, c.radius, c.join, c.inverted].join('|')";
|
||||
assert_eq!(eval(corner2, "styled"), "0,0,0|0.5pt|visible|solid|0in|square|0");
|
||||
assert_eq!(eval("this.border.getElement('edge', 3).color.value", "plain"), "0,0,0");
|
||||
// A write to an edge past those listed changes only it.
|
||||
let all = "[0, 1, 2, 3].map(function (i) { return this.border.getElement('edge', i).color.value; }, this).join('|')";
|
||||
assert_eq!(eval(&format!("this.border.getElement('edge', 3).color.value = '9,9,9'; {all}"), "styled"), "255,0,0|0,0,255|255,0,0|9,9,9");
|
||||
assert_eq!(eval("this.border.getElement('edge', 2).thickness = '3pt'; this.border.getElement('edge', 0).thickness", "styled"), "2pt");
|
||||
// Reached, it is a copy: a later write to the edge it was drawn from leaves it.
|
||||
let copy = "var e = this.border.getElement('edge', 3); this.border.getElement('edge', 1).color.value = '5,5,5'; e.color.value";
|
||||
assert_eq!(eval(copy, "styled"), "0,0,255");
|
||||
// With no edge listed, each index is a default edge of its own.
|
||||
assert_eq!(eval("this.border.getElement('edge', 2).color.value = '1,1,1'; this.border.edge.color.value", "plain"), "0,0,0");
|
||||
// One edge listed is drawn on every side; three reuse the second for the fourth.
|
||||
let edges = |colors: &[&str]| {
|
||||
let listed = colors.iter().map(|c| p("edge", &[], vec![p("color", &[("value", c)], vec![])])).collect();
|
||||
let f = node("e", XfaKind::Field, &[], vec![p("border", &[], listed)]);
|
||||
let mut root = form();
|
||||
root.children.push(f);
|
||||
let o = go_on(&root, all, "e");
|
||||
assert_eq!(o.error, None, "{o:?}");
|
||||
o.result.unwrap_or_default()
|
||||
};
|
||||
assert_eq!(edges(&["1,1,1"]), "1,1,1|1,1,1|1,1,1|1,1,1");
|
||||
assert_eq!(edges(&["1,1,1", "2,2,2", "3,3,3"]), "1,1,1|2,2,2|3,3,3|2,2,2");
|
||||
for gone in ["this.border.getElement('edge', 4)", "this.border.getElement('edge', -1)", "this.border.getElement('nothing', 0)"] {
|
||||
assert_eq!(eval(&format!("String({gone})"), "styled"), "null", "{gone}");
|
||||
}
|
||||
assert_eq!(eval("this.getElement('border', 0).presence", "styled"), "hidden");
|
||||
}
|
||||
|
||||
/// Each widget is the `ui`'s one-of child, by its class name.
|
||||
#[test]
|
||||
fn one_of_child_names_each_widget() {
|
||||
for widget in [
|
||||
"barcode",
|
||||
"button",
|
||||
"checkButton",
|
||||
"choiceList",
|
||||
"dateTimeEdit",
|
||||
"defaultUi",
|
||||
"imageEdit",
|
||||
"numericEdit",
|
||||
"passwordEdit",
|
||||
"signature",
|
||||
"textEdit",
|
||||
] {
|
||||
let f = node("w", XfaKind::Field, &[], vec![p("ui", &[], vec![p("picture", &[], vec![]), p(widget, &[], vec![])])]);
|
||||
let root = XfaNode { name: "form1".into(), som: "form1[0]".into(), kind: Some(XfaKind::Subform), children: vec![f], ..Default::default() };
|
||||
let o = go_on(&root, &format!("this.ui.oneOfChild.className == '{widget}'"), "w");
|
||||
assert_eq!((o.error.as_deref(), o.result.as_deref()), (None, Some("true")), "{widget}");
|
||||
}
|
||||
assert_eq!(eval("this.ui.oneOfChild.className == 'choiceList'", "styled"), "true");
|
||||
assert_eq!(eval("this.ui.oneOfChild.className == 'choiceList'", "plain"), "false");
|
||||
}
|
||||
|
||||
/// script-errors C04a: the forms' reads through widgets, edges and validations compare as
|
||||
/// strings and throw nothing, whether the template writes the property or not.
|
||||
#[test]
|
||||
fn widget_border_and_validate_reads_throw_no_type_error() {
|
||||
for target in ["styled", "plain", "label"] {
|
||||
let script = "var c = this.ui.oneOfChild.border.getElement('edge', 0).color.value; \
|
||||
var n = this.validate.nullTest == 'error'; \
|
||||
var w = this.ui.oneOfChild.className == 'choiceList'; \
|
||||
c + '|' + n + '|' + w";
|
||||
let o = go_on(&form(), script, target);
|
||||
assert_eq!(o.error, None, "{target}: {o:?}");
|
||||
let want = match target {
|
||||
"styled" => "9,9,9|true|true",
|
||||
_ => "0,0,0|false|false",
|
||||
};
|
||||
assert_eq!(o.result.as_deref(), Some(want), "{target}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A script's writes are read back later in the same run, make the property specified, and
|
||||
/// change nothing on the form (no effect comes back).
|
||||
#[test]
|
||||
fn writes_are_read_back_in_the_run_and_have_no_effect() {
|
||||
let script = "this.border.fill.color.value = '255,0,0'; \
|
||||
this.validate.nullTest = 'error'; \
|
||||
this.assist.toolTip.value = 'Now'; \
|
||||
this.font.setAttribute('bold', 'weight'); \
|
||||
[this.border.fill.color.value, this.validate.nullTest, this.assist.toolTip.value, this.font.weight, \
|
||||
this.isPropertySpecified('border'), this.border.isPropertySpecified('fill'), this.validate.isPropertySpecified('nullTest'), \
|
||||
this.font.getAttribute('weight')].join('|')";
|
||||
let o = go_on(&form(), script, "plain");
|
||||
assert_eq!(o.error, None, "{o:?}");
|
||||
assert_eq!(o.result.as_deref(), Some("255,0,0|error|Now|bold|true|true|true|bold"));
|
||||
assert!(o.effects.is_empty(), "{o:?}");
|
||||
// Another object is untouched, and a fresh run starts from the template again.
|
||||
assert_eq!(eval("this.border.fill.color.value", "label"), "255,255,255");
|
||||
assert_eq!(eval("this.validate.nullTest", "plain"), "disabled");
|
||||
}
|
||||
|
||||
/// form1 > { label (a field captioned "Name:"), rich (a field whose caption is rich text),
|
||||
/// list (a choice list with shown and saved items), box (a check button with on and off values) }
|
||||
fn caption_form() -> XfaNode {
|
||||
let caption =
|
||||
|content: XfaProp| p("caption", &[("reserve", "1in")], vec![p("value", &[], vec![content]), p("font", &[("weight", "bold")], vec![])]);
|
||||
let items = |attrs: &[(&str, &str)], tag: &str, entries: &[&str]| p("items", attrs, entries.iter().map(|e| t(tag, &[], e)).collect());
|
||||
let fields = vec![
|
||||
node("label", XfaKind::Field, &[], vec![caption(t("text", &[], "Name:"))]),
|
||||
node("rich", XfaKind::Field, &[], vec![caption(t("exData", &[("contentType", "text/html")], "Date of birth:"))]),
|
||||
node("list", XfaKind::Field, &[], vec![items(&[], "text", &["A", "B", "C"]), items(&[("save", "1")], "text", &["1", "2", "3"])]),
|
||||
node("box", XfaKind::Field, &[], vec![items(&[], "integer", &["1", "0"])]),
|
||||
node("bare", XfaKind::Field, &[], vec![]),
|
||||
];
|
||||
XfaNode { name: "form1".into(), som: "form1[0]".into(), kind: Some(XfaKind::Subform), children: fields, ..Default::default() }
|
||||
}
|
||||
|
||||
fn eval_on(root: &XfaNode, script: &str, target: &str) -> String {
|
||||
let o = go_on(root, script, target);
|
||||
assert_eq!(o.error, None, "{script}: {o:?}");
|
||||
o.result.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// A caption is a property object (XFA 3.3 p. 643): its value element holds the label, as text
|
||||
/// or rich text, read by name, by class, through `resolveNode` and `oneOfChild`; the content it
|
||||
/// doesn't hold reads as null (one of them, p. 598); its rich text reads as its words.
|
||||
#[test]
|
||||
fn a_caption_reads_its_label_as_text_or_rich_text() {
|
||||
let root = caption_form();
|
||||
for (expr, want) in [
|
||||
("this.caption.value.text.value", "Name:"),
|
||||
("this.caption.value['#text'].value", "Name:"),
|
||||
("this.caption.value.resolveNode('#text').value", "Name:"),
|
||||
("this.caption.value.oneOfChild.className", "text"),
|
||||
("xfa.resolveNode('xfa.form.form1.label.caption.value.oneOfChild').value", "Name:"),
|
||||
("String(this.caption.value.exData)", "null"),
|
||||
("String(this.caption.value.resolveNode('#exData'))", "null"),
|
||||
("this.caption.value.text.value.substr(0, 4) + this.caption.value.text.value.indexOf(':')", "Name4"),
|
||||
(
|
||||
"this.caption.reserve + '|' + this.caption.placement + '|' + this.caption.presence + '|' + this.caption.font.weight",
|
||||
"1in|left|visible|bold",
|
||||
),
|
||||
("this.caption.className + '|' + this.caption.value.className", "caption|value"),
|
||||
] {
|
||||
assert_eq!(eval_on(&root, expr, "label"), want, "{expr}");
|
||||
}
|
||||
for (expr, want) in [
|
||||
("this.caption.value.exData.value", "Date of birth:"),
|
||||
("this.caption.value['#exData'].value.length", "14"),
|
||||
("this.caption.value.resolveNode('#exData').value.indexOf(':')", "13"),
|
||||
("this.caption.value.resolveNode('exData').value.replace(':', '* :')", "Date of birth* :"),
|
||||
("this.caption.value.oneOfChild.className", "exData"),
|
||||
("String(this.caption.value.text)", "null"),
|
||||
("(this.caption.value.exData) ? this.caption.value.exData.value : this.caption.value.text.value", "Date of birth:"),
|
||||
] {
|
||||
assert_eq!(eval_on(&root, expr, "rich"), want, "{expr}");
|
||||
}
|
||||
// Its methods to load and save rich text are not modelled yet: calling them changes nothing
|
||||
// and throws nothing.
|
||||
let o = go_on(&root, "this.caption.value.exData.loadXML('<body/>', false, true); typeof this.caption.value.exData.saveXML('pretty')", "rich");
|
||||
assert_eq!((o.error, o.result.as_deref(), o.effects.len()), (None, Some("string"), 0));
|
||||
// A caption the template doesn't give reads its defaults; a write reads back in the run.
|
||||
assert_eq!(eval_on(&root, "this.caption.placement + '|' + this.caption.value.oneOfChild.className", "bare"), "left|text");
|
||||
assert_eq!(eval_on(&root, "this.caption.value.text.value = 'Surname:'; this.caption.value.text.value", "label"), "Surname:");
|
||||
}
|
||||
|
||||
/// A field's items are a property object (XFA 3.3 p. 758) whose entries `nodes` lists, each
|
||||
/// reading as the list or check button shows it; a write reads back in the run.
|
||||
#[test]
|
||||
fn items_list_their_entries_as_the_list_shows_them() {
|
||||
let root = caption_form();
|
||||
for (expr, want) in [
|
||||
("this.items.nodes.length", "3"),
|
||||
("this.items.nodes.item(1).value", "B"),
|
||||
("this.items.nodes.item(2).className", "text"),
|
||||
("String(this.items.nodes.item.value)", "undefined"),
|
||||
("this.getElement('items', 1).save + '|' + this.getElement('items', 1).nodes.item(0).value", "1|1"),
|
||||
// The first `<items>` is the shown list; the second is reached by index.
|
||||
("typeof this.items.save + ':' + this.items.save", "string:0"),
|
||||
("this.resolveNode('items[1]').nodes.item(1).value", "2"),
|
||||
("this.resolveNodes('items[*]').length", "2"),
|
||||
] {
|
||||
assert_eq!(eval_on(&root, expr, "list"), want, "{expr}");
|
||||
}
|
||||
assert_eq!(eval_on(&root, "this.items.nodes.item(0).value + '|' + this.items.nodes.item(1).value", "box"), "1|0");
|
||||
let script = "if (this.items.nodes.item(0).value == '1') this.items.nodes.item(0).value = 'On'; this.items.nodes.item(0).value";
|
||||
assert_eq!(eval_on(&root, script, "box"), "On");
|
||||
// A list without `<items>`: an empty items object, no error.
|
||||
assert_eq!(eval_on(&root, "this.items.nodes.length + '|' + this.resolveNodes('items').length", "bare"), "0|1");
|
||||
}
|
||||
|
||||
/// The items reads come from the list state (`HNode::items`) once one is set: the hook for
|
||||
/// PdfCraft's choice-list methods (`addItem`, `clearItems`; the Series 1 work, not on main yet).
|
||||
/// Nothing sets it in the product yet, so this test sets it directly.
|
||||
#[test]
|
||||
fn items_read_the_list_state_once_one_is_set() {
|
||||
use super::{ItemList, content, list_entries, occurrences};
|
||||
let doc = XfaDoc::default();
|
||||
let mut h = super::super::XHost::new(caption_form(), "form1[0].list[0]", &doc);
|
||||
let list = h.current;
|
||||
assert_eq!(list_entries(&h, list, 0).len(), 3, "as the template lists them");
|
||||
let entries = vec![("text".to_string(), "One".to_string()), ("text".to_string(), "Two".to_string())];
|
||||
if let Some(n) = h.nodes.get_mut(list) {
|
||||
n.items = Some(vec![ItemList { entries }]);
|
||||
}
|
||||
assert_eq!(list_entries(&h, list, 0).len(), 2);
|
||||
assert_eq!(content(&h, list, "items#0/text#1"), "Two");
|
||||
assert_eq!(occurrences(&h, list, "", "items"), 1);
|
||||
// A list that had no `<items>` and got one entry.
|
||||
let mut h = super::super::XHost::new(caption_form(), "form1[0].bare[0]", &doc);
|
||||
let bare = h.current;
|
||||
assert_eq!(list_entries(&h, bare, 0).len(), 0);
|
||||
if let Some(n) = h.nodes.get_mut(bare) {
|
||||
n.items = Some(vec![ItemList { entries: vec![("text".to_string(), "X".to_string())] }]);
|
||||
}
|
||||
assert_eq!((list_entries(&h, bare, 0).len(), content(&h, bare, "items#0/text#0")), (1, "X".to_string()));
|
||||
}
|
||||
|
||||
/// A script can't grow the run past its limits: paths deeper than [`super::MAX_PATH`] take
|
||||
/// anything (a template nested deeper than the parser keeps), and one property keeps at most
|
||||
/// [`super::MAX_WRITTEN_ATTRS`] written attributes, with a note.
|
||||
#[test]
|
||||
fn deep_paths_and_many_writes_stay_bounded() {
|
||||
let mut deep = p("x", &[], Vec::new());
|
||||
for _ in 0..20 {
|
||||
deep = p("x", &[], vec![deep]);
|
||||
}
|
||||
let f = node("deep", XfaKind::Field, &[], vec![deep]);
|
||||
let root = XfaNode { name: "form1".into(), som: "form1[0]".into(), kind: Some(XfaKind::Subform), children: vec![f], ..Default::default() };
|
||||
let script = "var o = this.getElement('x', 0); var tags = []; for (var i = 0; i < 12; i++) { tags.push(String(o.className)); o.y = 1; o = o.x; } tags.join(',')";
|
||||
let o = go_on(&root, script, "deep");
|
||||
assert_eq!(o.error, None, "{o:?}");
|
||||
assert_eq!(o.result.as_deref(), Some("x,x,x,x,x,x,x,x,,,,"));
|
||||
let script = "for (var i = 0; i < 1000; i++) { this.font.setAttribute('v' + i, 'a' + i); } this.font.getAttribute('a63') + '|' + this.font.getAttribute('a64')";
|
||||
let o = go_on(&form(), script, "plain");
|
||||
assert_eq!(o.error, None, "{o:?}");
|
||||
assert_eq!(o.result.as_deref(), Some("v63|"));
|
||||
assert!(o.notes.iter().any(|n| n.contains("936 more")), "{o:?}");
|
||||
}
|
||||
|
||||
/// The other reads forms make: children by name (`desc.vMandatory`, a message's `scriptTest`
|
||||
/// text), properties by class (`ui.#textEdit`), `resolveNode` from a property, `somExpression`,
|
||||
/// `nodes.namedItem` by name, and a node's `mandatory` and validation messages, with their
|
||||
/// defaults where the template has none.
|
||||
#[test]
|
||||
fn named_children_class_names_and_validation_members_read_as_forms_use_them() {
|
||||
for (expr, want) in [
|
||||
("this.desc.vMandatory.value", "1"),
|
||||
("this.desc.nodes.namedItem('title').value", "Title"),
|
||||
("this.extras.nodes.namedItem('note').value", "N"),
|
||||
("this.extras.note.value", "N"),
|
||||
("this.bind.ref", "$.a"),
|
||||
("this.validate.message.scriptTest.value", "Too big"),
|
||||
("this.validate.message.nullTest.value", "Required"),
|
||||
("this.ui['#choiceList'].open", "always"),
|
||||
("this.ui.resolveNode('#choiceList').className", "choiceList"),
|
||||
("this.ui.resolveNode('#choiceList.border.edge').color.value", "9,9,9"),
|
||||
("this.border.resolveNodes('edge[*]').length", "2"),
|
||||
("String(this.ui.resolveNode('#textEdit'))", "null"),
|
||||
("this.ui.nodes.item(0).className", "choiceList"),
|
||||
("this.border.edge.somExpression", "form1[0].styled[0].border[0].edge[0]"),
|
||||
("this.mandatory", "error"),
|
||||
("this.validationMessage", "Too big"),
|
||||
("this.mandatoryMessage", "Required"),
|
||||
("this.formatMessage", ""),
|
||||
("this.validate.script.value", "this.rawValue > 0"),
|
||||
// SOM expressions from a node that run into its properties.
|
||||
("this.resolveNode('ui.#choiceList').className", "choiceList"),
|
||||
("this.resolveNode('border.edge[1]').color.value", "0,0,255"),
|
||||
("xfa.resolveNode('xfa.form.form1.styled.border.edge[0].color').value", "255,0,0"),
|
||||
("xfa.form.form1.resolveNode('styled.ui.#choiceList.border.edge').color.value", "9,9,9"),
|
||||
("this.resolveNodes('border.edge[*]').length", "2"),
|
||||
("String(this.resolveNode('nothing.edge'))", "null"),
|
||||
("String(this.resolveNode('ui.#textEdit'))", "null"),
|
||||
("this.resolveNodes('extras.arrayGroup[*].groupName').length", "2"),
|
||||
("this.resolveNodes('extras.arrayGroup[*].groupName').item(1).value", "B"),
|
||||
("this.resolveNode('ui.*.border.edge').color.value", "9,9,9"),
|
||||
("this.fillColor + '|' + this.borderColor + '|' + this.fontColor", "1,2,3|255,0,0|0,0,0"),
|
||||
("this.fillColor = '255,0,0'; this.fillColor + '|' + this.border.fill.color.value", "255,0,0|255,0,0"),
|
||||
("this.desc.rawValue", "1"),
|
||||
("this.validate.message.text.rawValue", "Required"),
|
||||
] {
|
||||
assert_eq!(eval(expr, "styled"), want, "{expr}");
|
||||
}
|
||||
for (expr, want) in [
|
||||
("this.mandatory", "disabled"),
|
||||
("this.validationMessage", ""),
|
||||
("this.validationMessage.split('x').length", "1"),
|
||||
("this.validate.script.value", ""),
|
||||
("this.desc.rawValue", ""),
|
||||
("this.fillColor + '|' + this.borderColor + '|' + this.fontColor", "255,255,255|0,0,0|0,0,0"),
|
||||
("this.margin.leftInset", "0in"),
|
||||
("this.break.after", "auto"),
|
||||
("this.ui.textEdit.comb.numberOfCells", "0"),
|
||||
("this.ui['#textEdit'].border.fill.color.value", "255,255,255"),
|
||||
("this.bind.ref", ""),
|
||||
("String(this.desc.vMandatory.value)", ""),
|
||||
("this.border.fill.oneOfChild.color.value", ""),
|
||||
] {
|
||||
assert_eq!(eval(expr, "plain"), want, "{expr}");
|
||||
}
|
||||
// A gradient without a type runs to the right (XFA 3.3 p. 770).
|
||||
let f = node("g", XfaKind::Field, &[], vec![p("border", &[], vec![p("fill", &[], vec![p("linear", &[], vec![])])])]);
|
||||
let root = XfaNode { name: "form1".into(), som: "form1[0]".into(), kind: Some(XfaKind::Subform), children: vec![f], ..Default::default() };
|
||||
let o = go_on(&root, "this.border.fill.oneOfChild.type + '|' + this.border.fill.oneOfChild.color.value", "g");
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some("toRight|0,0,0")));
|
||||
// Writes are read back in the run, and only a known nullTest is taken.
|
||||
let o = go_on(
|
||||
&form(),
|
||||
"this.mandatory = 'error'; this.validationMessage = 'Bad'; var a = this.mandatory + this.validate.nullTest + this.validationMessage; this.mandatory = 'nonsense'; a + this.mandatory",
|
||||
"plain",
|
||||
);
|
||||
assert_eq!((o.error, o.result.as_deref(), o.effects.len()), (None, Some("errorerrorBaderror"), 0));
|
||||
}
|
||||
|
||||
/// A validation message set through the node is the message's text of that name, read back
|
||||
/// both ways; `rawValue` set on a property reads back where it is read from.
|
||||
#[test]
|
||||
fn messages_and_raw_values_written_read_back_from_their_property() {
|
||||
for (target, want) in [("plain", "Bad|Bad|Soon"), ("styled", "Bad|Bad|Soon")] {
|
||||
let script = "this.validationMessage = 'Bad'; this.mandatoryMessage = 'Soon'; \
|
||||
[this.validationMessage, this.validate.message.scriptTest.value, this.validate.message.nullTest.value].join('|')";
|
||||
let o = go_on(&form(), script, target);
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some(want)), "{target}");
|
||||
}
|
||||
assert_eq!(eval("this.validate.message.scriptTest.value = 'Mine'; this.validationMessage", "styled"), "Mine");
|
||||
for (expr, want) in [
|
||||
("this.desc.rawValue = 'Z'; this.desc.rawValue + this.desc.vMandatory.value", "ZZ"),
|
||||
("this.border.fill.color.rawValue = '5,5,5'; this.border.fill.color.value + '|' + this.border.fill.color.rawValue", "5,5,5|5,5,5"),
|
||||
("this.assist.toolTip.rawValue = 'T'; this.assist.toolTip.value", "T"),
|
||||
] {
|
||||
assert_eq!(eval(expr, "styled"), want, "{expr}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A child named like a property comes first, for every property name, as XFA SOM resolves
|
||||
/// name clashes in favour of the explicitly named object; `#name` reaches the property (XFA
|
||||
/// 3.3 p. 99).
|
||||
#[test]
|
||||
fn a_child_named_like_a_property_comes_first() {
|
||||
let mut parent = node("box", XfaKind::Subform, &[], vec![p("border", &[("presence", "hidden")], vec![]), p("occur", &[("min", "2")], vec![])]);
|
||||
parent.children = ["format", "desc", "border", "occur", "mandatory"]
|
||||
.iter()
|
||||
.map(|n| XfaNode { som: format!("form1[0].box[0].{n}[0]"), ..node(n, XfaKind::Field, &[], vec![]) })
|
||||
.collect();
|
||||
let root = XfaNode { name: "form1".into(), som: "form1[0]".into(), kind: Some(XfaKind::Subform), children: vec![parent], ..Default::default() };
|
||||
let script = "[this.format.className, this.desc.className, this.border.className, this.occur.className, this.mandatory.className, \
|
||||
this['#format'].className, this['#border'].presence, this['#occur'].min].join('|')";
|
||||
let o = go_on(&root, script, "box");
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some("field|field|field|field|field|format|hidden|2")));
|
||||
}
|
||||
|
||||
/// `resolveNodes` that runs through every instance into a property gives each instance's.
|
||||
#[test]
|
||||
fn resolve_nodes_through_instances_gives_each_instances_property() {
|
||||
let row = |i: usize, rgb: &str| XfaNode {
|
||||
som: format!("form1[0].row[{i}]"),
|
||||
index: i,
|
||||
repeatable: true,
|
||||
..node("row", XfaKind::Subform, &[], vec![p("border", &[], vec![p("fill", &[], vec![p("color", &[("value", rgb)], vec![])])])])
|
||||
};
|
||||
let root = XfaNode {
|
||||
name: "form1".into(),
|
||||
som: "form1[0]".into(),
|
||||
kind: Some(XfaKind::Subform),
|
||||
children: vec![row(0, "1,1,1"), row(1, "2,2,2")],
|
||||
..Default::default()
|
||||
};
|
||||
let script = "var l = xfa.resolveNodes('xfa.form.form1.row[*].border.fill.color'); l.length + '|' + l.item(0).value + '|' + l.item(1).value";
|
||||
let o = go_on(&root, script, "row");
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some("2|1,1,1|2,2,2")));
|
||||
}
|
||||
|
||||
/// `keep` reads as the template writes it, else with its defaults (XFA 3.3 pp. 762-763):
|
||||
/// `intact` is `contentArea` for a draw and a positioned or row subform, `none` for a field and
|
||||
/// a flowing subform; `next` and `previous` are `none`. A write reads back in the run.
|
||||
#[test]
|
||||
fn keep_reads_its_context_dependent_defaults_and_writes_read_back() {
|
||||
let kept = node("kept", XfaKind::Field, &[], vec![p("keep", &[("intact", "pageArea"), ("next", "contentArea")], vec![])]);
|
||||
let flowing = node("flowing", XfaKind::Subform, &[("layout", "tb")], vec![]);
|
||||
let row = node("arow", XfaKind::Subform, &[("layout", "row")], vec![]);
|
||||
let mut root = form();
|
||||
root.children.extend([kept, flowing, row]);
|
||||
let read = |target: &str, script: &str| {
|
||||
let o = go_on(&root, script, target);
|
||||
assert_eq!(o.error, None, "{target}: {o:?}");
|
||||
o.result.unwrap_or_default()
|
||||
};
|
||||
let all = "[this.keep.intact, this.keep.next, this.keep.previous, this.keep.isPropertySpecified('intact')].join('|')";
|
||||
for (target, want) in [
|
||||
("plain", "none|none|none|false"),
|
||||
("label", "contentArea|none|none|false"),
|
||||
("rows", "contentArea|none|none|false"),
|
||||
("flowing", "none|none|none|false"),
|
||||
("arow", "contentArea|none|none|false"),
|
||||
("kept", "pageArea|contentArea|none|true"),
|
||||
] {
|
||||
assert_eq!(read(target, all), want, "{target}");
|
||||
}
|
||||
assert_eq!(
|
||||
read("plain", "this.keep.intact = 'contentArea'; this.keep.intact + '|' + this.keep.isPropertySpecified('intact')"),
|
||||
"contentArea|true"
|
||||
);
|
||||
}
|
||||
|
||||
/// A single unnamed message text is every validation's message (XFA 3.3 p. 782), and setting
|
||||
/// a test's message overwrites it, as readers do; with several texts and none named for the
|
||||
/// test, a text of that name is added. A test's text that doesn't exist can't be read: readers
|
||||
/// raise an error, which a script can catch.
|
||||
#[test]
|
||||
fn a_single_unnamed_message_text_serves_every_validation() {
|
||||
let f = node("one", XfaKind::Field, &[], vec![p("validate", &[], vec![p("message", &[], vec![t("text", &[], "Fix it")])])]);
|
||||
let two = node(
|
||||
"two",
|
||||
XfaKind::Field,
|
||||
&[],
|
||||
vec![p("validate", &[], vec![p("message", &[], vec![t("text", &[("name", "nullTest")], "N"), t("text", &[], "U")])])],
|
||||
);
|
||||
let mut root = form();
|
||||
root.children.extend([f, two]);
|
||||
let o = go_on(&root, "[this.validationMessage, this.formatMessage, this.mandatoryMessage].join('|')", "one");
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some("Fix it|Fix it|Fix it")));
|
||||
let script = "this.validationMessage = 'New'; \
|
||||
[this.validationMessage, this.formatMessage, this.mandatoryMessage, this.validate.message.nodes.length, this.validate.message.text.value].join('|')";
|
||||
let o = go_on(&root, script, "one");
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some("New|New|New|1|New")));
|
||||
let o = go_on(&root, "try { this.validate.message.scriptTest.value } catch (e) { 'caught ' + (e instanceof TypeError) }", "one");
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some("caught true")));
|
||||
let o = go_on(
|
||||
&root,
|
||||
"this.validationMessage = 'S'; [this.validationMessage, this.mandatoryMessage, this.validate.message.scriptTest.value].join('|')",
|
||||
"two",
|
||||
);
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some("S|N|S")));
|
||||
// A name a script gives the text counts: it no longer serves every test.
|
||||
let o = go_on(&root, "this.validate.message.text.name = 'scriptTest'; this.validationMessage + '|' + this.mandatoryMessage", "one");
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some("Fix it|")));
|
||||
}
|
||||
|
||||
/// A write to a key that names a child sets that child's value, as a read gets the child.
|
||||
#[test]
|
||||
fn a_write_to_a_named_child_sets_its_value() {
|
||||
let f = node("named", XfaKind::Field, &[], vec![p("extras", &[("id", "attr")], vec![t("text", &[("name", "id")], "X")])]);
|
||||
let mut root = form();
|
||||
root.children.push(f);
|
||||
let o = go_on(&root, "this.extras.id = 'Y'; this.extras.id.value + '|' + this.extras.getAttribute('id')", "named");
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some("Y|attr")));
|
||||
}
|
||||
|
||||
/// A property's child of a name comes before an attribute or a property of that name, as for
|
||||
/// nodes (XFA 3.3 p. 99), and a node's child named like a member not modelled yet comes before
|
||||
/// the member.
|
||||
#[test]
|
||||
fn named_children_come_before_attributes_and_unmodelled_members() {
|
||||
let f = node(
|
||||
"named",
|
||||
XfaKind::Field,
|
||||
&[],
|
||||
vec![
|
||||
p("extras", &[("id", "attr")], vec![t("text", &[("name", "id")], "X")]),
|
||||
p("validate", &[], vec![p("message", &[], vec![t("text", &[("name", "scriptTest")], "A"), t("text", &[("name", "text")], "B")])]),
|
||||
],
|
||||
);
|
||||
let mut parent = node("box", XfaKind::Subform, &[], vec![]);
|
||||
parent.children = ["items", "event", "locale"]
|
||||
.iter()
|
||||
.map(|n| XfaNode { som: format!("form1[0].box[0].{n}[0]"), ..node(n, XfaKind::Field, &[], vec![]) })
|
||||
.collect();
|
||||
let mut root = form();
|
||||
root.children.extend([f, parent]);
|
||||
let o = go_on(&root, "this.extras.id.value + '|' + this.extras.getAttribute('id') + '|' + this.validate.message.text.value", "named");
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some("X|attr|B")));
|
||||
let o = go_on(&root, "[this.items.className, this.event.className, this.locale.className].join('|')", "box");
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some("field|field|field")));
|
||||
// Without such a child the name reaches the property: a field with no `<items>` reads an
|
||||
// items object with no entries.
|
||||
let o = go_on(&root, "this.items.className + '|' + this.items.nodes.length", "plain");
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some("items|0")));
|
||||
}
|
||||
|
||||
/// `borderColor` set colours all four edges, the ones not listed made first as copies of the
|
||||
/// edge drawn there. Read, it is edge 0's.
|
||||
#[test]
|
||||
fn border_color_colours_all_four_edges() {
|
||||
let all = "[0, 1, 2, 3].map(function (i) { return this.border.getElement('edge', i).color.value; }, this).concat([this.borderColor]).join('|')";
|
||||
for target in ["styled", "plain"] {
|
||||
assert_eq!(eval(&format!("this.borderColor = '7,7,7'; {all}"), target), "7,7,7|7,7,7|7,7,7|7,7,7|7,7,7", "{target}");
|
||||
}
|
||||
// The edges made are copies of those drawn there: only their colour changed.
|
||||
assert_eq!(eval("this.borderColor = '7,7,7'; this.border.getElement('edge', 3).thickness", "styled"), "2pt");
|
||||
}
|
||||
|
||||
/// A border's presence reads `hidden` when the object has no `<border>` element, as readers
|
||||
/// read it, and `visible` when it has one that doesn't say (XFA 3.3 p. 629).
|
||||
#[test]
|
||||
fn border_presence_reads_hidden_without_a_border_element() {
|
||||
let f = node("bordered", XfaKind::Field, &[], vec![p("border", &[], vec![])]);
|
||||
let mut root = form();
|
||||
root.children.push(f);
|
||||
for (target, want) in [("plain", "hidden"), ("label", "hidden"), ("bordered", "visible"), ("styled", "hidden")] {
|
||||
let o = go_on(&root, "this.border.presence", target);
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some(want)), "{target}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A border has four sides (edges and corners occur at most four times, XFA 3.3 p. 629): an
|
||||
/// index past them is no element, from a script or from inside the host, and nothing is made
|
||||
/// for it.
|
||||
#[test]
|
||||
fn edges_and_corners_past_the_fourth_are_none() {
|
||||
for script in [
|
||||
"String(this.border.getElement('edge', 7))",
|
||||
"String(this.border.resolveNode('edge[7]'))",
|
||||
"String(this.border.resolveNode('corner[5]'))",
|
||||
"String(xfa.resolveNode('xfa.form.form1.styled.border.edge[4]'))",
|
||||
] {
|
||||
assert_eq!(eval(script, "styled"), "null", "{script}");
|
||||
}
|
||||
let doc = XfaDoc::default();
|
||||
let mut h = super::super::XHost::new(form(), "form1[0].styled[0]", &doc);
|
||||
let styled = h.current;
|
||||
h.instantiate(styled, "border#0/edge#7");
|
||||
h.instantiate(styled, "border#0/corner#5/color#0");
|
||||
h.instantiate(styled, "border#0/edge#3");
|
||||
let made: Vec<String> = h.prop_writes.get(&styled).map(|m| m.keys().cloned().collect()).unwrap_or_default();
|
||||
assert!(made.iter().all(|p| !p.contains("edge#7") && !p.contains("corner#5")), "{made:?}");
|
||||
assert!(made.iter().any(|p| p.starts_with("border#0/edge#3")), "a side within the four is made: {made:?}");
|
||||
}
|
||||
|
||||
/// An edge not listed mirrors the drawn one with what a script made on it, not only what the
|
||||
/// template lists: a colour set on the one listed edge shows on all four sides.
|
||||
#[test]
|
||||
fn edges_not_listed_mirror_what_a_script_made_on_the_drawn_one() {
|
||||
let f = node("one", XfaKind::Field, &[], vec![p("border", &[], vec![p("edge", &[], vec![])])]);
|
||||
let mut root = form();
|
||||
root.children.push(f);
|
||||
let script = "this.border.getElement('edge', 0).color.value = '255,0,0'; \
|
||||
[0, 1, 2, 3].map(function (i) { return this.border.getElement('edge', i).color.value; }, this).join('|')";
|
||||
let o = go_on(&root, script, "one");
|
||||
assert_eq!((o.error, o.result.as_deref()), (None, Some("255,0,0|255,0,0|255,0,0|255,0,0")));
|
||||
}
|
||||
|
||||
/// A calculation is a property object (XFA 3.3 pp. 641-642): `override` reads `disabled` where
|
||||
/// the template has no `<calculate>` and `error` where it has one that doesn't say, else as
|
||||
/// written; its script and message read; a write reads back in the run.
|
||||
#[test]
|
||||
fn a_calculation_reads_its_override_script_and_message() {
|
||||
let mut root = form();
|
||||
root.children.push(node(
|
||||
"calc",
|
||||
XfaKind::Field,
|
||||
&[],
|
||||
vec![p(
|
||||
"calculate",
|
||||
&[("override", "disabled")],
|
||||
vec![p("message", &[], vec![t("text", &[], "Computed")]), t("script", &[("contentType", "application/x-javascript")], "a + b")],
|
||||
)],
|
||||
));
|
||||
root.children.push(node("sum", XfaKind::Field, &[], vec![p("calculate", &[], vec![t("script", &[], "1")])]));
|
||||
for (target, expr, want) in [
|
||||
("calc", "this.calculate.override", "disabled"),
|
||||
("calc", "this.calculate.script.value + '|' + this.calculate.message.text.value", "a + b|Computed"),
|
||||
("calc", "this.calculate.className + '|' + this.calculate.isPropertySpecified('override')", "calculate|true"),
|
||||
("calc", "this.calculate.override = 'warning'; this.calculate.override", "warning"),
|
||||
("sum", "this.calculate.override", "error"),
|
||||
("plain", "this.calculate.override + '|' + this.calculate.isPropertySpecified('override')", "disabled|false"),
|
||||
("plain", "this['#calculate'].className + '|' + this.getElement('calculate').override", "calculate|disabled"),
|
||||
] {
|
||||
assert_eq!(eval_on(&root, expr, target), want, "{target}: {expr}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A fill type the fill doesn't have (a pattern on a solid fill) can't be read: the read
|
||||
/// throws an error a script catches, and the fill keeps its type; the type it has reads.
|
||||
#[test]
|
||||
fn a_fill_type_the_fill_does_not_have_throws_and_leaves_the_fill_as_it_was() {
|
||||
let mut root = form();
|
||||
root.children.push(node(
|
||||
"hatched",
|
||||
XfaKind::Field,
|
||||
&[],
|
||||
vec![p("border", &[], vec![p("fill", &[], vec![p("pattern", &[("type", "crossHatch")], vec![])])])],
|
||||
));
|
||||
root.children.push(node(
|
||||
"solid",
|
||||
XfaKind::Field,
|
||||
&[],
|
||||
vec![p("border", &[], vec![p("fill", &[], vec![p("color", &[("value", "1,2,3")], vec![])])])],
|
||||
));
|
||||
for (target, expr) in
|
||||
[("solid", "this.border.fill.pattern.type = 'crossHatch'"), ("plain", "this.border.fill.pattern"), ("styled", "this.border.fill.radial")]
|
||||
{
|
||||
let o = go_on(&root, expr, target);
|
||||
let e = o.error.unwrap_or_default();
|
||||
assert!(e.contains("GeneralError") && e.contains("operation failed"), "{target}: {expr}: {e}");
|
||||
}
|
||||
let caught = "var r; try { this.border.fill.pattern.type = 'crossHatch'; r = 'no error'; } catch (e) { r = e.name; } \
|
||||
r + '|' + this.border.fill.oneOfChild.className + '|' + this.border.fill.color.value";
|
||||
assert_eq!(eval_on(&root, caught, "solid"), "GeneralError|solid|1,2,3");
|
||||
assert_eq!(eval_on(&root, "this.border.fill.pattern.type = 'horizontal'; this.border.fill.pattern.type", "hatched"), "horizontal");
|
||||
assert_eq!(eval_on(&root, "this.border.fill.oneOfChild.className + '|' + this.border.fill.linear.type", "styled"), "linear|toTop");
|
||||
}
|
||||
|
||||
/// A border that lists more than four edges or corners still gives scripts four of each, as
|
||||
/// the layout draws the first four: the fifth and later are out of reach by index, by
|
||||
/// `resolveNodes`, and in `nodes`.
|
||||
#[test]
|
||||
fn a_border_listing_more_than_four_edges_gives_scripts_four() {
|
||||
let six = |tag: &str| (0..6).map(|i| p(tag, &[("thickness", &format!("{i}pt"))], vec![])).collect::<Vec<_>>();
|
||||
let mut kids = six("edge");
|
||||
kids.extend(six("corner"));
|
||||
let mut root = form();
|
||||
root.children.push(node("six", XfaKind::Field, &[], vec![p("border", &[], kids)]));
|
||||
for (expr, want) in [
|
||||
("String(this.border.getElement('edge', 5))", "null"),
|
||||
("String(this.border.getElement('edge', 4))", "null"),
|
||||
("this.border.getElement('edge', 3).thickness", "3pt"),
|
||||
("this.border.resolveNodes('edge[*]').length", "4"),
|
||||
("this.border.resolveNodes('corner[*]').length", "4"),
|
||||
("String(this.border.resolveNode('edge[5]'))", "null"),
|
||||
("String(xfa.resolveNode('xfa.form.form1.six.border.edge[5]'))", "null"),
|
||||
("this.border.nodes.length", "8"),
|
||||
] {
|
||||
assert_eq!(eval_on(&root, expr, "six"), want, "{expr}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Copying the edges a script reaches past those a border lists is charged to its own budget,
|
||||
/// not to the run's text budget: reaching thousands of sides leaves room for the script's
|
||||
/// writes, and the sides past the copy budget still read as the side drawn there.
|
||||
#[test]
|
||||
fn reaching_unlisted_edges_leaves_the_write_budget_to_writes() {
|
||||
let wide = "x".repeat(40 << 10);
|
||||
let rows: Vec<XfaNode> = (0..1_000)
|
||||
.map(|i| XfaNode {
|
||||
som: format!("form1[0].row[{i}]"),
|
||||
index: i,
|
||||
repeatable: true,
|
||||
..node("row", XfaKind::Field, &[], vec![p("border", &[], vec![p("edge", &[("thickness", "2pt"), ("note", wide.as_str())], vec![])])])
|
||||
})
|
||||
.collect();
|
||||
let mut root = form();
|
||||
root.children.extend(rows);
|
||||
let script = "var l = xfa.resolveNodes('xfa.form.form1.row[*].border.edge[3]'); \
|
||||
var t = l.item(999).thickness; \
|
||||
xfa.resolveNode('xfa.form.form1.styled').rawValue = 'kept'; l.length + '|' + t";
|
||||
let o = go_on(&root, script, "styled");
|
||||
assert_eq!((o.error.as_deref(), o.result.as_deref()), (None, Some("1000|2pt")), "{:?}", o.notes);
|
||||
assert!(o.effects.iter().any(|e| matches!(e, crate::xfa::XfaEffect::SetValue { value, .. } if value == "kept")), "{:?}", o.notes);
|
||||
assert!(o.notes.iter().all(|n| !n.contains("MiB")), "{:?}", o.notes);
|
||||
}
|
||||
|
||||
/// `name in fill` is false for a fill type the fill doesn't have, whose read throws, and true
|
||||
/// for what a read answers.
|
||||
#[test]
|
||||
fn in_says_which_fill_types_can_be_read() {
|
||||
let mut root = form();
|
||||
root.children.push(node(
|
||||
"solid",
|
||||
XfaKind::Field,
|
||||
&[],
|
||||
vec![p("border", &[], vec![p("fill", &[], vec![p("color", &[("value", "1,2,3")], vec![])])])],
|
||||
));
|
||||
let expr = "['pattern', 'linear', '#radial', 'color', 'solid', 'presence', 'oneOfChild'].map(function (k) { return k in this.border.fill; }, this).join('|')";
|
||||
assert_eq!(eval_on(&root, expr, "solid"), "false|false|false|true|true|true|true");
|
||||
assert_eq!(eval_on(&root, "('linear' in this.border.fill) + '|' + ('pattern' in this.border.fill)", "styled"), "true|false");
|
||||
}
|
||||
|
||||
/// `value.#text` on a draw stays unresolved: the property objects don't carry a draw's content,
|
||||
/// so it reads as null rather than as an empty text until a node's value is its value element.
|
||||
/// On a field its value element's content type is reached, as before.
|
||||
#[test]
|
||||
fn a_draws_value_text_stays_unresolved() {
|
||||
let mut root = form();
|
||||
root.children.push(node("words", XfaKind::Draw, &[], vec![p("value", &[], vec![p("text", &[], vec![])])]));
|
||||
assert_eq!(eval_on(&root, "String(this.resolveNode('value.#text'))", "words"), "null");
|
||||
assert_eq!(eval_on(&root, "this.resolveNode('value.#decimal').className", "styled"), "decimal");
|
||||
}
|
||||
|
||||
/// A node with a child named like a member read from its properties reads that child, and a
|
||||
/// write to the member doesn't reach the property either.
|
||||
#[test]
|
||||
fn a_member_shadowed_by_a_child_is_not_written_to_the_property() {
|
||||
let mut shadow =
|
||||
node("shadow", XfaKind::Subform, &[], vec![p("border", &[], vec![p("fill", &[], vec![p("color", &[("value", "4,4,4")], vec![])])])]);
|
||||
shadow.children = ["fillColor", "mandatory"]
|
||||
.iter()
|
||||
.map(|n| XfaNode { som: format!("form1[0].shadow[0].{n}[0]"), ..node(n, XfaKind::Field, &[], vec![]) })
|
||||
.collect();
|
||||
let mut root = form();
|
||||
root.children.push(shadow);
|
||||
let script = "this.fillColor = '9,9,9'; this.mandatory = 'error'; \
|
||||
[this.fillColor.className, this.border.fill.color.value, this.validate.nullTest].join('|')";
|
||||
assert_eq!(eval_on(&root, script, "shadow"), "field|4,4,4|disabled");
|
||||
}
|
||||
|
||||
/// A SOM expression into the properties of many instances gives at most
|
||||
/// [`super::MAX_RESOLVED`] of them.
|
||||
#[test]
|
||||
fn resolving_into_many_instances_stops_at_the_limit() {
|
||||
let rows: Vec<XfaNode> = (0..5_000)
|
||||
.map(|i| XfaNode {
|
||||
som: format!("form1[0].row[{i}]"),
|
||||
index: i,
|
||||
repeatable: true,
|
||||
..node("row", XfaKind::Subform, &[], vec![p("border", &[], vec![])])
|
||||
})
|
||||
.collect();
|
||||
let mut root = form();
|
||||
root.children.extend(rows);
|
||||
assert_eq!(eval_on(&root, "xfa.resolveNodes('xfa.form.form1.row[*].border').length", "styled"), super::MAX_RESOLVED.to_string());
|
||||
}
|
||||
|
||||
/// An `assist`'s `speak` reads its text and attributes.
|
||||
#[test]
|
||||
fn assist_speak_reads_its_text_and_attributes() {
|
||||
let mut root = form();
|
||||
root.children.push(node(
|
||||
"spoken",
|
||||
XfaKind::Field,
|
||||
&[],
|
||||
vec![p("assist", &[], vec![t("speak", &[("priority", "toolTip"), ("disable", "1")], "Say this")])],
|
||||
));
|
||||
let expr = "[this.assist.speak.value, this.assist.speak.priority, this.assist.speak.disable, this.assist.speak.className].join('|')";
|
||||
assert_eq!(eval_on(&root, expr, "spoken"), "Say this|toolTip|1|speak");
|
||||
assert_eq!(eval_on(&root, "this.assist.speak.value + '|' + this.assist.isPropertySpecified('speak')", "plain"), "|false");
|
||||
}
|
||||
+11
-2
@@ -109,8 +109,17 @@ let form = pdfcraft_xfa::layout_xml(template_xml)?; // pages of items, for
|
||||
The object model covers what forms commonly use (`xfa.form`
|
||||
navigation and SOM resolution, `rawValue`, `presence`, `access`, instance managers,
|
||||
`xfa.host` messages, reset, print, focus and URLs, `xfa.layout` page numbers, `xfa.event`);
|
||||
not `xfa.template`, data descriptions, `xfa.connectionSet`, `border`/`font`/`ui` properties
|
||||
(reads give a sink that accepts writes), `execEvent`, or the change event per keystroke.
|
||||
property objects (`border`, `font`, `para`, `margin`, `ui` and its `oneOfChild`, `validate`,
|
||||
`calculate`, `occur`, `break`, `breakBefore`, `breakAfter`, `assist`, `format`, `bind`,
|
||||
`desc`, `extras`, `keep`, `caption` and a field's `items`), read as the template writes them
|
||||
(the parser keeps each object's property elements, shared by every instance and every
|
||||
script's snapshot) with the spec's defaults for what forms read, `getElement`,
|
||||
`isPropertySpecified`, `getAttribute`, and a node's `mandatory`, validation messages and
|
||||
colour shorthands; writes to them are read back in the same script but change nothing yet.
|
||||
Not `xfa.template`, data descriptions, `xfa.connectionSet`, the choice-list methods
|
||||
(`addItem`, `clearItems`, `boundItem`; `selectedIndex` reads give a sink that accepts
|
||||
writes), a node's `value` as its value element, `traversal` and `overflow`, property reads
|
||||
in FormCalc, `execEvent`, or the change event per keystroke.
|
||||
Validate failures show their message and keep the value, as Acrobat does for scripts.
|
||||
A hidden instance of a repeating subform hides every instance (overrides apply to the template).
|
||||
A repeating subform shows what its data holds once the data holds any instance, else its
|
||||
|
||||
@@ -347,6 +347,31 @@ pub struct Common {
|
||||
pub para: ParaSpec,
|
||||
/// Table cells: how many columns this cell spans.
|
||||
pub col_span: usize,
|
||||
/// The object's own attributes and its property elements as the template writes them, for
|
||||
/// scripts to read (see [`Props`]). Shared: the live form's instances of the object and
|
||||
/// copies of the template point at the same tree.
|
||||
pub props: std::sync::Arc<Props>,
|
||||
}
|
||||
|
||||
/// What scripts read of an object's properties (XFA 3.3 ch. 3, "Properties vs. Children",
|
||||
/// p. 78): the object's own attributes and the property elements the template gives it (border,
|
||||
/// font, para, margin, ui, validate, occur, break, assist, format, value, bind, desc, extras, keep),
|
||||
/// as written. Properties the template leaves out are not here: the scripting host supplies
|
||||
/// their defaults.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Props {
|
||||
pub attrs: Vec<(String, String)>,
|
||||
pub elements: Vec<Prop>,
|
||||
}
|
||||
|
||||
/// One property element: its tag, attributes, text (for elements that hold text, such as
|
||||
/// `picture` or `toolTip`) and its own property elements.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Prop {
|
||||
pub tag: String,
|
||||
pub attrs: Vec<(String, String)>,
|
||||
pub text: Option<std::sync::Arc<str>>,
|
||||
pub children: Vec<Prop>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -737,6 +762,41 @@ pub struct Template {
|
||||
/// Fields that compute the page number or the page count on layout, by `id`, so text that
|
||||
/// embeds them can show the numbers.
|
||||
pub page_roles: Vec<(String, PageRole)>,
|
||||
/// What a user of the template keeps for as long as it lives (see [`TemplateCache`]).
|
||||
pub cache: TemplateCache,
|
||||
}
|
||||
|
||||
/// Room for one value a user of a template keeps alongside it for as long as the template
|
||||
/// lives, and drops with it (the engine keeps the script engine's copies of the property trees
|
||||
/// here, so they go when the form is closed). Not part of the template: a clone starts empty,
|
||||
/// and two caches always compare equal.
|
||||
#[derive(Default)]
|
||||
pub struct TemplateCache(std::sync::OnceLock<Box<dyn std::any::Any + Send + Sync>>);
|
||||
|
||||
impl TemplateCache {
|
||||
/// The value kept here, made by `make` the first time; `None` when a value of another type
|
||||
/// was kept first.
|
||||
pub fn get_or_init<T: std::any::Any + Send + Sync>(&self, make: impl FnOnce() -> T) -> Option<&T> {
|
||||
self.0.get_or_init(|| Box::new(make())).downcast_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for TemplateCache {
|
||||
fn clone(&self) -> Self {
|
||||
TemplateCache::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for TemplateCache {
|
||||
fn eq(&self, _: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TemplateCache {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("TemplateCache")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
|
||||
+255
-9
@@ -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, Prop, Props, Rect, RichText, Run, Script, Stroke, Subform,
|
||||
Template, Ui, VAlign, Value,
|
||||
};
|
||||
|
||||
/// Deepest element nesting followed (templates are shallow; hostile ones are not).
|
||||
@@ -198,8 +198,9 @@ fn parse_para(n: Option<XmlNode>) -> ParaSpec {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_common(n: XmlNode) -> Common {
|
||||
fn parse_common(n: XmlNode, warnings: &mut Vec<String>) -> Common {
|
||||
Common {
|
||||
props: std::sync::Arc::new(parse_props(n, warnings)),
|
||||
name: n.attribute("name").map(str::to_string).filter(|s| !s.is_empty()),
|
||||
id: n.attribute("id").map(str::to_string),
|
||||
x: attr_measure(n, "x"),
|
||||
@@ -220,6 +221,247 @@ fn parse_common(n: XmlNode) -> Common {
|
||||
}
|
||||
}
|
||||
|
||||
/// The property elements of an object that scripts read through the object model (XFA 3.3
|
||||
/// ch. 17: border p. 629, font p. 741, para p. 800, margin p. 776, ui p. 877, validate p. 880,
|
||||
/// occur p. 786, break p. 632, breakBefore p. 637, breakAfter p. 635, assist p. 609, format
|
||||
/// p. 745, value p. 882, bind p. 623, desc p. 674, extras p. 724, keep p. 762, caption p. 643,
|
||||
/// items p. 758, calculate p. 641).
|
||||
const PROP_TAGS: &[&str] = &[
|
||||
"border",
|
||||
"font",
|
||||
"para",
|
||||
"margin",
|
||||
"ui",
|
||||
"validate",
|
||||
"occur",
|
||||
"break",
|
||||
"breakBefore",
|
||||
"breakAfter",
|
||||
"assist",
|
||||
"format",
|
||||
"value",
|
||||
"bind",
|
||||
"desc",
|
||||
"extras",
|
||||
"keep",
|
||||
"caption",
|
||||
"items",
|
||||
"calculate",
|
||||
];
|
||||
/// Most property elements kept for one object, all levels together. A field written by a form
|
||||
/// designer holds about 40 (four edges and corners with their colours, a fill, a font, a widget
|
||||
/// with its own border); past this the rest are left out and a warning says so.
|
||||
pub const MAX_OBJECT_PROPS: usize = 256;
|
||||
/// Most bytes of tags, attribute names, values and text kept for one object's properties: a
|
||||
/// designer's field holds well under 2 KiB.
|
||||
pub const MAX_OBJECT_PROP_BYTES: usize = 16 << 10;
|
||||
/// Deepest property element kept below an object (`ui > textEdit > border > edge > color` is 5).
|
||||
const MAX_PROP_DEPTH: usize = 8;
|
||||
/// Most attributes kept on one element.
|
||||
const MAX_PROP_ATTRS: usize = 64;
|
||||
/// Longest validation or calculation script kept for scripts to read, in bytes: the script
|
||||
/// engine's own limit on a script (`pdfcraft_js`'s `MAX_SCRIPT_BYTES`), past which it would not run anyway.
|
||||
const MAX_VALIDATE_SCRIPT_BYTES: usize = 256 * 1024;
|
||||
/// The warning when an object's properties passed a limit.
|
||||
pub const PROPS_CUT_WARNING: &str = "an object has more property elements, attributes or text than scripts are given; the rest are left out";
|
||||
|
||||
/// What is left of one object's property budget.
|
||||
struct PropBudget {
|
||||
elements: usize,
|
||||
bytes: usize,
|
||||
cut: bool,
|
||||
}
|
||||
|
||||
impl PropBudget {
|
||||
/// `s` when the bytes are left for it (charged), else `None` and the cut noted.
|
||||
fn text(&mut self, s: &str) -> Option<String> {
|
||||
match self.bytes.checked_sub(s.len()) {
|
||||
Some(left) => {
|
||||
self.bytes = left;
|
||||
Some(s.to_string())
|
||||
}
|
||||
None => {
|
||||
self.cut = true;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The attributes of `n`, to [`MAX_PROP_ATTRS`] and while the bytes last.
|
||||
fn attrs(&mut self, n: XmlNode) -> Vec<(String, String)> {
|
||||
let mut out = Vec::new();
|
||||
for a in n.attributes() {
|
||||
if out.len() >= MAX_PROP_ATTRS {
|
||||
self.cut = true;
|
||||
break;
|
||||
}
|
||||
let Some(name) = self.text(a.name()) else { break };
|
||||
let Some(value) = self.text(a.value()) else { break };
|
||||
out.push((name, value));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Most `<items>` of one object kept as lists ([`items_prop`]): a field has two at most, the
|
||||
/// shown list and the saved values (XFA 3.3 p. 726, `<items/> [0..2]`), the two the field's own
|
||||
/// lists are read from. Forms do carry more (one public form has a third, empty, saved list);
|
||||
/// any further one is kept as an ordinary property, every entry charged to the object's budget.
|
||||
const MAX_ITEM_LISTS: usize = 2;
|
||||
|
||||
/// An `<items>` element (XFA 3.3 p. 758) as scripts read it: its attributes and its entries,
|
||||
/// each with its tag and text, as the choice list or check button shows them ([`parse_items`]:
|
||||
/// the same [`MAX_ITEMS`] and the same text), so a script reads what the list holds. The
|
||||
/// element and its attributes are charged to the object's budget like any property; its
|
||||
/// entries are not (a list of a few hundred countries would otherwise crowd out every other
|
||||
/// property), so they are bounded by the list's own limits instead: [`MAX_ITEMS`] entries of
|
||||
/// at most [`MAX_TEXT`] each, in at most [`MAX_ITEM_LISTS`] lists, no more than the field's
|
||||
/// own lists hold. An entry's attributes are not kept: scripts read an entry's value.
|
||||
fn items_prop(n: XmlNode, b: &mut PropBudget) -> Option<Prop> {
|
||||
if b.elements == 0 {
|
||||
b.cut = true;
|
||||
return None;
|
||||
}
|
||||
b.elements -= 1;
|
||||
let tag = b.text(n.tag_name().name())?;
|
||||
let attrs = b.attrs(n);
|
||||
let children = n
|
||||
.children()
|
||||
.filter(|c| c.is_element())
|
||||
.take(MAX_ITEMS)
|
||||
.map(|c| Prop { tag: c.tag_name().name().to_string(), attrs: Vec::new(), text: Some(std::sync::Arc::from(text_of(c))), children: Vec::new() })
|
||||
.collect();
|
||||
Some(Prop { tag, attrs, text: None, children })
|
||||
}
|
||||
|
||||
/// The property elements of `n` scripts read: its own attributes and the [`PROP_TAGS`]
|
||||
/// elements with what they hold, within [`MAX_OBJECT_PROPS`] elements,
|
||||
/// [`MAX_OBJECT_PROP_BYTES`] of text, [`MAX_PROP_DEPTH`] levels and [`MAX_PROP_ATTRS`]
|
||||
/// attributes an element; past them the rest is left out and [`PROPS_CUT_WARNING`] is added
|
||||
/// once. Only elements in the object's namespace are properties (XFA 3.3 p. 598); a `script` is
|
||||
/// read only in a `validate` or a `calculate`, and only its first (forms read its text, which
|
||||
/// is not charged to the budget but kept within [`MAX_VALIDATE_SCRIPT_BYTES`]), the first
|
||||
/// [`MAX_ITEM_LISTS`] `items` as lists (see [`items_prop`]), `extras` only as the object's own, the XHTML
|
||||
/// of `exData` not at all, and of `value` only its content element's tag and attributes (the
|
||||
/// data is the object's value, read elsewhere).
|
||||
fn parse_props(n: XmlNode, warnings: &mut Vec<String>) -> Props {
|
||||
/// The property elements under `n`, whose tag is `within` (empty for the object itself):
|
||||
/// `extras` in the object and inside one (forms keep named values there), `script` in a
|
||||
/// `validate` or a `calculate` (XFA 3.3 p. 880, p. 641, p. 827).
|
||||
fn elements<'a, 'd>(n: XmlNode<'a, 'd>, within: &str) -> impl Iterator<Item = XmlNode<'a, 'd>> + use<'a, 'd> {
|
||||
let (extras, script) = (within.is_empty() || within == "extras", matches!(within, "validate" | "calculate"));
|
||||
let ns = n.tag_name().namespace();
|
||||
n.children().filter(move |c| {
|
||||
c.is_element()
|
||||
&& c.tag_name().namespace() == ns
|
||||
&& match c.tag_name().name() {
|
||||
"script" => script,
|
||||
"extras" => extras,
|
||||
_ => true,
|
||||
}
|
||||
})
|
||||
}
|
||||
/// `n` as a property, `depth` below the object; `bare`: its tag and attributes only.
|
||||
fn prop(n: XmlNode, depth: usize, bare: bool, b: &mut PropBudget) -> Option<Prop> {
|
||||
if depth > MAX_PROP_DEPTH || b.elements == 0 {
|
||||
b.cut = true;
|
||||
return None;
|
||||
}
|
||||
b.elements -= 1;
|
||||
let tag = b.text(n.tag_name().name())?;
|
||||
let mut p = Prop { attrs: b.attrs(n), tag, text: None, children: Vec::new() };
|
||||
if bare {
|
||||
return Some(p);
|
||||
}
|
||||
if p.tag == "exData" {
|
||||
// Rich text: its words, as scripts read a caption's rich text (`exData.value`), not
|
||||
// its XHTML elements.
|
||||
let t = text_of(n);
|
||||
let t = t.trim();
|
||||
if !t.is_empty() {
|
||||
p.text = b.text(t).map(std::sync::Arc::from);
|
||||
}
|
||||
return Some(p);
|
||||
}
|
||||
if p.tag == "script" {
|
||||
// A validation's or calculation's script: its text is not charged to the budget, so a long script
|
||||
// leaves the other properties their room, but it is kept only within the script
|
||||
// engine's own limit on a script, past which it would not run anyway.
|
||||
let t = text_of(n);
|
||||
let t = t.trim();
|
||||
if t.len() > MAX_VALIDATE_SCRIPT_BYTES {
|
||||
b.cut = true;
|
||||
} else if !t.is_empty() {
|
||||
p.text = Some(std::sync::Arc::from(t));
|
||||
}
|
||||
return Some(p);
|
||||
}
|
||||
let mut kids = elements(n, p.tag.as_str()).peekable();
|
||||
if kids.peek().is_none() {
|
||||
// The element's own text only: not that of children left out (an `extras`, a
|
||||
// `script`), and no more than the budget holds.
|
||||
let mut own = String::new();
|
||||
for t in n.children().filter(|c| c.is_text()).filter_map(|c| c.text()) {
|
||||
if own.len().saturating_add(t.len()) > b.bytes {
|
||||
b.cut = true;
|
||||
own.clear();
|
||||
break;
|
||||
}
|
||||
own.push_str(t);
|
||||
}
|
||||
let t = own.trim();
|
||||
if !t.is_empty() {
|
||||
p.text = b.text(t).map(std::sync::Arc::from);
|
||||
}
|
||||
return Some(p);
|
||||
}
|
||||
// The object's own value holds its data, read elsewhere: of it only the content
|
||||
// element's tag. A caption's value is its label, and is read whole.
|
||||
let content_only = p.tag == "value" && depth == 1;
|
||||
// A validation or a calculation has one script (XFA 3.3 p. 880, p. 641: [0..1]); any
|
||||
// more are not read.
|
||||
let mut scripts = 0usize;
|
||||
for k in kids {
|
||||
if k.tag_name().name() == "script" {
|
||||
scripts = scripts.saturating_add(1);
|
||||
if scripts > 1 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
match prop(k, depth + 1, content_only, b) {
|
||||
Some(c) => p.children.push(c),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
Some(p)
|
||||
}
|
||||
let mut b = PropBudget { elements: MAX_OBJECT_PROPS, bytes: MAX_OBJECT_PROP_BYTES, cut: false };
|
||||
let attrs = b.attrs(n);
|
||||
let mut out = Vec::new();
|
||||
// `desc` and `extras` last: they can hold the most (forms keep data there), so a cut falls
|
||||
// on them before the appearance and validation properties (their order means nothing,
|
||||
// XFA 3.3 p. 78).
|
||||
let bulky = |c: &XmlNode| matches!(c.tag_name().name(), "desc" | "extras");
|
||||
let first = elements(n, "").filter(|c| PROP_TAGS.contains(&c.tag_name().name()) && !bulky(c));
|
||||
let mut lists = 0usize;
|
||||
for c in first.chain(elements(n, "").filter(bulky)) {
|
||||
let p = if c.tag_name().name() == "items" && lists < MAX_ITEM_LISTS {
|
||||
lists += 1;
|
||||
items_prop(c, &mut b)
|
||||
} else {
|
||||
prop(c, 1, false, &mut b)
|
||||
};
|
||||
match p {
|
||||
Some(p) => out.push(p),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
if b.cut && !warnings.iter().any(|w| w == PROPS_CUT_WARNING) {
|
||||
warnings.push(PROPS_CUT_WARNING.into());
|
||||
}
|
||||
Props { attrs, elements: out }
|
||||
}
|
||||
|
||||
/// Rich text: XHTML `body > p > span…`. `br` starts a new paragraph; `xfa:embed` spans become
|
||||
/// embedded fields; the text of an `<a href>` keeps where it links to.
|
||||
fn parse_rich(body: XmlNode) -> RichText {
|
||||
@@ -520,7 +762,7 @@ fn parse_field(n: XmlNode, warnings: &mut Vec<String>) -> Field {
|
||||
.map(|t| t.trim().to_string())
|
||||
.filter(|t| !t.is_empty());
|
||||
Field {
|
||||
common: parse_common(n),
|
||||
common: parse_common(n, warnings),
|
||||
ui: kind,
|
||||
ui_border: ui_el.and_then(|e| parse_border(child(e, "border"))),
|
||||
ui_margin: parse_margin(ui_el.and_then(|e| child(e, "margin"))),
|
||||
@@ -619,7 +861,7 @@ fn parse_page_set(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> PageS
|
||||
}
|
||||
|
||||
fn parse_subform(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Subform {
|
||||
let common = parse_common(n);
|
||||
let common = parse_common(n, warnings);
|
||||
let mut children_out = Vec::new();
|
||||
let mut page_set = None;
|
||||
for c in n.children().filter(|c| c.is_element()) {
|
||||
@@ -663,13 +905,13 @@ fn parse_node(c: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Option<No
|
||||
Node::Subform(Box::new(sf))
|
||||
}
|
||||
"area" => Node::Area(Box::new(Area {
|
||||
common: parse_common(c),
|
||||
common: parse_common(c, warnings),
|
||||
children: c.children().filter(|k| k.is_element()).filter_map(|k| parse_node(k, warnings, depth + 1)).collect(),
|
||||
})),
|
||||
"field" => Node::Field(Box::new(parse_field(c, warnings))),
|
||||
"draw" => Node::Draw(Box::new(Draw { common: parse_common(c), value: parse_value(child(c, "value"), warnings) })),
|
||||
"draw" => Node::Draw(Box::new(Draw { common: parse_common(c, warnings), value: parse_value(child(c, "value"), warnings) })),
|
||||
"exclGroup" => Node::ExclGroup(Box::new(ExclGroup {
|
||||
common: parse_common(c),
|
||||
common: parse_common(c, warnings),
|
||||
layout: Layout::parse(c.attribute("layout")),
|
||||
fields: children(c, "field").map(|f| parse_field(f, warnings)).collect(),
|
||||
tooltip: child(c, "assist").and_then(|a| child(a, "toolTip")).map(text_of).map(|t| t.trim().to_string()).filter(|t| !t.is_empty()),
|
||||
@@ -759,7 +1001,7 @@ pub fn parse(xml: &str) -> Result<(Template, Vec<String>), XfaError> {
|
||||
}
|
||||
let mut page_roles = Vec::new();
|
||||
collect_page_roles(&[Node::Subform(Box::new(root_sf.clone()))], &mut page_roles, 0);
|
||||
Ok((Template { root: root_sf, page_roles }, warnings))
|
||||
Ok((Template { root: root_sf, page_roles, cache: Default::default() }, warnings))
|
||||
}
|
||||
|
||||
fn parse_access(a: Option<&str>) -> Option<Access> {
|
||||
@@ -835,3 +1077,7 @@ fn apply_saved_state(nodes: &mut [Node], saved: XmlNode, depth: usize) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "props_tests.rs"]
|
||||
mod props_tests;
|
||||
@@ -0,0 +1,223 @@
|
||||
//! The property elements the parser keeps for scripts (XFA 3.3 ch. 3, "Properties vs.
|
||||
//! Children"), their limits, and the live form sharing them rather than copying.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::model::{Node, Prop};
|
||||
use crate::parse::{MAX_OBJECT_PROP_BYTES, MAX_OBJECT_PROPS, PROPS_CUT_WARNING};
|
||||
use crate::{Overrides, form_tree, parse};
|
||||
|
||||
fn template(body: &str) -> String {
|
||||
format!(
|
||||
r#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="form" layout="tb"><pageSet><pageArea name="p"><contentArea w="8in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>{body}</subform></template>"#
|
||||
)
|
||||
}
|
||||
|
||||
fn first_field(xml: &str) -> (crate::model::Field, Vec<String>) {
|
||||
let (t, warnings) = parse(xml).expect("parses");
|
||||
let f = t.root.children.iter().find_map(|n| if let Node::Field(f) = n { Some((**f).clone()) } else { None }).expect("a field");
|
||||
(f, warnings)
|
||||
}
|
||||
|
||||
fn count(p: &Prop) -> usize {
|
||||
1 + p.children.iter().map(count).sum::<usize>()
|
||||
}
|
||||
|
||||
fn depth(p: &Prop) -> usize {
|
||||
1 + p.children.iter().map(depth).max().unwrap_or(0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fields_property_elements_are_kept_for_scripts() {
|
||||
let xml = template(
|
||||
r#"<field name="f" w="2in" access="readOnly"><ui><choiceList open="always"><border><edge><color value="9,9,9"/></edge></border></choiceList><extras><x/></extras></ui>
|
||||
<border presence="hidden"><edge thickness="2pt"><color value="255,0,0"/></edge><fill><color value="1,2,3"/><linear type="toTop"/></fill></border>
|
||||
<caption><value><text>Label</text></value></caption>
|
||||
<validate nullTest="error"><message><text name="nullTest">Required</text></message><script>true</script></validate>
|
||||
<assist><toolTip>Tip</toolTip></assist><format><picture>num{z9}</picture></format>
|
||||
<value><exData contentType="text/html"><body xmlns="http://www.w3.org/1999/xhtml"><p>Hi</p></body></exData></value>
|
||||
<keep intact="contentArea"/><event activity="click"><script>1</script></event></field>"#,
|
||||
);
|
||||
let (f, warnings) = first_field(&xml);
|
||||
assert!(warnings.iter().all(|w| w != PROPS_CUT_WARNING), "{warnings:?}");
|
||||
let mut props = (*f.common.props).clone();
|
||||
assert!(props.attrs.contains(&("w".into(), "2in".into())) && props.attrs.contains(&("access".into(), "readOnly".into())), "{props:?}");
|
||||
let tags: Vec<&str> = props.elements.iter().map(|p| p.tag.as_str()).collect();
|
||||
// Not events. The caption is read whole (its label's text), the object's own value only
|
||||
// as its content element's tag.
|
||||
assert_eq!(tags, ["ui", "border", "caption", "validate", "assist", "format", "value", "keep"]);
|
||||
let caption = props.elements.remove(2);
|
||||
assert_eq!(caption.children[0].children[0].text.as_deref(), Some("Label"));
|
||||
let ui = &props.elements[0];
|
||||
assert_eq!(ui.children.len(), 1, "extras left out: {ui:?}");
|
||||
assert_eq!(ui.children[0].tag, "choiceList");
|
||||
assert_eq!(ui.children[0].children[0].children[0].children[0].attrs, [("value".to_string(), "9,9,9".to_string())]);
|
||||
let validate = &props.elements[2];
|
||||
assert_eq!(validate.children[0].children[0].text.as_deref(), Some("Required"));
|
||||
// A validation's script is kept (forms check whether there is one); event scripts are not.
|
||||
assert_eq!((validate.children[1].tag.as_str(), validate.children[1].text.as_deref()), ("script", Some("true")));
|
||||
assert!(props.elements.iter().all(|p| p.tag != "event"));
|
||||
assert_eq!(props.elements[3].children[0].text.as_deref(), Some("Tip"));
|
||||
assert_eq!(props.elements[4].children[0].text.as_deref(), Some("num{z9}"));
|
||||
// The value's content element, not its data.
|
||||
let value = &props.elements[5];
|
||||
assert_eq!((value.children.len(), value.children[0].tag.as_str(), value.children[0].children.len()), (1, "exData", 0));
|
||||
assert_eq!(value.children[0].text, None);
|
||||
}
|
||||
|
||||
/// A hostile template: thousands of edges, deep nesting, a huge attribute and many attributes.
|
||||
/// What scripts are given stays within the per-object limits, and a warning says so once.
|
||||
#[test]
|
||||
fn hostile_property_trees_are_cut_to_the_limits() {
|
||||
let edges = "<edge><color value=\"1,1,1\"/></edge>".repeat(5_000);
|
||||
let deep = format!("{}{}", "<fill>".repeat(200), "</fill>".repeat(200));
|
||||
let big = "x".repeat(200_000);
|
||||
let attrs: String = (0..500).map(|i| format!(" a{i}=\"{i}\"")).collect();
|
||||
// Many lists of many entries: only two lists are kept (a field has two at most), each to the
|
||||
// list's own entry limit, and the lists count against the budget like any property.
|
||||
let entries = "<text>e</text>".repeat(1_200);
|
||||
let lists = format!("<items>{entries}</items>").repeat(40);
|
||||
for body in [
|
||||
format!("<field name=\"f\"><border>{edges}</border></field>"),
|
||||
format!("<field name=\"f\"><border>{deep}</border></field>"),
|
||||
format!("<field name=\"f\"><font typeface=\"{big}\"/></field>"),
|
||||
format!("<field name=\"f\"><font{attrs}/></field>"),
|
||||
format!("<field name=\"f\" {}><font/></field>", attrs.trim()),
|
||||
format!("<field name=\"f\">{lists}</field>"),
|
||||
] {
|
||||
let (f, warnings) = first_field(&template(&format!("{body}{body}")));
|
||||
assert_eq!(warnings.iter().filter(|w| *w == PROPS_CUT_WARNING).count(), 1, "once: {warnings:?}");
|
||||
let props = &f.common.props;
|
||||
// The first two lists' entries are the lists' own (up to 1,000 each), not property
|
||||
// elements; any further list, entries and all, counts against the budget.
|
||||
let mut lists = 0;
|
||||
let elements: usize = props
|
||||
.elements
|
||||
.iter()
|
||||
.map(|p| match p.tag.as_str() {
|
||||
"items" if lists < 2 => {
|
||||
lists += 1;
|
||||
assert!(p.children.len() <= 1_000, "entries past the list's limit");
|
||||
1
|
||||
}
|
||||
_ => count(p),
|
||||
})
|
||||
.sum();
|
||||
assert!(elements <= MAX_OBJECT_PROPS, "{elements}");
|
||||
assert!(props.elements.iter().map(depth).max().unwrap_or(0) <= 9, "deep");
|
||||
let bytes = |p: &Prop| -> usize { p.attrs.iter().map(|(k, v)| k.len() + v.len()).sum::<usize>() + p.tag.len() };
|
||||
fn all<'a>(p: &'a Prop, out: &mut Vec<&'a Prop>) {
|
||||
out.push(p);
|
||||
p.children.iter().for_each(|c| all(c, out));
|
||||
}
|
||||
let mut flat = Vec::new();
|
||||
props.elements.iter().for_each(|p| all(p, &mut flat));
|
||||
let total: usize = flat.iter().map(|p| bytes(p)).sum::<usize>() + props.attrs.iter().map(|(k, v)| k.len() + v.len()).sum::<usize>();
|
||||
assert!(total <= MAX_OBJECT_PROP_BYTES, "{total}");
|
||||
assert!(flat.iter().all(|p| p.attrs.len() <= 64) && props.attrs.len() <= 64);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every instance of a repeating subform, and every live form built, points at the template's
|
||||
/// property tree: nothing is copied per instance or per build.
|
||||
#[test]
|
||||
fn the_live_form_shares_the_templates_property_trees() {
|
||||
let xml = template(
|
||||
r#"<subform name="row" layout="tb"><occur min="3" max="-1" initial="3"/><field name="f"><border><edge/></border></field></subform>"#,
|
||||
);
|
||||
let (t, _) = parse(&xml).expect("parses");
|
||||
let a = form_tree(&t, None, &Overrides::default());
|
||||
let b = form_tree(&t, None, &Overrides::default());
|
||||
let rows: Vec<_> = a.root.children.iter().filter(|c| c.name == "row").collect();
|
||||
assert_eq!(rows.len(), 3);
|
||||
let Some(Node::Subform(row)) = t.root.children.first() else { panic!("a row subform") };
|
||||
for r in &rows {
|
||||
assert!(Arc::ptr_eq(&r.props, &row.common.props));
|
||||
assert!(Arc::ptr_eq(&r.children[0].props, &rows[0].children[0].props));
|
||||
}
|
||||
assert_eq!(rows[0].props.elements[0].tag, "occur");
|
||||
assert!(Arc::ptr_eq(&a.root.children[0].props, &b.root.children[0].props));
|
||||
}
|
||||
|
||||
/// A property element whose children are all left out (an `extras`, a `script`) has only its
|
||||
/// own text, not theirs, and nothing is reported cut.
|
||||
#[test]
|
||||
fn a_property_keeps_its_own_text_not_that_of_children_left_out() {
|
||||
let big = "x".repeat(20_000);
|
||||
let xml = template(&format!(
|
||||
r#"<field name="f"><font typeface="Arial"><extras><text>{big}</text></extras></font><assist><toolTip>Tip<extras><text>hidden</text></extras></toolTip></assist><breakBefore targetType="pageArea"><script>xfa.host.messageBox("{big}")</script></breakBefore><validate><script>{big}</script></validate><border><edge thickness="2pt"/></border></field>"#
|
||||
));
|
||||
let (f, warnings) = first_field(&xml);
|
||||
assert!(warnings.iter().all(|w| w != PROPS_CUT_WARNING), "{warnings:?}");
|
||||
let tags: Vec<(&str, Option<&str>)> = f.common.props.elements.iter().map(|p| (p.tag.as_str(), p.text.as_deref())).collect();
|
||||
assert_eq!(tags, [("font", None), ("assist", None), ("breakBefore", None), ("validate", None), ("border", None)]);
|
||||
assert_eq!(f.common.props.elements[1].children[0].text.as_deref(), Some("Tip"));
|
||||
// A validation's script keeps its text, outside the budget: the border after it is kept.
|
||||
assert_eq!(f.common.props.elements[3].children[0].text.as_ref().map(|t| t.len()), Some(20_000));
|
||||
assert_eq!(f.common.props.elements[4].children[0].attrs, [("thickness".to_string(), "2pt".to_string())]);
|
||||
}
|
||||
|
||||
/// A validation keeps its first script only (it has one, XFA 3.3 p. 880), and that script's
|
||||
/// text only within the script engine's limit on a script.
|
||||
#[test]
|
||||
fn a_validation_keeps_its_first_script_within_the_script_limit() {
|
||||
let xml = template(r#"<field name="f"><validate><script>first</script><script>second</script></validate></field>"#);
|
||||
let (f, _) = first_field(&xml);
|
||||
let scripts: Vec<Option<&str>> = f.common.props.elements[0].children.iter().map(|s| s.text.as_deref()).collect();
|
||||
assert_eq!(scripts, [Some("first")]);
|
||||
let huge = "x".repeat(300 * 1024);
|
||||
let (f, warnings) =
|
||||
first_field(&template(&format!(r#"<field name="f"><validate><script>{huge}</script></validate><font typeface="Arial"/></field>"#)));
|
||||
assert_eq!(f.common.props.elements[0].children[0].text, None);
|
||||
assert_eq!(f.common.props.elements[1].attrs, [("typeface".to_string(), "Arial".to_string())]);
|
||||
assert!(warnings.iter().any(|w| w == PROPS_CUT_WARNING), "{warnings:?}");
|
||||
}
|
||||
|
||||
/// A field with a third `<items>` (a public form carries an empty saved list twice) keeps it
|
||||
/// too, as an ordinary property, with no warning.
|
||||
#[test]
|
||||
fn a_third_items_list_is_kept_as_a_property() {
|
||||
let xml =
|
||||
template(r#"<field name="f"><items/><items save="1" presence="hidden"/><items save="1" presence="hidden"><text>0</text></items></field>"#);
|
||||
let (f, warnings) = first_field(&xml);
|
||||
assert!(warnings.iter().all(|w| w != PROPS_CUT_WARNING), "{warnings:?}");
|
||||
let lists: Vec<usize> = f.common.props.elements.iter().filter(|p| p.tag == "items").map(|p| p.children.len()).collect();
|
||||
assert_eq!(lists, [0, 0, 1]);
|
||||
}
|
||||
|
||||
/// A calculation is kept with its override, message and first script (XFA 3.3 p. 641: one
|
||||
/// script), as a validation is; the field still calculates with that script.
|
||||
#[test]
|
||||
fn a_calculation_is_kept_with_its_override_message_and_script() {
|
||||
let xml = template(
|
||||
r#"<field name="f"><calculate override="warning"><message><text>Computed</text></message><script>a + b</script><script>c</script></calculate></field>"#,
|
||||
);
|
||||
let (f, _) = first_field(&xml);
|
||||
let calc = f.common.props.elements.first().expect("the calculation");
|
||||
assert_eq!((calc.tag.as_str(), calc.attrs.as_slice()), ("calculate", [("override".to_string(), "warning".to_string())].as_slice()));
|
||||
let kids: Vec<(&str, Option<&str>)> = calc.children.iter().map(|k| (k.tag.as_str(), k.text.as_deref())).collect();
|
||||
assert_eq!(kids, [("message", None), ("script", Some("a + b"))]);
|
||||
assert_eq!(calc.children[0].children[0].text.as_deref(), Some("Computed"));
|
||||
assert_eq!(f.calculate.as_ref().map(|s| s.text.as_str()), Some("a + b"));
|
||||
}
|
||||
|
||||
/// A caption's rich text is kept as its words, and a field's items are kept whole, each entry
|
||||
/// with its text as the list shows it, outside the object's property budget (a long list of
|
||||
/// countries must not crowd out the other properties).
|
||||
#[test]
|
||||
fn a_caption_s_rich_text_and_a_list_s_items_are_kept() {
|
||||
let entries: String = (0..400).map(|i| format!("<text>Country {i}</text>")).collect();
|
||||
let xml = template(&format!(
|
||||
r#"<field name="f"><caption><value><exData contentType="text/html"><body xmlns="http://www.w3.org/1999/xhtml"><p>Date of <span style="font-weight:bold">birth</span>:</p></body></exData></value></caption><items>{entries}</items><items save="1"><integer>1</integer></items><border><edge/></border></field>"#
|
||||
));
|
||||
let (f, warnings) = first_field(&xml);
|
||||
assert!(warnings.iter().all(|w| w != PROPS_CUT_WARNING), "{warnings:?}");
|
||||
let tags: Vec<&str> = f.common.props.elements.iter().map(|p| p.tag.as_str()).collect();
|
||||
assert_eq!(tags, ["caption", "items", "items", "border"]);
|
||||
let exdata = &f.common.props.elements[0].children[0].children[0];
|
||||
assert_eq!((exdata.tag.as_str(), exdata.text.as_deref()), ("exData", Some("Date of birth:")));
|
||||
let shown = &f.common.props.elements[1];
|
||||
assert_eq!((shown.children.len(), shown.children[399].text.as_deref()), (400, Some("Country 399")));
|
||||
assert_eq!(f.common.props.elements[2].attrs, [("save".to_string(), "1".to_string())]);
|
||||
assert_eq!(f.items.len(), 400, "the list shows the same entries");
|
||||
}
|
||||
@@ -3,11 +3,12 @@
|
||||
//! survive undo and reopening), and laying the form out again after scripts changed it.
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::sync::Arc;
|
||||
|
||||
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, Props, Script, SomIndices, Template};
|
||||
use crate::{Report, XfaError};
|
||||
|
||||
/// Private key on the AcroForm: presence scripts set, by SOM path.
|
||||
@@ -49,6 +50,9 @@ pub struct FormNode {
|
||||
pub children: Vec<FormNode>,
|
||||
/// Fields: the display picture (see [`Field::display_picture`]), for `formattedValue`.
|
||||
pub display_picture: Option<String>,
|
||||
/// The object's attributes and property elements as the template gives them, for scripts to
|
||||
/// read: shared with the template and every other instance of the object, never copied.
|
||||
pub props: Arc<Props>,
|
||||
}
|
||||
|
||||
/// A script the template runs for an event of one node instance.
|
||||
@@ -269,6 +273,7 @@ impl Walker<'_> {
|
||||
index,
|
||||
children: Vec::new(),
|
||||
display_picture: f.display_picture.clone(),
|
||||
props: Arc::clone(&f.common.props),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,6 +338,7 @@ impl Walker<'_> {
|
||||
index,
|
||||
children,
|
||||
display_picture: None,
|
||||
props: Arc::clone(&sf.common.props),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -349,6 +355,7 @@ impl Walker<'_> {
|
||||
occur_max: Some(1),
|
||||
index: sib,
|
||||
children,
|
||||
props: Arc::clone(&a.common.props),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
@@ -360,6 +367,7 @@ impl Walker<'_> {
|
||||
som: som.clone(),
|
||||
kind: Some(NodeKind::Draw),
|
||||
value: d.value.plain().unwrap_or_default(),
|
||||
props: Arc::clone(&d.common.props),
|
||||
presence: self.presence(d.common.name.as_deref(), &som, d.common.presence),
|
||||
access: "open".into(),
|
||||
occur_min: 1,
|
||||
@@ -393,6 +401,7 @@ impl Walker<'_> {
|
||||
occur_max: Some(1),
|
||||
index: sib,
|
||||
children,
|
||||
props: Arc::clone(&g.common.props),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5308,8 +5308,8 @@ milestone = "M12"
|
||||
status = "partial"
|
||||
commands = ["file.open"]
|
||||
tools = ["doc_open", "doc_info", "form_fields", "form_fill", "js_run"]
|
||||
evidence = ["crates/xfa/src/tests.rs::layout_places_captions_fields_and_sizes_containers_to_their_content", "crates/xfa/src/tests.rs::tables_break_across_pages_and_repeat_their_header_row", "crates/xfa/src/tests.rs::rendering_into_the_pdf_replaces_the_placeholder_page_and_adds_fields", "crates/engine/src/tests.rs::dynamic_xfa_forms_are_laid_out_on_open_filled_and_saved_incrementally", "crates/automation/tests/it/automation.rs::dynamic_xfa_forms_open_render_fill_and_save_through_tools", "crates/ui-egui/tests/it/xfa.rs::a_dynamic_xfa_form_is_laid_out_and_filled", "crates/xfa/src/tests.rs::nested_width_less_subforms_lay_out_in_polynomial_time", "crates/js/src/tests.rs::instance_managers_add_remove_and_count", "crates/xfa/src/tests.rs::the_form_tree_carries_values_presence_instances_and_the_events", "crates/engine/src/tests.rs::xfa_scripts_initialize_calculate_validate_toggle_and_add_rows", "crates/engine/src/tests.rs::xfa_date_arithmetic_reads_the_date_as_its_display_picture_shows_it", "crates/automation/tests/it/automation.rs::xfa_scripts_run_for_buttons_and_field_changes_through_tools", "crates/ui-egui/tests/it/xfa.rs::xfa_buttons_run_their_scripts_in_the_app", "crates/engine/src/tests.rs::hostile_xfa_open_scripts_are_merged_capped_kept_from_printing_and_noted", "crates/engine/src/tests.rs::runaway_xfa_calculations_at_open_end_quickly_with_a_report", "crates/ui-egui/tests/it/xfa.rs::a_form_whose_scripts_were_turned_off_at_open_says_so", "crates/ui-egui/tests/it/xfa.rs::scripts_turned_off_by_a_click_bring_a_closed_notice_bar_back", "crates/automation/tests/it/automation.rs::doc_info_says_when_a_runaway_script_turned_the_forms_scripts_off", "crates/xfa/src/tests.rs::the_live_form_of_nested_repeating_subforms_stops_at_its_node_budget", "crates/xfa/src/tests.rs::data_ops_of_one_event_are_one_datasets_rewrite", "crates/xfa/src/tests.rs::choice_lists_signatures_passwords_and_pictures_become_fields_and_images", "crates/xfa/src/tests.rs::choice_values_come_from_the_data_as_saved_values_and_several_for_lists", "crates/xfa/src/tests.rs::pictures_are_sized_from_their_headers_and_hostile_ones_are_refused_unallocated", "crates/engine/src/tests.rs::xfa_choice_lists_signature_and_picture_fields_open_fill_script_and_save", "crates/xfa/src/tests.rs::repeating_subforms_inside_positioned_content_get_their_instances", "crates/engine/src/tests.rs::xfa_rows_in_areas_are_laid_out_read_calculated_and_written_where_adobe_binds_them", "crates/xfa/src/tests.rs::page_area_without_occur_serves_every_body_page", "apps/pdfcraft-cli/tests/it/xfa.rs::info_reports_the_laid_out_pages_and_fields", "apps/pdfcraft-cli/tests/it/xfa.rs::text_reads_the_layout_including_a_page_absent_from_the_placeholder", "apps/pdfcraft-cli/tests/it/xfa.rs::render_matches_the_engine_layout_on_the_second_page", "apps/pdfcraft-cli/tests/it/xfa.rs::a_laid_out_form_keeps_the_original_copy_permissions", "crates/engine/src/tests.rs::filling_growable_xfa_text_reflows_and_undo_restores_the_layout", "crates/engine/src/tests.rs::xfa_text_growth_refreshes_widget_geometry_but_fixed_height_fields_keep_their_widgets", "crates/automation/tests/it/automation.rs::growable_xfa_text_paginates_after_fill_save_and_print", "crates/engine/src/tests.rs::an_xfa_script_that_fails_partway_keeps_what_it_changed_before_the_error", "crates/engine/src/tests.rs::repeated_xfa_print_buttons_run_their_click_script", "crates/automation/tests/it/automation.rs::repeated_xfa_print_buttons_work_through_tools_and_after_reopen", "crates/xfa/src/tests.rs::rich_text_max_length_reaches_the_field_and_layout", "crates/automation/tests/it/automation.rs::xfa_rich_text_max_length_is_reported_and_enforced_through_tools", "crates/xfa/src/tests.rs::bmp_and_tiff_pictures_are_decoded_and_drawn", "crates/engine/src/tests.rs::active_xfa_does_not_run_pdf_container_scripts_but_keeps_requested_alerts", "crates/engine/src/tests.rs::active_xfa_without_template_scripts_does_not_initialize_pdf_fallback_scripts", "crates/engine/src/tests.rs::inactive_xfa_and_ordinary_pdfs_keep_document_helpers_and_alerts", "crates/automation/tests/it/automation.rs::active_xfa_skips_container_fallback_scripts_after_save_and_reopen", "crates/xfa/src/tests.rs::laying_a_form_out_takes_off_the_usage_rights_it_voids", "crates/engine/src/tests.rs::a_dynamic_xfa_form_is_saved_without_the_usage_rights_its_layout_voided", "crates/xfa/src/tests.rs::faces_measure_text_at_their_standard_14_widths", "crates/xfa/src/tests.rs::rich_text_links_keep_their_address", "crates/xfa/src/tests.rs::links_in_rich_text_become_link_annotations_one_per_line", "crates/xfa/src/tests.rs::a_links_pieces_share_one_address", "crates/xfa/src/tests.rs::a_rule_between_two_pieces_of_a_link_keeps_them_apart", "crates/xfa/src/tests.rs::links_to_anything_but_web_and_email_addresses_stay_plain_text", "crates/automation/tests/it/automation.rs::links_in_xfa_rich_text_are_listed_through_tools", "crates/xfa/src/tests.rs::rounded_corners_make_a_border_one_rounded_shape", "crates/xfa/src/testsLine truncated
|
||||
notes = "Active XFA layout skips PDF-container document-script initialization for the Acrobat console/field context, so fallback-viewer scripts do not run there. Scripts remain in the file/listing; explicit console/field and XFA alerts still run. Ordinary PDF, static XFA and unlaid-out shells keep document scripts; static-conversion undo restores the XFA policy. Changed growable multiline text (no fixed h) reflows from final datasets after edits, with or without scripts; pages/widgets refresh and undo/redo, save/reopen and print retain all text. Oversized text still uses the existing read-only page-text fallback with a warning; fixed-height fields do not reflow. Repeated page-master buttons resolve their own PDF field to the shared XFA click script; the numPages-based print script requests printing from every copy, including after save/reopen (#2572). Rich-text exData/maxLength is carried into AcroForm MaxLen, reported by form_fields and enforced by form_fill (Unicode characters); limits and accepted values survive save/reopen (#2570). laid out from the template on open by the xfa crate (positioned, tb, lr-tb, table/row, page masters (a page area with no occur repeats without limit, one with occur max=1 is used once, one with occur min=1 the body never reached gets a page after the body, as Acrobat Pro does on a synthetic form), breaks, repeated header rows, borders and fills of flowed subforms (a page break between their children gives each page's part a closed border, the margins repeated at the cut, as measured on a synthetic form in another viewer, and a child's own page break breaks them the same way; one holding an item that doesn't fit on its page, cut across pages or placed past the content area's bottom, is drawn without its border, with a layout warning), captions, rounded and inverted border corners, rich text (its http, https and mailto links clickable), JPEG/PNG/GIF/BMP/TIFF images) into real pages and AcroForm fields (text, password, date with AFDate formats, check boxes and radio groups (each box placed by its field's margins, caption and paragraph alignment as measured on two synthetic forms in another viewer: with no caption at the right of the content area, beside a reserved caption where the alignment puts it, and a caption without a reserve taking the rest of the field; a check box's own margin larger than the box draws a bigger box there, which isn't copied), drop-downs and list boxes with shown/saved item pairs and multi-select, signature fields (each named by its SOM path, under a generated field for each step above it, so that another viewer, which lays the form out again, finds a signature made here, as seen on a synthetic form, and a box signed here keeps its signature, and that name, when a script lays the form out again; a box whose path, or a part of it, the file already gives another field keeps its own name, and its signature isn't found there yet), image fields showing their picture (pictures decoded once per document, at most 20 million pixels, and reused across re-layouts), buttons with reset/print/save/URL), filled from the datasets packet (including rows another viewer added), saved as an incremental revision with the values merged into the datasets so Adobe's viewers show them; usage rights the form carried (/Perms /UR3) are taken off with a note, as the layout voids them and a form with void usage rights opens with every field locked; doc_info reports xfa_layout. CLI info reports the laid-out page/field counts and dimensions while retaining the input file size and raw counts as pdf_pages/acroform_fields; text and render use those pages too, preserving copy permissions and leaving the source file unchanged (#1645). XFA JavaScript and FormCalc (form.xfa-formcalc) run in the js crate's XFA object model (xfa.form, SOM resolution, rawValue, formattedValue in a date display picture's pattern (number and text display pictures still give the raw value), presence, access, instance managers, xfa.host, xfa.layout, xfa.event): initialize and calculate on open, exit, validate and calculate after a field changes, click for buttons (through js_run and the app), with rows added and removed, subforms shown and hidden, and the form laid out again, all as one undo step and kept in the file. All the scripts of one event run on one thread, each in a fresh context on the form as the engine holds it then (nothing shared between scripts; the live form kept up to date in place for values, rebuilt only when the structure changed), so hundreds of calculations run within the open budget. Hostile scripts are bounded per event (merged and capped effects, messages and console lines, lower loop limits and a time budget on open and calculate, one datasets rewrite per event, print/save/links/focus from clicks only, a runaway script abandoned and the form's scripts turned off, which the app says in a toast and on the notice bar, with a button to the JavaScript console, and doc_info reports as xfa_scriptLine truncated
|
||||
evidence = ["crates/xfa/src/tests.rs::layout_places_captions_fields_and_sizes_containers_to_their_content", "crates/xfa/src/tests.rs::tables_break_across_pages_and_repeat_their_header_row", "crates/xfa/src/tests.rs::rendering_into_the_pdf_replaces_the_placeholder_page_and_adds_fields", "crates/engine/src/tests.rs::dynamic_xfa_forms_are_laid_out_on_open_filled_and_saved_incrementally", "crates/automation/tests/it/automation.rs::dynamic_xfa_forms_open_render_fill_and_save_through_tools", "crates/ui-egui/tests/it/xfa.rs::a_dynamic_xfa_form_is_laid_out_and_filled", "crates/xfa/src/tests.rs::nested_width_less_subforms_lay_out_in_polynomial_time", "crates/js/src/tests.rs::instance_managers_add_remove_and_count", "crates/xfa/src/tests.rs::the_form_tree_carries_values_presence_instances_and_the_events", "crates/engine/src/tests.rs::xfa_scripts_initialize_calculate_validate_toggle_and_add_rows", "crates/engine/src/tests.rs::xfa_date_arithmetic_reads_the_date_as_its_display_picture_shows_it", "crates/automation/tests/it/automation.rs::xfa_scripts_run_for_buttons_and_field_changes_through_tools", "crates/ui-egui/tests/it/xfa.rs::xfa_buttons_run_their_scripts_in_the_app", "crates/engine/src/tests.rs::hostile_xfa_open_scripts_are_merged_capped_kept_from_printing_and_noted", "crates/engine/src/tests.rs::runaway_xfa_calculations_at_open_end_quickly_with_a_report", "crates/ui-egui/tests/it/xfa.rs::a_form_whose_scripts_were_turned_off_at_open_says_so", "crates/ui-egui/tests/it/xfa.rs::scripts_turned_off_by_a_click_bring_a_closed_notice_bar_back", "crates/automation/tests/it/automation.rs::doc_info_says_when_a_runaway_script_turned_the_forms_scripts_off", "crates/xfa/src/tests.rs::the_live_form_of_nested_repeating_subforms_stops_at_its_node_budget", "crates/xfa/src/tests.rs::data_ops_of_one_event_are_one_datasets_rewrite", "crates/xfa/src/tests.rs::choice_lists_signatures_passwords_and_pictures_become_fields_and_images", "crates/xfa/src/tests.rs::choice_values_come_from_the_data_as_saved_values_and_several_for_lists", "crates/xfa/src/tests.rs::pictures_are_sized_from_their_headers_and_hostile_ones_are_refused_unallocated", "crates/engine/src/tests.rs::xfa_choice_lists_signature_and_picture_fields_open_fill_script_and_save", "crates/xfa/src/tests.rs::repeating_subforms_inside_positioned_content_get_their_instances", "crates/engine/src/tests.rs::xfa_rows_in_areas_are_laid_out_read_calculated_and_written_where_adobe_binds_them", "crates/xfa/src/tests.rs::page_area_without_occur_serves_every_body_page", "apps/pdfcraft-cli/tests/it/xfa.rs::info_reports_the_laid_out_pages_and_fields", "apps/pdfcraft-cli/tests/it/xfa.rs::text_reads_the_layout_including_a_page_absent_from_the_placeholder", "apps/pdfcraft-cli/tests/it/xfa.rs::render_matches_the_engine_layout_on_the_second_page", "apps/pdfcraft-cli/tests/it/xfa.rs::a_laid_out_form_keeps_the_original_copy_permissions", "crates/engine/src/tests.rs::filling_growable_xfa_text_reflows_and_undo_restores_the_layout", "crates/engine/src/tests.rs::xfa_text_growth_refreshes_widget_geometry_but_fixed_height_fields_keep_their_widgets", "crates/automation/tests/it/automation.rs::growable_xfa_text_paginates_after_fill_save_and_print", "crates/engine/src/tests.rs::an_xfa_script_that_fails_partway_keeps_what_it_changed_before_the_error", "crates/engine/src/tests.rs::repeated_xfa_print_buttons_run_their_click_script", "crates/automation/tests/it/automation.rs::repeated_xfa_print_buttons_work_through_tools_and_after_reopen", "crates/xfa/src/tests.rs::rich_text_max_length_reaches_the_field_and_layout", "crates/automation/tests/it/automation.rs::xfa_rich_text_max_length_is_reported_and_enforced_through_tools", "crates/xfa/src/tests.rs::bmp_and_tiff_pictures_are_decoded_and_drawn", "crates/engine/src/tests.rs::active_xfa_does_not_run_pdf_container_scripts_but_keeps_requested_alerts", "crates/engine/src/tests.rs::active_xfa_without_template_scripts_does_not_initialize_pdf_fallback_scripts", "crates/engine/src/tests.rs::inactive_xfa_and_ordinary_pdfs_keep_document_helpers_and_alerts", "crates/automation/tests/it/automation.rs::active_xfa_skips_container_fallback_scripts_after_save_and_reopen", "crates/xfa/src/tests.rs::laying_a_form_out_takes_off_the_usage_rights_it_voids", "crates/engine/src/tests.rs::a_dynamic_xfa_form_is_saved_without_the_usage_rights_its_layout_voided", "crates/xfa/src/tests.rs::faces_measure_text_at_their_standard_14_widths", "crates/xfa/src/tests.rs::rich_text_links_keep_their_address", "crates/xfa/src/tests.rs::links_in_rich_text_become_link_annotations_one_per_line", "crates/xfa/src/tests.rs::a_links_pieces_share_one_address", "crates/xfa/src/tests.rs::a_rule_between_two_pieces_of_a_link_keeps_them_apart", "crates/xfa/src/tests.rs::links_to_anything_but_web_and_email_addresses_stay_plain_text", "crates/automation/tests/it/automation.rs::links_in_xfa_rich_text_are_listed_through_tools", "crates/xfa/src/tests.rs::rounded_corners_make_a_border_one_rounded_shape", "crates/xfa/src/testsLine truncated
|
||||
notes = "Active XFA layout skips PDF-container document-script initialization for the Acrobat console/field context, so fallback-viewer scripts do not run there. Scripts remain in the file/listing; explicit console/field and XFA alerts still run. Ordinary PDF, static XFA and unlaid-out shells keep document scripts; static-conversion undo restores the XFA policy. Changed growable multiline text (no fixed h) reflows from final datasets after edits, with or without scripts; pages/widgets refresh and undo/redo, save/reopen and print retain all text. Oversized text still uses the existing read-only page-text fallback with a warning; fixed-height fields do not reflow. Repeated page-master buttons resolve their own PDF field to the shared XFA click script; the numPages-based print script requests printing from every copy, including after save/reopen (#2572). Rich-text exData/maxLength is carried into AcroForm MaxLen, reported by form_fields and enforced by form_fill (Unicode characters); limits and accepted values survive save/reopen (#2570). laid out from the template on open by the xfa crate (positioned, tb, lr-tb, table/row, page masters (a page area with no occur repeats without limit, one with occur max=1 is used once, one with occur min=1 the body never reached gets a page after the body, as Acrobat Pro does on a synthetic form), breaks, repeated header rows, borders and fills of flowed subforms (a page break between their children gives each page's part a closed border, the margins repeated at the cut, as measured on a synthetic form in another viewer, and a child's own page break breaks them the same way; one holding an item that doesn't fit on its page, cut across pages or placed past the content area's bottom, is drawn without its border, with a layout warning), captions, rounded and inverted border corners, rich text (its http, https and mailto links clickable), JPEG/PNG/GIF/BMP/TIFF images) into real pages and AcroForm fields (text, password, date with AFDate formats, check boxes and radio groups (each box placed by its field's margins, caption and paragraph alignment as measured on two synthetic forms in another viewer: with no caption at the right of the content area, beside a reserved caption where the alignment puts it, and a caption without a reserve taking the rest of the field; a check box's own margin larger than the box draws a bigger box there, which isn't copied), drop-downs and list boxes with shown/saved item pairs and multi-select, signature fields (each named by its SOM path, under a generated field for each step above it, so that another viewer, which lays the form out again, finds a signature made here, as seen on a synthetic form, and a box signed here keeps its signature, and that name, when a script lays the form out again; a box whose path, or a part of it, the file already gives another field keeps its own name, and its signature isn't found there yet), image fields showing their picture (pictures decoded once per document, at most 20 million pixels, and reused across re-layouts), buttons with reset/print/save/URL), filled from the datasets packet (including rows another viewer added), saved as an incremental revision with the values merged into the datasets so Adobe's viewers show them; usage rights the form carried (/Perms /UR3) are taken off with a note, as the layout voids them and a form with void usage rights opens with every field locked; doc_info reports xfa_layout. CLI info reports the laid-out page/field counts and dimensions while retaining the input file size and raw counts as pdf_pages/acroform_fields; text and render use those pages too, preserving copy permissions and leaving the source file unchanged (#1645). XFA JavaScript and FormCalc (form.xfa-formcalc) run in the js crate's XFA object model (xfa.form, SOM resolution, rawValue, formattedValue in a date display picture's pattern (number and text display pictures still give the raw value), presence, access, instance managers, xfa.host, xfa.layout, xfa.event, and property objects (XFA 3.3 ch. 3 p. 78, ch. 17), every property path the 367 inventoried forms read (the one known gap is value.text and value.exData, read by about 15 of them: a node's value stays its raw value, and changing what value returns is left to a follow-up with a full-corpus regression check): read as the template writes them, with the spec's defaults for what those forms read where it leaves them out (presence of border (hidden when the object has no border element, as readers read it), edge, corner and fill; an edge or corner past those the template lists, up to four (a border's fifth and later are out of reach, as the layout draws four), as the one drawn on that side (XFA 3.3 p. 38 says the last one listed is reused for the sides not listed; readers draw such a side from the edge or corner on the same side instead, two listed alternating, three reusing the second for the fourth, one serving every side, and reads follow the readers; with none Line truncated
|
||||
|
||||
[[feature]]
|
||||
id = "form.xfa-formcalc"
|
||||
|
||||
Reference in new issue
Block a user