From 17ad260d714bd52b8c25c79950c03ffd4f70bb20 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Fri, 30 Oct 2026 06:31:39 +0800 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv --- crates/js/src/xfa/props.rs | 20 ++++++++++---------- crates/js/src/xfa/props_tests.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/crates/js/src/xfa/props.rs b/crates/js/src/xfa/props.rs index cc35251f36..3a1f7c0c46 100644 --- a/crates/js/src/xfa/props.rs +++ b/crates/js/src/xfa/props.rs @@ -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::>() + 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::>() } }) .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() }; diff --git a/crates/js/src/xfa/props_tests.rs b/crates/js/src/xfa/props_tests.rs index 93445cd8ea..521df84a78 100644 --- a/crates/js/src/xfa/props_tests.rs +++ b/crates/js/src/xfa/props_tests.rs @@ -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.