diff --git a/crates/xfa/src/parse.rs b/crates/xfa/src/parse.rs index 838d1730ea..b1f4f36e04 100644 --- a/crates/xfa/src/parse.rs +++ b/crates/xfa/src/parse.rs @@ -602,7 +602,7 @@ fn parse_page_set(n: XmlNode, warnings: &mut Vec, depth: usize) -> PageS if content.is_empty() { content.push(Rect::new(0.0, 0.0, width, height)); } - let items = pa.children().filter(|c| c.is_element()).filter_map(|c| parse_node(c, warnings, depth + 1)).collect(); + let items = pa.children().filter(|c| c.is_element()).filter_map(|c| parse_node(c, warnings, depth + 1, Layout::Positioned)).collect(); // A page area with no `` has no limit and no minimum (a subform's default of one // would exhaust it, and demand a page for every master). let occur = child(pa, "occur").map(|o| parse_occur(Some(o))); @@ -618,15 +618,16 @@ fn parse_page_set(n: XmlNode, warnings: &mut Vec, depth: usize) -> PageS PageSet { areas } } -fn parse_subform(n: XmlNode, warnings: &mut Vec, depth: usize) -> Subform { +fn parse_subform(n: XmlNode, warnings: &mut Vec, depth: usize, set_layout: Option) -> Subform { let common = parse_common(n); + let layout = set_layout.unwrap_or_else(|| Layout::parse(n.attribute("layout"))); let mut children_out = Vec::new(); let mut page_set = None; for c in n.children().filter(|c| c.is_element()) { match c.tag_name().name() { "pageSet" => page_set = Some(parse_page_set(c, warnings, depth + 1)), _ => { - if let Some(node) = parse_node(c, warnings, depth + 1) { + if let Some(node) = parse_node(c, warnings, depth + 1, layout) { children_out.push(node); } } @@ -637,7 +638,7 @@ fn parse_subform(n: XmlNode, warnings: &mut Vec, depth: usize) -> Subfor let break_to_page = |name: &str| children(n, name).any(|b| b.attribute("targetType").is_none_or(|t| t == "pageArea" || t == "contentArea")); Subform { common, - layout: Layout::parse(n.attribute("layout")), + layout, children: children_out, scripts: parse_events(n), page_set, @@ -650,21 +651,23 @@ fn parse_subform(n: XmlNode, warnings: &mut Vec, depth: usize) -> Subfor } } -fn parse_node(c: XmlNode, warnings: &mut Vec, depth: usize) -> Option { +/// `parent`: the layout of the container `c` is in, which a `subformSet` takes on. +fn parse_node(c: XmlNode, warnings: &mut Vec, depth: usize, parent: Layout) -> Option { if depth > MAX_DEPTH { return None; } Some(match c.tag_name().name() { - "subform" => Node::Subform(Box::new(parse_subform(c, warnings, depth))), + "subform" => Node::Subform(Box::new(parse_subform(c, warnings, depth, None))), + // A set of related subforms is transparent to layout (XFA 3.3 p. 357): its members are laid out + // as its container lays out its own children, at their own positions in a positioned one. In a + // table or a row they go one under another. "subformSet" => { - // A set of alternative subforms: lay out its members in order. - let mut sf = parse_subform(c, warnings, depth); - sf.layout = Layout::Tb; - Node::Subform(Box::new(sf)) + let layout = if matches!(parent, Layout::Table | Layout::Row) { Layout::Tb } else { parent }; + Node::Subform(Box::new(parse_subform(c, warnings, depth, Some(layout)))) } "area" => Node::Area(Box::new(Area { common: parse_common(c), - children: c.children().filter(|k| k.is_element()).filter_map(|k| parse_node(k, warnings, depth + 1)).collect(), + children: c.children().filter(|k| k.is_element()).filter_map(|k| parse_node(k, warnings, depth + 1, Layout::Positioned)).collect(), })), "field" => Node::Field(Box::new(parse_field(c, warnings))), "draw" => Node::Draw(Box::new(Draw { common: parse_common(c), value: parse_value(child(c, "value"), warnings) })), @@ -742,7 +745,7 @@ pub fn parse(xml: &str) -> Result<(Template, Vec), XfaError> { }; let mut warnings = Vec::new(); let sf = children(template, "subform").next().ok_or_else(|| XfaError::Malformed("the template has no root subform".into()))?; - let mut root_sf = parse_subform(sf, &mut warnings, 0); + let mut root_sf = parse_subform(sf, &mut warnings, 0, None); if root_sf.page_set.is_none() { // A page set may sit beside the root subform. root_sf.page_set = child(template, "pageSet").map(|ps| parse_page_set(ps, &mut warnings, 0)); diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index e424be5441..5383e98b41 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -4145,3 +4145,29 @@ fn clear_password_values_clears_static_xfa_password_fields() { let packets = read_packets(&doc).unwrap().unwrap(); assert!(!packets.xdp.contains("hunter2"), "{}", packets.xdp); } + +/// A `subformSet` is transparent to layout (XFA 3.3 p. 357): in a positioned subform its members +/// sit at their own positions, side by side as a public stamp-duty form's two columns do, not one +/// under the other; in a flowed one they still go one under another. +#[test] +fn a_subform_set_in_a_positioned_subform_places_its_members_where_they_are() { + let xml = |layout: &str| { + format!( + r#""# + ) + }; + let baseline = |layout: &str, text: &str| { + let form = crate::layout::layout(&crate::parse(&xml(layout)).unwrap().0, None).unwrap(); + form.pages[0].items.iter().find_map(|i| if let Item::Text(s) = i { (s.text == text).then_some(s.baseline) } else { None }).unwrap() + }; + assert!((baseline(r#"h="3in""#, "left") - baseline(r#"h="3in""#, "right")).abs() < 0.01, "positioned: side by side"); + assert!(baseline(r#"layout="tb""#, "right") - baseline(r#"layout="tb""#, "left") > 140.0, "flowed: one under the other"); +}