M12: XFA resolveNodes takes a child over a property row by row

`row[*].border` with a field named `border` in one row and only the border
property in another listed the field and dropped the other row's property:
the property was looked up only when no child matched at all. Each object
the expression names now decides on its own: a child named as the property
comes first, and an object without one gives its property, in order.
resolveNodes' list holds both, and item() and namedItem() return a node or
a property object accordingly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
This commit is contained in:
mtlcl410andClaude Opus 5.5 committed 2026-10-30 01:54:18 +08:00
1 parent 8d40450d22
commit 5a6ab51bf9
3 files changed
+67 -17

No files matched your search

+15 -11
View File
@@ -387,18 +387,22 @@ 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) };
// An expression that runs into an object's properties (`ui.#textEdit`, `border.edge[1]`),
// for some of the objects it names at least: a child named as the property comes first
// for each object on its own, in place ((child, "")).
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().filter(|(_, p)| !p.is_empty()).for_each(|(n, p)| h.instantiate(*n, p));
}
return match (many, path.is_empty()) {
(true, _) => 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);
+26 -6
View File
@@ -701,8 +701,11 @@ fn resolve(h: &XHost, node: usize, path: &str, expr: &str) -> Vec<String> {
/// 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::<Vec<_>>()
} 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()),
}
+26
View File
@@ -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>| 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.