diff --git a/crates/js/src/xfa/nodes.rs b/crates/js/src/xfa/nodes.rs index 2346dece8b..72a095eaa9 100644 --- a/crates/js/src/xfa/nodes.rs +++ b/crates/js/src/xfa/nodes.rs @@ -387,18 +387,22 @@ pub(super) fn resolver(ctx: &mut Context, from: usize, many: bool) -> JsResult props::list_object(ctx, props), + (false, true) => node_object(ctx, node), + (false, false) => props::prop_object(ctx, node, &path), + }; } if many { return list_object(ctx, &found); diff --git a/crates/js/src/xfa/props.rs b/crates/js/src/xfa/props.rs index aefc08b812..cc35251f36 100644 --- a/crates/js/src/xfa/props.rs +++ b/crates/js/src/xfa/props.rs @@ -701,8 +701,11 @@ fn resolve(h: &XHost, node: usize, path: &str, expr: &str) -> Vec { /// A SOM expression from node `from` that runs into an object's properties /// (`resolveNode("ui.#textEdit")`, `xfa.resolveNode("form1.page.f.border.edge[1]")`): the /// objects the longest leading part names (each instance of `row[*]`), and the properties the -/// rest names from each, as (node, path). Empty when no part of it does, or the expression has -/// more than [`super::model::MAX_DEPTH`] steps. +/// rest names from each, as (node, path). A child named as the property comes first, for each +/// object on its own: where one has such a child, what the rest names from it is listed in +/// place, as (child, "") (`row[*].border` with a field `border` in one row and only the property +/// in another). Empty when no part of it names a property, or the expression has more than +/// [`super::model::MAX_DEPTH`] steps. pub(crate) fn resolve_into_props(h: &XHost, from: usize, expr: &str) -> Vec<(usize, String)> { let segs: Vec<&str> = expr.trim().split('.').collect(); if segs.len() > super::model::MAX_DEPTH { @@ -727,10 +730,19 @@ pub(crate) fn resolve_into_props(h: &XHost, from: usize, expr: &str) -> Vec<(usi // carry: `value.#text` on a draw stays unresolved (null) rather than reading as // empty, until a node's value is its value element. .filter(|&n| !(first == "value" && h.nodes.get(n).is_some_and(|x| x.kind == XfaKind::Draw))) - .flat_map(|n| resolve(h, n, "", &rest).into_iter().map(move |p| (n, p))) + .flat_map(|n| { + let named = h.nodes.get(n).is_some_and(|x| x.children.iter().any(|&c| h.nodes.get(c).is_some_and(|c| !c.gone && c.name == first))); + let children = if named { super::resolve_som(h, n, &rest) } else { Vec::new() }; + if children.is_empty() { + resolve(h, n, "", &rest).into_iter().map(|p| (n, p)).collect::>() + } else { + children.into_iter().map(|c| (c, String::new())).collect() + } + }) .take(MAX_RESOLVED) .collect(); - if !found.is_empty() { + // Children alone are no property: the expression names nodes, as `resolve_som` finds them. + if found.iter().any(|(_, p)| !p.is_empty()) { return found; } } @@ -1419,6 +1431,7 @@ pub(crate) fn list_object(ctx: &mut Context, items: Vec<(usize, String)>) -> JsR return Ok(JsValue::null()); } match listed(key, ctx)?.get(i as usize) { + Some((node, p)) if p.is_empty() => super::nodes::node_object(ctx, *node), Some((node, p)) => prop_object(ctx, *node, p), None => Ok(JsValue::null()), } @@ -1428,10 +1441,17 @@ pub(crate) fn list_object(ctx: &mut Context, items: Vec<(usize, String)>) -> JsR let items = listed(key, ctx)?; let found = { let h = host(ctx)?.borrow(); - let named = |(n, p): &&(usize, String)| template_prop(&h, *n, p).is_some_and(|e| e.attrs.iter().any(|(k, v)| k == "name" && *v == name)); - items.iter().find(named).or_else(|| items.iter().find(|(_, p)| tag_of(p) == name)).cloned() + let named = |(n, p): &&(usize, String)| { + if p.is_empty() { + h.nodes.get(*n).is_some_and(|x| x.name == name) + } else { + template_prop(&h, *n, p).is_some_and(|e| e.attrs.iter().any(|(k, v)| k == "name" && *v == name)) + } + }; + items.iter().find(named).or_else(|| items.iter().find(|(_, p)| !p.is_empty() && tag_of(p) == name)).cloned() }; match found { + Some((node, p)) if p.is_empty() => super::nodes::node_object(ctx, node), Some((node, p)) => prop_object(ctx, node, &p), None => Ok(JsValue::null()), } diff --git a/crates/js/src/xfa/props_tests.rs b/crates/js/src/xfa/props_tests.rs index 94a1a8c5b8..93445cd8ea 100644 --- a/crates/js/src/xfa/props_tests.rs +++ b/crates/js/src/xfa/props_tests.rs @@ -569,6 +569,32 @@ fn resolve_nodes_through_instances_gives_each_instances_property() { assert_eq!((o.error, o.result.as_deref()), (None, Some("2|1,1,1|2,2,2"))); } +/// A child named as a property comes first for each instance on its own: with a field `border` +/// in the first row and only the border property in the second, `row[*].border` lists the +/// field and then the second row's border. +#[test] +fn resolve_nodes_takes_a_child_over_a_property_instance_by_instance() { + let row = |i: usize, children: Vec| XfaNode { + som: format!("form1[0].row[{i}]"), + index: i, + repeatable: true, + children, + ..node("row", XfaKind::Subform, &[], vec![p("border", &[("presence", "hidden")], vec![])]) + }; + let field = XfaNode { som: "form1[0].row[0].border[0]".into(), ..node("border", XfaKind::Field, &[], vec![]) }; + let root = XfaNode { + name: "form1".into(), + som: "form1[0]".into(), + kind: Some(XfaKind::Subform), + children: vec![row(0, vec![field]), row(1, Vec::new())], + ..Default::default() + }; + let script = "var l = xfa.resolveNodes('xfa.form.form1.row[*].border'); \ + [l.length, l.item(0).className, l.item(1).className, l.item(1).presence, l.namedItem('border').className].join('|')"; + let o = go_on(&root, script, "row"); + assert_eq!((o.error, o.result.as_deref()), (None, Some("2|field|border|hidden|field"))); +} + /// `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.