mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 05:42:52 +00:00
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:
1 parent
8d40450d22
commit
5a6ab51bf9
3 files changed
+67
-17
No files matched your search
+15
-11
@@ -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);
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user