diff --git a/crates/xfa/src/parse.rs b/crates/xfa/src/parse.rs index cec433f469..7f8a080be4 100644 --- a/crates/xfa/src/parse.rs +++ b/crates/xfa/src/parse.rs @@ -662,7 +662,9 @@ fn parse_subform(n: XmlNode, warnings: &mut Vec, depth: usize) -> Subfor } let column_widths: Vec = n.attribute("columnWidths").map(|s| s.split_whitespace().filter_map(measure).map(|w| w.max(0.0)).take(256).collect()).unwrap_or_default(); - let break_to_page = |name: &str| children(n, name).any(|b| b.attribute("targetType").is_none_or(|t| t == "pageArea" || t == "contentArea")); + // A break to a page or content area. With no targetType, the default is `auto`: "layout + // continues using the current layout container" (XFA 3.3 p. 638), so no break. + let break_to_page = |name: &str| children(n, name).any(|b| b.attribute("targetType").is_some_and(|t| t == "pageArea" || t == "contentArea")); Subform { common, layout: Layout::parse(n.attribute("layout")), diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index af3cb094bd..35a25077f0 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -4525,6 +4525,67 @@ fn clear_password_values_clears_static_xfa_password_fields() { assert!(!packets.xdp.contains("hunter2"), "{}", packets.xdp); } +/// The pages of `form[0].last[0].f[0]` and of the field `after` that follows `last`, when `last` +/// holds `before` ahead of `f` and `after` behind it. A breakAfter acts after its subform, so the +/// page of `after` shows whether it broke. +fn pages_around_break(before: &str, after: &str) -> (Option, Option) { + let xml = format!( + r#""# + ); + let t = crate::parse(&xml).unwrap().0; + let form = crate::layout::layout(&t, None).unwrap(); + let page_of = |som: &str| form.pages.iter().position(|p| widgets(p).iter().any(|w| w.som == som)); + (page_of("form[0].last[0].f[0]"), page_of("form[0].after[0]")) +} + +/// A `` without a targetType breaks nothing: its default is `auto`, layout goes on +/// in the current container (XFA 3.3 p. 638), as a public form's last section expects. Written +/// out as `auto` it breaks nothing either. One to a page area, or a content area, still starts a +/// new page; so does a `breakAfter` to one. +#[test] +fn a_break_before_without_a_target_type_does_not_start_a_page() { + let page_before = |brk: &str| pages_around_break(brk, "").0; + assert_eq!(page_before(""), Some(0), "auto: no break"); + assert_eq!(page_before(r#""#), Some(0)); + assert_eq!(page_before(r#""#), Some(1)); + assert_eq!(page_before(r#""#), Some(1)); + let page_after = |brk: &str| pages_around_break("", brk).1; + assert_eq!(page_after(""), Some(0), "auto: no break"); + assert_eq!(page_after(r#""#), Some(0)); + assert_eq!(page_after(r#""#), Some(1)); + assert_eq!(page_after(r#""#), Some(1)); +} + +/// targetType is one of `auto`, `contentArea` and `pageArea` (XFA 3.3 pp. 636-638). An empty or +/// unknown value, in either direction, names neither a page nor a content area, so it breaks +/// nothing; the names are case-sensitive. +#[test] +fn a_break_with_an_empty_or_unknown_target_type_does_not_start_a_page() { + for value in ["", " ", "page", "pagearea", "PageArea", "next"] { + let before = format!(r#""#); + assert_eq!(pages_around_break(&before, "").0, Some(0), "breakBefore targetType={value:?}"); + let after = format!(r#""#); + assert_eq!(pages_around_break("", &after).1, Some(0), "breakAfter targetType={value:?}"); + } +} + +/// The deprecated `` form (XFA 3.3 pp. 633-634): `auto`, its default, forces +/// no transition; `pageArea` and `contentArea` start a new page; an empty or unknown value breaks +/// nothing. +#[test] +fn the_deprecated_break_before_form_breaks_only_to_a_page_or_content_area() { + let page = |brk: &str| pages_around_break(brk, "").0; + assert_eq!(page(""), Some(0), "auto: no break"); + assert_eq!(page(r#""#), Some(0)); + assert_eq!(page(r#""#), Some(0)); + assert_eq!(page(r#""#), Some(0)); + assert_eq!(page(r#""#), Some(1)); + assert_eq!(page(r#""#), Some(1)); +} + /// A form with `draws` in a subform placed by position, on a letter page whose content area is a /// quarter inch in, with `master` on the page itself. fn form_of_draws(master: &str, draws: &str) -> String {