M12: an XFA child named as a property shadows it for the whole expression

`row.border.edge[0]` with a field `border` in the row no longer falls back
to the row's own border when the field has no `edge`; `row.#border` still
names the property by class (XFA 3.3 p. 96). `namedItem` on a list of
property objects reads each name as written in the run.

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 06:31:39 +08:00
1 parent 5a6ab51bf9
commit 17ad260d71
2 files changed
+36 -10

No files matched your search

+10 -10
View File
@@ -715,6 +715,8 @@ pub(crate) fn resolve_into_props(h: &XHost, from: usize, expr: &str) -> Vec<(usi
let (Some(head), Some(rest)) = (segs.get(..i), segs.get(i..)) else { continue };
let first = rest.first().map_or("", |s| s.trim());
let first = first.split('[').next().unwrap_or(first);
// `#border` names the property by its class (XFA 3.3 p. 96), past a child named `border`.
let by_class = first.starts_with('#');
let first = first.strip_prefix('#').unwrap_or(first);
// Only an object's properties start the rest (the parser's property tags): the
// prefix is resolved only then.
@@ -731,12 +733,14 @@ pub(crate) fn resolve_into_props(h: &XHost, from: usize, expr: &str) -> Vec<(usi
// 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| {
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<_>>()
let named = !by_class
&& 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)));
// The child shadows the property for the whole rest: what the child lacks names nothing
// here (a property of the child is found where the head names the child).
if named {
super::resolve_som(h, n, &rest).into_iter().map(|c| (c, String::new())).collect()
} else {
children.into_iter().map(|c| (c, String::new())).collect()
resolve(h, n, "", &rest).into_iter().map(|p| (n, p)).collect::<Vec<_>>()
}
})
.take(MAX_RESOLVED)
@@ -1442,11 +1446,7 @@ pub(crate) fn list_object(ctx: &mut Context, items: Vec<(usize, String)>) -> JsR
let found = {
let h = host(ctx)?.borrow();
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))
}
if p.is_empty() { h.nodes.get(*n).is_some_and(|x| x.name == name) } else { attr(&h, *n, p, "name").is_some_and(|v| v == name) }
};
items.iter().find(named).or_else(|| items.iter().find(|(_, p)| !p.is_empty() && tag_of(p) == name)).cloned()
};
+26
View File
@@ -445,6 +445,11 @@ 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"),
// A name written in the run is the one `namedItem` finds.
(
"this.desc.nodes.namedItem('title').setAttribute('heading', 'name'), String(this.desc.nodes.namedItem('title')) + '|' + this.desc.nodes.namedItem('heading').value",
"null|Title",
),
("this.extras.nodes.namedItem('note').value", "N"),
("this.extras.note.value", "N"),
("this.bind.ref", "$.a"),
@@ -595,6 +600,27 @@ fn resolve_nodes_takes_a_child_over_a_property_instance_by_instance() {
assert_eq!((o.error, o.result.as_deref()), (None, Some("2|field|border|hidden|field")));
}
/// A child named as a property shadows that property for the rest of the expression too
/// (`row.border.edge[0]` with a field `border`): the rest is resolved from the child, and when the
/// child has no such property the expression names nothing; it does not fall back to the parent's
/// property. `#border` names the property by class (XFA 3.3 p. 96), so it still reaches it.
#[test]
fn a_child_named_as_a_property_shadows_it_for_the_rest_of_the_expression() {
let edges = |presence: &str| vec![p("edge", &[("presence", presence)], vec![])];
let field =
XfaNode { som: "form1[0].row[0].border[0]".into(), ..node("border", XfaKind::Field, &[], vec![p("border", &[], edges("invisible"))]) };
let row = XfaNode {
som: "form1[0].row[0]".into(),
children: vec![field],
..node("row", XfaKind::Subform, &[], vec![p("border", &[], edges("hidden"))])
};
let root = XfaNode { name: "form1".into(), som: "form1[0]".into(), kind: Some(XfaKind::Subform), children: vec![row], ..Default::default() };
let script = "var r = function (s) { var o = xfa.resolveNode('xfa.form.form1.row.' + s); return o === null ? 'null' : o.presence; }; \
[r('border.border.edge[0]'), r('border.edge[0]'), r('#border.edge[0]')].join('|')";
let o = go_on(&root, script, "row");
assert_eq!((o.error, o.result.as_deref()), (None, Some("invisible|null|hidden")));
}
/// `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.