1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum MergeMode {
32 ConsumeData,
34 MatchTemplate,
36}
37
38pub fn detect_merge_mode(template_xml: &str) -> MergeMode {
46 if template_xml.contains("matchTemplate") {
48 return MergeMode::MatchTemplate;
49 }
50 MergeMode::ConsumeData
51}
52
53use crate::error::{Result, XfaError};
54use roxmltree::Node;
55use xfa_dom_resolver::data_dom::{DataDom, DataNodeId};
56use xfa_dom_resolver::som::resolve_data_path;
57use xfa_layout_engine::form::{
58 Access, AnchorType, ContentArea, DrawContent, EventScript, FieldKind, FormNode, FormNodeId,
59 FormNodeMeta, FormNodeStyle, FormNodeType, FormTree, GroupKind, Occur, Presence, RichTextSpan,
60 ScriptLanguage,
61};
62use xfa_layout_engine::text::{FontFamily, FontMetrics};
63use xfa_layout_engine::types::{
64 BoxModel, Caption, CaptionPlacement, Insets, LayoutStrategy, Measurement, TextAlign,
65 VerticalAlign,
66};
67
68pub struct FormMerger<'a> {
70 data_dom: &'a DataDom,
71 form_tree: FormTree,
72 image_files: std::collections::HashMap<String, Vec<u8>>,
76}
77
78fn area_layout(elem: Node<'_, '_>) -> LayoutStrategy {
79 if elem.tag_name().name() == "area" && attr(elem, "layout").is_none() {
80 let has_positioned_child = elem.children().filter(|n| n.is_element()).any(|child| {
81 attr(child, "x")
82 .and_then(parse_dim)
83 .is_some_and(|v| v > 0.0)
84 || attr(child, "y")
85 .and_then(parse_dim)
86 .is_some_and(|v| v > 0.0)
87 });
88 if has_positioned_child {
89 LayoutStrategy::Positioned
90 } else {
91 LayoutStrategy::TopToBottom
92 }
93 } else {
94 parse_layout_attr(elem)
95 }
96}
97
98impl<'a> FormMerger<'a> {
99 pub fn new(data_dom: &'a DataDom) -> Self {
101 Self {
102 data_dom,
103 form_tree: FormTree::new(),
104 image_files: std::collections::HashMap::new(),
105 }
106 }
107
108 pub fn with_image_files(mut self, files: std::collections::HashMap<String, Vec<u8>>) -> Self {
110 self.image_files = files;
111 self
112 }
113
114 pub fn merge(mut self, template_xml: &str) -> Result<(FormTree, FormNodeId)> {
122 let mode = detect_merge_mode(template_xml);
123 log::debug!(
124 "XFA merge: mode={:?}, {} data nodes",
125 mode,
126 self.data_dom.len()
127 );
128 let doc = roxmltree::Document::parse(template_xml)
129 .map_err(|e| XfaError::ParseFailed(format!("template XML parse error: {e}")))?;
130 let root_elem = doc.root_element();
131
132 let template_elem = if root_elem.tag_name().name() == "template" {
135 root_elem
136 } else {
137 find_first_child_by_name(root_elem, "template").ok_or_else(|| {
139 XfaError::PacketNotFound("no <template> element found".to_string())
140 })?
141 };
142
143 if mode == MergeMode::MatchTemplate {
144 self.apply_match_template_bindings(template_elem);
147 }
148
149 self.collect_variables_scripts(template_elem);
155 let (root_id, _trailing) = self.parse_node(template_elem, None, true)?;
156
157 Ok((self.form_tree, root_id))
158 }
159
160 fn collect_variables_scripts(&mut self, template_elem: Node<'_, '_>) {
166 let Some(root_subform) = find_first_child_by_name(template_elem, "subform") else {
167 return;
168 };
169 self.collect_variables_scripts_recursive(root_subform, None);
170 }
171
172 fn collect_variables_scripts_recursive(
173 &mut self,
174 subform: Node<'_, '_>,
175 subform_scope: Option<String>,
176 ) {
177 for child in subform.children().filter(|n| n.is_element()) {
178 match child.tag_name().name() {
179 "variables" => {
180 for var_child in child.children().filter(|n| n.is_element()) {
181 match var_child.tag_name().name() {
182 "script" => {
183 let Some(name) = attr(var_child, "name") else {
184 continue;
185 };
186 let body: String = var_child
187 .children()
188 .filter(|n| n.is_text())
189 .filter_map(|n| n.text())
190 .collect::<String>();
191 if body.trim().is_empty() {
192 continue;
193 }
194 self.form_tree.variables_scripts.push((
195 subform_scope.clone(),
196 name.to_string(),
197 body,
198 ));
199 }
200 "text" => {
216 let Some(name) = attr(var_child, "name") else {
217 continue;
218 };
219 if name.is_empty() {
220 continue;
221 }
222 let initial: String = var_child
223 .children()
224 .filter(|n| n.is_text())
225 .filter_map(|n| n.text())
226 .collect::<String>();
227 self.form_tree.variables_data_items.push((
228 subform_scope.clone(),
229 name.to_string(),
230 initial,
231 ));
232 }
233 _ => {}
234 }
235 }
236 }
237 "subform" | "area" | "exclGroup" => {
238 let child_scope = attr(child, "name")
239 .filter(|n| !n.is_empty())
240 .map(|n| n.to_string());
241 self.collect_variables_scripts_recursive(child, child_scope);
242 }
243 _ => {}
244 }
245 }
246 }
247
248 fn apply_match_template_bindings(&mut self, _template_elem: roxmltree::Node<'_, '_>) {
263 }
270
271 fn parse_node(
272 &mut self,
273 elem: Node<'_, '_>,
274 data_context: Option<DataNodeId>,
275 is_root: bool,
276 ) -> Result<(FormNodeId, (bool, Option<String>))> {
277 let tag = elem.tag_name().name();
278
279 let (node, trailing_info) = match tag {
280 "template" => {
281 let mut n = self.blank_node("root", FormNodeType::Root);
282 n.layout = LayoutStrategy::TopToBottom;
283 let ti = self.add_children(&mut n, elem, data_context)?;
284 (n, ti)
285 }
286 "subform" | "exclGroup" | "area" => {
287 let name = attr(elem, "name").unwrap_or("").to_string();
288 let layout = area_layout(elem);
289 let bm = parse_box_model(elem);
290 let occur = parse_occur(elem);
291 self.build_subform_instance(elem, data_context, is_root, occur, name, layout, bm)?
292 }
293 "field" => (self.parse_field(elem, data_context)?, (false, None)),
294 "draw" => (self.parse_draw(elem, data_context)?, (false, None)),
295 "pageSet" => (self.parse_page_set(elem, data_context)?, (false, None)),
296 "pageArea" => (self.parse_page_area(elem, data_context)?, (false, None)),
297 _ => {
298 let mut n = self.blank_node(tag, FormNodeType::Subform);
299 let ti = self.add_children(&mut n, elem, data_context)?;
300 (n, ti)
301 }
302 };
303
304 if tag == "exclGroup" {
305 self.apply_exclusive_choice_value(elem, data_context, &node.children);
310 }
311
312 let mut meta = parse_node_meta(elem);
313
314 if let Some(bound_presence) = self.lookup_value_by_name("presence", data_context) {
318 meta.presence = parse_presence_str(&bound_presence);
319 }
320
321 let is_draw_or_field = tag == "draw" || tag == "field";
322 if is_draw_or_field {
323 if tag == "field" {
333 meta.style.inset_top_pt = Some(node.box_model.margins.top);
334 meta.style.inset_bottom_pt = Some(node.box_model.margins.bottom);
335 meta.style.inset_left_pt = Some(node.box_model.margins.left);
336 meta.style.inset_right_pt = Some(node.box_model.margins.right);
337 }
338 if meta.style.font_weight.is_none() {
339 if let Some(weight) = extract_exdata_font_weight(elem) {
340 meta.style.font_weight = Some(weight);
341 }
342 }
343 if meta.style.font_style.is_none() {
344 if let Some(style) = extract_exdata_font_style(elem) {
345 meta.style.font_style = Some(style);
346 }
347 }
348 if meta.style.font_family.is_none() {
349 if let Some(family) = extract_exdata_font_family(elem) {
350 meta.style.font_family = Some(family);
351 }
352 }
353 if meta.style.text_color.is_none() {
354 if let Some(color) = extract_exdata_color(elem) {
355 meta.style.text_color = Some(color);
356 }
357 }
358 if meta.style.space_above_pt.is_none() || meta.style.space_below_pt.is_none() {
359 if let Some((above, below)) = extract_exdata_margins(elem) {
360 if meta.style.space_above_pt.is_none() {
361 meta.style.space_above_pt = Some(above);
362 }
363 if meta.style.space_below_pt.is_none() {
364 meta.style.space_below_pt = Some(below);
365 }
366 }
367 }
368 if meta.style.rich_text_spans.is_none() {
369 meta.style.rich_text_spans = parse_exdata_rich_text_spans(elem);
370 }
371 }
372 meta.bound_data_node = data_context.map(|c| c.as_raw());
375 let id = self.form_tree.add_node_with_meta(node, meta);
376 Ok((id, trailing_info))
377 }
378
379 #[allow(clippy::too_many_arguments)]
380 fn build_subform_instance(
381 &mut self,
382 elem: Node<'_, '_>,
383 data_context: Option<DataNodeId>,
384 is_root: bool,
385 occur: Occur,
386 name: String,
387 layout: LayoutStrategy,
388 mut bm: BoxModel,
389 ) -> Result<(FormNode, (bool, Option<String>))> {
390 if layout == LayoutStrategy::TopToBottom && bm.width.is_none() && is_root {
391 bm.width = Some(612.0);
392 }
393
394 if name.is_empty() {
399 let mut n = FormNode {
401 name,
402 node_type: FormNodeType::Subform,
403 box_model: bm,
404 layout,
405 children: Vec::new(),
406 occur,
407 font: FontMetrics::default(),
408 calculate: None,
409 validate: None,
410 column_widths: Vec::new(),
411 col_span: 1,
412 };
413 let ti = self.add_children(&mut n, elem, data_context)?;
414 return Ok((n, ti));
415 }
416
417 let mut child_context = data_context;
423 if let Some(ctx) = data_context {
424 let matches = self.data_dom.children_by_name(ctx, &name);
426 if let Some(&first) = matches.first() {
427 child_context = Some(first);
428 } else {
429 child_context = self.resolve_with_scope_group(ctx, &name).or(data_context);
431 }
432 } else if let Some(root) = self.data_dom.root() {
433 if self.data_dom.get(root).is_some_and(|n| n.name() == name) {
434 child_context = Some(root);
436 } else {
437 let matches = self.data_dom.children_by_name(root, &name);
438 if let Some(&first) = matches.first() {
439 child_context = Some(first);
441 } else {
442 child_context = Some(root);
451 }
452 }
453 }
454
455 let mut n = FormNode {
456 name,
457 node_type: FormNodeType::Subform,
458 box_model: bm,
459 layout,
460 children: Vec::new(),
461 occur,
462 font: FontMetrics::default(),
463 calculate: None,
464 validate: None,
465 column_widths: Vec::new(),
466 col_span: 1,
467 };
468 let ti = self.add_children(&mut n, elem, child_context)?;
469 Ok((n, ti))
470 }
471
472 #[allow(clippy::type_complexity)]
483 fn expand_repeating_subform_instances(
484 &mut self,
485 element: Node<'_, '_>,
486 data_context: Option<DataNodeId>,
487 is_root: bool,
488 ) -> Result<Vec<(FormNodeId, (bool, Option<String>))>> {
489 let name = attr(element, "name").unwrap_or("").to_string();
490 let occur = parse_occur(element);
491
492 let (_bind_ref_unused, bind_none) = parse_bind(element);
496 if bind_none {
497 if std::env::var_os("XFA_IE_TRACE").is_some() {
500 eprintln!("XFA_IE_TRACE expand name={name:?} bind=none data_count=0 -> count=1 (script-instanceManager-only)");
501 }
502 return Ok(vec![self.parse_node(element, data_context, is_root)?]);
503 }
504
505 let bind_ref = find_first_child_by_name(element, "bind")
509 .and_then(|b| attr(b, "ref"))
510 .map(|s| s.to_string());
511
512 let data_instances = if let Some(ref raw_ref) = bind_ref {
513 let normalized = if let Some(rest) = raw_ref.strip_prefix("$record") {
515 format!("${}", rest)
516 } else {
517 raw_ref.clone()
518 };
519 let with_wildcard = if normalized.ends_with("[*]") {
521 normalized
522 } else {
523 format!("{}[*]", normalized)
524 };
525 resolve_data_path(self.data_dom, &with_wildcard, data_context)
527 .or_else(|_| resolve_data_path(self.data_dom, &with_wildcard, self.data_dom.root()))
528 .unwrap_or_default()
529 } else {
530 if let Some(ctx) = data_context {
532 self.data_dom.children_by_name(ctx, &name)
533 } else if let Some(root) = self.data_dom.root() {
534 self.data_dom.children_by_name(root, &name)
535 } else {
536 Vec::new()
537 }
538 };
539
540 let data_count = data_instances.len() as u32;
541 let min = occur.min;
542 let max = occur.max.unwrap_or(data_count).max(min);
543 let count = if data_count == 0 {
557 occur.initial.min(max)
558 } else {
559 data_count.clamp(min, max)
560 };
561
562 if std::env::var_os("XFA_IE_TRACE").is_some() {
565 let bind_kind = if bind_ref.is_some() { "ref" } else { "byname" };
566 eprintln!(
567 "XFA_IE_TRACE expand name={name:?} bind={bind_kind} data_count={data_count} occur(min={},max={:?},initial={}) -> count={count}",
568 min, occur.max, occur.initial
569 );
570 }
571
572 let layout = area_layout(element);
573 let bm = parse_box_model(element);
574
575 if count == 0 && occur.max.unwrap_or(u32::MAX) > 0 {
576 let (inst_node, trailing) = self.build_subform_instance(
580 element,
581 None,
582 is_root,
583 Occur::once(),
584 name.clone(),
585 layout,
586 bm.clone(),
587 )?;
588 let mut meta = parse_node_meta(element);
589 meta.is_zero_instance_prototype = true;
590 meta.presence = Presence::Hidden;
591 let inst_id = self.form_tree.add_node_with_meta(inst_node, meta);
592 return Ok(vec![(inst_id, trailing)]);
593 }
594
595 let mut instances = Vec::with_capacity(count as usize);
596
597 for i in 0..count {
598 let instance_data_ctx = data_instances.get(i as usize).copied();
599 let (inst_node, trailing) = self.build_subform_instance(
600 element,
601 instance_data_ctx,
602 is_root,
603 Occur::once(),
604 name.clone(),
605 layout,
606 bm.clone(),
607 )?;
608 if element.tag_name().name() == "exclGroup" {
609 self.apply_exclusive_choice_value(element, instance_data_ctx, &inst_node.children);
610 }
611 let mut meta = parse_node_meta(element);
612 meta.bound_data_node = instance_data_ctx.map(|c| c.as_raw());
614 let inst_id = self.form_tree.add_node_with_meta(inst_node, meta);
615 instances.push((inst_id, trailing));
616 }
617
618 Ok(instances)
619 }
620
621 fn find_value_in_descendants(&self, node: DataNodeId, name: &str) -> Option<String> {
627 for &child in self.data_dom.children(node) {
628 if let Some(cn) = self.data_dom.get(child) {
629 if cn.name() == name && cn.is_value() {
630 return self.data_dom.value(child).ok().map(|s| s.to_string());
631 }
632 }
633 }
634 for &child in self.data_dom.children(node) {
636 if self.data_dom.get(child).is_some_and(|n| n.is_group()) {
637 if let Some(val) = self.find_value_in_descendants(child, name) {
638 return Some(val);
639 }
640 }
641 }
642 None
643 }
644
645 fn resolve_with_scope_value(&self, data_context: DataNodeId, name: &str) -> Option<String> {
653 let direct = self.data_dom.children_by_name(data_context, name);
655 if let Some(&val_id) = direct.first() {
656 if self.data_dom.get(val_id).is_some_and(|n| n.is_value()) {
657 return self.data_dom.value(val_id).ok().map(|s| s.to_string());
658 }
659 }
660
661 if let Some(parent) = self.data_dom.get(data_context).and_then(|n| n.parent()) {
663 let sibling_matches = self.data_dom.children_by_name(parent, name);
664 if let Some(&val_id) = sibling_matches.first() {
665 if self.data_dom.get(val_id).is_some_and(|n| n.is_value()) {
666 return self.data_dom.value(val_id).ok().map(|s| s.to_string());
667 }
668 }
669
670 let mut ancestor = self.data_dom.get(parent).and_then(|n| n.parent());
672 while let Some(anc_id) = ancestor {
673 let anc_matches = self.data_dom.children_by_name(anc_id, name);
674 if let Some(&val_id) = anc_matches.first() {
675 if self.data_dom.get(val_id).is_some_and(|n| n.is_value()) {
676 return self.data_dom.value(val_id).ok().map(|s| s.to_string());
677 }
678 }
679 ancestor = self.data_dom.get(anc_id).and_then(|n| n.parent());
680 }
681 }
682
683 self.data_dom
685 .root()
686 .and_then(|root| self.find_value_in_descendants(root, name))
687 }
688
689 fn resolve_with_scope_group(&self, data_context: DataNodeId, name: &str) -> Option<DataNodeId> {
694 let mut cursor = self.data_dom.get(data_context).and_then(|n| n.parent());
696 while let Some(anc_id) = cursor {
697 let matches = self.data_dom.children_by_name(anc_id, name);
698 if let Some(&grp_id) = matches.first() {
699 if self.data_dom.get(grp_id).is_some_and(|n| n.is_group()) {
700 return Some(grp_id);
701 }
702 }
703 cursor = self.data_dom.get(anc_id).and_then(|n| n.parent());
704 }
705 None
706 }
707
708 fn lookup_value_by_name(&self, name: &str, data_context: Option<DataNodeId>) -> Option<String> {
709 if name.is_empty() {
710 return None;
711 }
712
713 if let Some(ctx) = data_context {
714 return self.resolve_with_scope_value(ctx, name);
716 }
717
718 self.data_dom
719 .root()
720 .and_then(|root| self.find_value_in_descendants(root, name))
721 }
722
723 fn apply_exclusive_choice_value(
724 &mut self,
725 elem: Node<'_, '_>,
726 data_context: Option<DataNodeId>,
727 child_ids: &[FormNodeId],
728 ) {
729 let Some(group_name) = attr(elem, "name") else {
730 return;
731 };
732 let Some(group_value) = self.lookup_value_by_name(group_name, data_context) else {
733 return;
734 };
735
736 for &child_id in child_ids {
737 let item_value = self.form_tree.meta(child_id).item_value.clone();
738 if let FormNodeType::Field { value } = &mut self.form_tree.get_mut(child_id).node_type {
739 *value = if item_value.as_deref() == Some(group_value.as_str()) {
743 group_value.clone()
744 } else {
745 String::new()
746 };
747 }
748 }
749 }
750
751 fn parse_field(
754 &mut self,
755 elem: Node<'_, '_>,
756 data_context: Option<DataNodeId>,
757 ) -> Result<FormNode> {
758 let name = attr(elem, "name").unwrap_or("").to_string();
759 let bm = parse_box_model(elem);
760
761 let mut value = extract_value_text(elem).unwrap_or_default();
762
763 if let Some(bound_value) = self.lookup_value_by_name(&name, data_context) {
772 value = bound_value;
773 }
774
775 let mut bm_with_caption = bm.clone();
776 if !is_hidden(elem) {
777 if let Some(cap) = parse_caption(elem) {
778 bm_with_caption.caption = Some(cap);
779 }
780 }
781
782 let mut font = parse_font_metrics(elem);
783 if let Some(html_size) = extract_exdata_font_size(elem) {
784 font.size = html_size;
785 }
786 if let Some(css_align) = extract_exdata_text_align(elem) {
787 font.text_align = css_align;
788 }
789
790 Ok(FormNode {
791 name,
792 node_type: FormNodeType::Field { value },
793 box_model: bm_with_caption,
794 layout: LayoutStrategy::Positioned,
795 children: Vec::new(),
796 occur: Occur::once(),
797 font,
798 calculate: None,
799 validate: None,
800 column_widths: Vec::new(),
801 col_span: parse_col_span(elem),
802 })
803 }
804
805 fn parse_draw(
809 &mut self,
810 elem: Node<'_, '_>,
811 _data_context: Option<DataNodeId>,
812 ) -> Result<FormNode> {
813 let name = attr(elem, "name").unwrap_or("").to_string();
814 let bm = parse_box_model(elem);
815
816 if let Some(draw_content) = extract_draw_content(elem) {
817 return Ok(FormNode {
818 name,
819 node_type: FormNodeType::Draw(draw_content),
820 box_model: bm,
821 layout: LayoutStrategy::Positioned,
822 children: Vec::new(),
823 occur: Occur::once(),
824 font: FontMetrics::default(),
825 calculate: None,
826 validate: None,
827 column_widths: Vec::new(),
828 col_span: 1,
829 });
830 }
831
832 if let Some((image_data, mime_type)) = extract_value_image(elem, &self.image_files) {
833 return Ok(FormNode {
834 name,
835 node_type: FormNodeType::Image {
836 data: image_data,
837 mime_type,
838 },
839 box_model: bm,
840 layout: LayoutStrategy::Positioned,
841 children: Vec::new(),
842 occur: Occur::once(),
843 font: FontMetrics::default(),
844 calculate: None,
845 validate: None,
846 column_widths: Vec::new(),
847 col_span: 1,
848 });
849 }
850
851 let mut font = parse_font_metrics(elem);
852 if let Some(html_size) = extract_exdata_font_size(elem) {
853 font.size = html_size;
854 }
855 if let Some(css_align) = extract_exdata_text_align(elem) {
856 font.text_align = css_align;
857 }
858
859 let content = extract_value_text(elem).unwrap_or_default();
860
861 Ok(FormNode {
862 name,
863 node_type: FormNodeType::Draw(DrawContent::Text(content)),
864 box_model: bm,
865 layout: LayoutStrategy::Positioned,
866 children: Vec::new(),
867 occur: Occur::once(),
868 font,
869 calculate: None,
870 validate: None,
871 column_widths: Vec::new(),
872 col_span: 1,
873 })
874 }
875
876 fn parse_page_set(
877 &mut self,
878 elem: Node<'_, '_>,
879 data_context: Option<DataNodeId>,
880 ) -> Result<FormNode> {
881 let name = attr(elem, "name").unwrap_or("pageSet").to_string();
882 let mut node = FormNode {
883 name,
884 node_type: FormNodeType::PageSet,
885 box_model: BoxModel {
886 max_width: f64::MAX,
887 max_height: f64::MAX,
888 ..Default::default()
889 },
890 layout: LayoutStrategy::TopToBottom,
891 children: Vec::new(),
892 occur: Occur::once(),
893 font: FontMetrics::default(),
894 calculate: None,
895 validate: None,
896 column_widths: Vec::new(),
897 col_span: 1,
898 };
899 for child in elem.children().filter(|n| n.is_element()) {
900 if child.tag_name().name() == "pageArea" {
901 let (child_id, _) = self.parse_node(child, data_context, false)?;
902 node.children.push(child_id);
903 }
904 }
905 Ok(node)
906 }
907
908 fn parse_page_area(
909 &mut self,
910 elem: Node<'_, '_>,
911 data_context: Option<DataNodeId>,
912 ) -> Result<FormNode> {
913 let name = attr(elem, "name").unwrap_or("").to_string();
914 let (page_w, page_h) = read_medium(elem);
915 let content_areas = read_content_areas(elem, page_w, page_h);
916
917 let bm = BoxModel {
918 width: Some(page_w),
919 height: Some(page_h),
920 ..Default::default()
921 };
922
923 let mut node = FormNode {
924 name,
925 node_type: FormNodeType::PageArea { content_areas },
926 box_model: bm,
927 layout: LayoutStrategy::Positioned,
928 children: Vec::new(),
929 occur: Occur::once(),
930 font: FontMetrics::default(),
931 calculate: None,
932 validate: None,
933 column_widths: Vec::new(),
934 col_span: 1,
935 };
936 self.add_children(&mut node, elem, data_context)?;
937 Ok(node)
938 }
939
940 fn add_children(
956 &mut self,
957 node: &mut FormNode,
958 elem: Node<'_, '_>,
959 data_context: Option<DataNodeId>,
960 ) -> Result<(bool, Option<String>)> {
961 let mut pending_break = false;
962 let mut pending_break_target = None;
963 let mut pending_ca_break = false;
964
965 for child in elem.children().filter(|n| n.is_element()) {
966 let tag = child.tag_name().name();
967 match tag {
968 "subform" | "field" | "draw" | "pageSet" | "pageArea" | "exclGroup" | "area" => {
969 let child_entries = if matches!(tag, "subform" | "exclGroup" | "area") {
970 let name = attr(child, "name").unwrap_or("");
971 let occur = parse_occur(child);
972 let (_, bind_none) = parse_bind(child);
973 if occur.is_repeating() && !name.is_empty() && !bind_none {
974 self.expand_repeating_subform_instances(child, data_context, false)?
975 } else {
976 vec![self.parse_node(child, data_context, false)?]
977 }
978 } else {
979 vec![self.parse_node(child, data_context, false)?]
980 };
981
982 for (child_id, (trailing_break, trailing_target)) in child_entries {
983 if pending_break {
984 let meta = self.form_tree.meta_mut(child_id);
985 meta.page_break_before = true;
986 if meta.break_target.is_none() {
987 meta.break_target = pending_break_target.take();
988 }
989 pending_break = false;
990 }
991 if pending_ca_break {
992 self.form_tree.meta_mut(child_id).content_area_break = true;
993 pending_ca_break = false;
994 }
995 node.children.push(child_id);
996 if trailing_break {
997 pending_break = true;
998 pending_break_target = trailing_target;
999 }
1000 }
1001 }
1002 "breakBefore" => {
1003 let target_type = attr(child, "targetType");
1004 if target_type == Some("pageArea") {
1005 pending_break = true;
1006 pending_break_target = attr(child, "target").map(|s| s.to_string());
1007 } else if target_type == Some("contentArea") {
1008 pending_ca_break = true;
1009 }
1010 }
1011 "break"
1012 if attr(child, "before") == Some("pageArea")
1013 && attr(child, "targetType") == Some("pageArea") =>
1014 {
1015 pending_break = true;
1016 pending_break_target = attr(child, "target").map(|s| s.to_string());
1017 }
1018 _ => {}
1019 }
1020 }
1021 Ok((pending_break, pending_break_target))
1022 }
1023
1024 fn blank_node(&self, name: &str, node_type: FormNodeType) -> FormNode {
1025 FormNode {
1026 name: name.to_string(),
1027 node_type,
1028 box_model: BoxModel {
1029 max_width: f64::MAX,
1030 max_height: f64::MAX,
1031 ..Default::default()
1032 },
1033 layout: LayoutStrategy::TopToBottom,
1034 children: Vec::new(),
1035 occur: Occur::once(),
1036 font: FontMetrics::default(),
1037 calculate: None,
1038 validate: None,
1039 column_widths: Vec::new(),
1040 col_span: 1,
1041 }
1042 }
1043}
1044
1045fn attr<'a>(elem: Node<'a, '_>, name: &str) -> Option<&'a str> {
1050 elem.attributes()
1051 .find(|a| a.name() == name)
1052 .map(|a| a.value())
1053}
1054
1055fn find_first_child_by_name<'a, 'input>(
1056 elem: Node<'a, 'input>,
1057 name: &str,
1058) -> Option<Node<'a, 'input>> {
1059 elem.children()
1060 .filter(|n| n.is_element())
1061 .find(|n| n.tag_name().name() == name)
1062}
1063
1064fn parse_layout_attr(elem: Node<'_, '_>) -> LayoutStrategy {
1065 match attr(elem, "layout").unwrap_or("") {
1066 "tb" => LayoutStrategy::TopToBottom,
1067 "lr-tb" => LayoutStrategy::LeftToRightTB,
1068 "rl-tb" => LayoutStrategy::RightToLeftTB,
1069 "table" => LayoutStrategy::Table,
1070 "row" => LayoutStrategy::Row,
1071 "paginate" => LayoutStrategy::TopToBottom,
1072 "position" => LayoutStrategy::Positioned,
1073 _ => LayoutStrategy::Positioned,
1074 }
1075}
1076
1077fn parse_anchor_type(elem: Node<'_, '_>) -> AnchorType {
1078 match attr(elem, "anchorType").unwrap_or("") {
1079 "topCenter" => AnchorType::TopCenter,
1080 "topRight" => AnchorType::TopRight,
1081 "middleLeft" => AnchorType::MiddleLeft,
1082 "middleCenter" => AnchorType::MiddleCenter,
1083 "middleRight" => AnchorType::MiddleRight,
1084 "bottomLeft" => AnchorType::BottomLeft,
1085 "bottomCenter" => AnchorType::BottomCenter,
1086 "bottomRight" => AnchorType::BottomRight,
1087 _ => AnchorType::TopLeft,
1088 }
1089}
1090
1091fn parse_box_model(elem: Node<'_, '_>) -> BoxModel {
1092 let w = attr(elem, "w").and_then(parse_dim);
1093 let h = attr(elem, "h").and_then(parse_dim);
1094 let x = attr(elem, "x").and_then(parse_dim).unwrap_or(0.0);
1095 let y = attr(elem, "y").and_then(parse_dim).unwrap_or(0.0);
1096 let min_h = attr(elem, "minH").and_then(parse_dim).unwrap_or(0.0);
1097 let min_w = attr(elem, "minW").and_then(parse_dim).unwrap_or(0.0);
1098 let max_h = attr(elem, "maxH").and_then(parse_dim).unwrap_or(f64::MAX);
1099 let max_w = attr(elem, "maxW").and_then(parse_dim).unwrap_or(f64::MAX);
1100 let margins = parse_margin(elem);
1101
1102 BoxModel {
1103 width: w,
1104 height: h,
1105 x,
1106 y,
1107 margins,
1108 min_width: min_w,
1109 max_width: max_w,
1110 min_height: min_h,
1111 max_height: max_h,
1112 ..Default::default()
1113 }
1114}
1115
1116fn parse_margin(elem: Node<'_, '_>) -> Insets {
1117 if let Some(margin) = find_first_child_by_name(elem, "margin") {
1118 Insets {
1119 top: attr(margin, "topInset").and_then(parse_dim).unwrap_or(0.0),
1120 bottom: attr(margin, "bottomInset")
1121 .and_then(parse_dim)
1122 .unwrap_or(0.0),
1123 left: attr(margin, "leftInset").and_then(parse_dim).unwrap_or(0.0),
1124 right: attr(margin, "rightInset")
1125 .and_then(parse_dim)
1126 .unwrap_or(0.0),
1127 }
1128 } else {
1129 Insets::default()
1130 }
1131}
1132
1133fn parse_dim(s: &str) -> Option<f64> {
1134 if s.trim().parse::<f64>().is_ok() {
1135 return Measurement::parse(&format!("{s}in")).map(|m: Measurement| m.to_points());
1136 }
1137 Measurement::parse(s).map(|m: Measurement| m.to_points())
1138}
1139
1140fn parse_percentage(s: &str) -> Option<f64> {
1142 let s = s.trim();
1143 let num_str = s.strip_suffix('%')?;
1144 let v: f64 = num_str.trim().parse().ok()?;
1145 Some(v / 100.0)
1146}
1147
1148fn parse_letter_spacing(s: &str, font_size_pt: f64) -> Option<f64> {
1150 let s = s.trim();
1151 if s == "0" {
1152 return Some(0.0);
1153 }
1154 if let Some(num_str) = s.strip_suffix("em") {
1155 let v: f64 = num_str.trim().parse().ok()?;
1156 return Some(v * font_size_pt);
1157 }
1158 Measurement::parse(s).map(|m| m.to_points())
1159}
1160
1161fn parse_occur(elem: Node<'_, '_>) -> Occur {
1171 if let Some(occur) = find_first_child_by_name(elem, "occur") {
1172 let min: u32 = attr(occur, "min").and_then(|s| s.parse().ok()).unwrap_or(1);
1173 let max: Option<u32> = match attr(occur, "max") {
1180 Some("-1") => None, Some(s) => Some(s.parse::<u32>().unwrap_or(min)),
1182 None => Some(min),
1183 };
1184 let initial: u32 = attr(occur, "initial")
1187 .and_then(|s| s.parse().ok())
1188 .unwrap_or(min);
1189 Occur::repeating(min, max, initial)
1190 } else {
1191 Occur::once()
1192 }
1193}
1194
1195fn parse_font_size(s: &str) -> Option<f64> {
1196 if let Ok(v) = s.trim().parse::<f64>() {
1197 return if v > 0.0 { Some(v) } else { None };
1198 }
1199 Measurement::parse(s).map(|m: Measurement| m.to_points())
1200}
1201
1202fn parse_font_metrics(elem: Node<'_, '_>) -> FontMetrics {
1203 let font_elem = find_first_child_by_name(elem, "font");
1204 let size = font_elem
1205 .and_then(|f| attr(f, "size"))
1206 .and_then(parse_font_size)
1207 .unwrap_or(FontMetrics::default().size);
1208 let typeface = font_elem
1209 .and_then(|f| attr(f, "typeface"))
1210 .map(FontFamily::from_typeface)
1211 .unwrap_or_default();
1212 let text_align = find_first_child_by_name(elem, "para")
1213 .and_then(|p| attr(p, "hAlign"))
1214 .map(|a| match a {
1215 "center" => TextAlign::Center,
1216 "right" => TextAlign::Right,
1217 "justify" => TextAlign::Justify,
1218 _ => TextAlign::Left,
1219 })
1220 .unwrap_or_default();
1221 FontMetrics {
1222 size,
1223 text_align,
1224 typeface,
1225 ..FontMetrics::default()
1226 }
1227}
1228
1229fn read_medium(page_area: Node<'_, '_>) -> (f64, f64) {
1230 if let Some(m) = find_first_child_by_name(page_area, "medium") {
1231 let short = attr(m, "short").and_then(parse_dim).unwrap_or(612.0);
1232 let long_ = attr(m, "long").and_then(parse_dim).unwrap_or(792.0);
1233 (short, long_)
1234 } else {
1235 (612.0, 792.0)
1236 }
1237}
1238
1239fn read_content_areas(
1240 page_area: Node<'_, '_>,
1241 page_width: f64,
1242 page_height: f64,
1243) -> Vec<ContentArea> {
1244 let mut areas = Vec::new();
1245 for child in page_area.children().filter(|n| n.is_element()) {
1246 if child.tag_name().name() == "contentArea" {
1247 let x = attr(child, "x").and_then(parse_dim).unwrap_or(0.0);
1251 let y = attr(child, "y").and_then(parse_dim).unwrap_or(0.0);
1252 let w = attr(child, "w").and_then(parse_dim).unwrap_or(page_width);
1257 let h = attr(child, "h").and_then(parse_dim).unwrap_or(page_height);
1258 areas.push(ContentArea {
1259 name: attr(child, "name").unwrap_or("").to_string(),
1260 x,
1261 y,
1262 width: w,
1263 height: h,
1264 leader: None,
1265 trailer: None,
1266 });
1267 }
1268 }
1269 if areas.is_empty() {
1270 areas.push(ContentArea {
1271 name: String::new(),
1272 x: 0.0,
1273 y: 0.0,
1274 width: page_width,
1275 height: page_height,
1276 leader: None,
1277 trailer: None,
1278 });
1279 }
1280 areas
1281}
1282
1283fn extract_value_text(elem: Node<'_, '_>) -> Option<String> {
1284 let value = find_first_child_by_name(elem, "value")?;
1285 for tag in &["text", "float", "integer", "date", "dateTime", "decimal"] {
1286 if let Some(child) = find_first_child_by_name(value, tag) {
1287 let text = child.text().unwrap_or("");
1288 let trimmed = text.trim_start_matches(|c: char| c.is_whitespace() && c != '\n');
1289 let trimmed = trimmed.trim_end_matches(|c: char| c.is_whitespace() && c != '\n');
1290 if !trimmed.is_empty() {
1291 return Some(trimmed.to_string());
1292 }
1293 }
1294 }
1295 if let Some(ex) = find_first_child_by_name(value, "exData") {
1296 let text = extract_text_from_descendants(ex);
1297 if !text.is_empty() {
1298 return Some(text);
1299 }
1300 }
1301 None
1302}
1303
1304fn extract_text_from_descendants(node: Node<'_, '_>) -> String {
1305 let block_tags = [
1306 "p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "li", "tr", "br",
1307 ];
1308 let mut result = String::new();
1309 let mut last_was_block = false;
1310
1311 for desc in node.descendants() {
1312 let is_block = desc.is_element() && block_tags.contains(&desc.tag_name().name());
1313
1314 if is_block && !result.is_empty() {
1315 result.push('\n');
1316 last_was_block = true;
1317 }
1318
1319 if desc.is_text() {
1320 if let Some(t) = desc.text() {
1321 let t = t.trim();
1322 if !t.is_empty() {
1323 if last_was_block || result.is_empty() {
1324 result.push_str(t);
1325 } else {
1326 result.push(' ');
1327 result.push_str(t);
1328 }
1329 last_was_block = false;
1330 }
1331 }
1332 }
1333 }
1334
1335 result.trim().to_string()
1336}
1337
1338fn extract_exdata_font_size(elem: Node<'_, '_>) -> Option<f64> {
1339 let value = find_first_child_by_name(elem, "value")?;
1340 let ex = find_first_child_by_name(value, "exData")?;
1341 for desc in ex.descendants() {
1342 if !desc.is_element() {
1343 continue;
1344 }
1345 let style = desc
1346 .attribute("style")
1347 .or_else(|| desc.attribute("Style"))?;
1348 for part in style.split(';') {
1349 let part = part.trim();
1350 if let Some(val) = part
1351 .strip_prefix("font-size:")
1352 .or_else(|| part.strip_prefix("font-size :"))
1353 {
1354 let val = val.trim();
1355 if let Some(pt) = val.strip_suffix("pt") {
1356 if let Ok(size) = pt.trim().parse::<f64>() {
1357 if size > 0.0 {
1358 return Some(size);
1359 }
1360 }
1361 }
1362 }
1363 }
1364 }
1365 None
1366}
1367
1368fn extract_exdata_font_weight(elem: Node<'_, '_>) -> Option<String> {
1372 let value = find_first_child_by_name(elem, "value")?;
1373 let ex = find_first_child_by_name(value, "exData")?;
1374 for desc in ex.descendants() {
1375 if !desc.is_element() {
1376 continue;
1377 }
1378 let style = desc.attribute("style")?;
1379 for part in style.split(';') {
1380 let part = part.trim();
1381 if let Some(val) = part
1382 .strip_prefix("font-weight:")
1383 .or_else(|| part.strip_prefix("font-weight :"))
1384 {
1385 let val = val.trim();
1386 if val == "bold" || val == "700" || val == "800" || val == "900" {
1387 return Some("bold".to_string());
1388 }
1389 if val == "normal" || val == "400" || val == "500" || val == "600" {
1390 return Some("normal".to_string());
1391 }
1392 }
1393 }
1394 }
1395 None
1396}
1397
1398fn extract_exdata_font_style(elem: Node<'_, '_>) -> Option<String> {
1401 let value = find_first_child_by_name(elem, "value")?;
1402 let ex = find_first_child_by_name(value, "exData")?;
1403 for desc in ex.descendants() {
1404 if !desc.is_element() {
1405 continue;
1406 }
1407 let style = desc.attribute("style")?;
1408 for part in style.split(';') {
1409 let part = part.trim();
1410 if let Some(val) = part
1411 .strip_prefix("font-style:")
1412 .or_else(|| part.strip_prefix("font-style :"))
1413 {
1414 let val = val.trim();
1415 if val == "italic" || val == "oblique" {
1416 return Some("italic".to_string());
1417 }
1418 if val == "normal" {
1419 return Some("normal".to_string());
1420 }
1421 }
1422 }
1423 }
1424 None
1425}
1426
1427fn extract_exdata_color(elem: Node<'_, '_>) -> Option<(u8, u8, u8)> {
1431 let value = find_first_child_by_name(elem, "value")?;
1432 let ex = find_first_child_by_name(value, "exData")?;
1433 for desc in ex.descendants() {
1434 if !desc.is_element() {
1435 continue;
1436 }
1437 let style = desc.attribute("style")?;
1438 for part in style.split(';') {
1439 let part = part.trim();
1440 if let Some(val) = part
1441 .strip_prefix("color:")
1442 .or_else(|| part.strip_prefix("color :"))
1443 {
1444 let val = val.trim();
1445 if let Some(rgb) = parse_css_color(val) {
1446 return Some(rgb);
1447 }
1448 }
1449 }
1450 }
1451 None
1452}
1453
1454fn parse_ascii_hex_color(hex: &str) -> Option<(u8, u8, u8)> {
1456 fn hex_nibble(byte: u8) -> Option<u8> {
1457 match byte {
1458 b'0'..=b'9' => Some(byte - b'0'),
1459 b'a'..=b'f' => Some(byte - b'a' + 10),
1460 b'A'..=b'F' => Some(byte - b'A' + 10),
1461 _ => None,
1462 }
1463 }
1464
1465 match hex.as_bytes() {
1466 [r, g, b] => Some((
1467 hex_nibble(*r)? * 17,
1468 hex_nibble(*g)? * 17,
1469 hex_nibble(*b)? * 17,
1470 )),
1471 [r1, r2, g1, g2, b1, b2] => Some((
1472 (hex_nibble(*r1)? << 4) | hex_nibble(*r2)?,
1473 (hex_nibble(*g1)? << 4) | hex_nibble(*g2)?,
1474 (hex_nibble(*b1)? << 4) | hex_nibble(*b2)?,
1475 )),
1476 _ => None,
1477 }
1478}
1479
1480fn parse_css_color(s: &str) -> Option<(u8, u8, u8)> {
1481 let s = s.trim();
1482 if let Some(hex) = s.strip_prefix('#') {
1483 parse_ascii_hex_color(hex)
1484 } else if let Some(rgb_part) = s.strip_prefix("rgb(") {
1485 if let Some(inner) = rgb_part.strip_suffix(')') {
1486 let parts: Vec<&str> = inner.split(',').collect();
1487 if parts.len() >= 3 {
1488 let r: f64 = parts[0].trim().parse().ok()?;
1489 let g: f64 = parts[1].trim().parse().ok()?;
1490 let b: f64 = parts[2].trim().parse().ok()?;
1491 return Some((r as u8, g as u8, b as u8));
1492 }
1493 }
1494 None
1495 } else {
1496 let parts: Vec<&str> = s.split(',').collect();
1497 if parts.len() >= 3 {
1498 let r = parts[0].trim().parse::<u8>().ok()?;
1499 let g = parts[1].trim().parse::<u8>().ok()?;
1500 let b = parts[2].trim().parse::<u8>().ok()?;
1501 Some((r, g, b))
1502 } else {
1503 None
1504 }
1505 }
1506}
1507
1508fn extract_exdata_font_family(elem: Node<'_, '_>) -> Option<String> {
1512 let value = find_first_child_by_name(elem, "value")?;
1513 let ex = find_first_child_by_name(value, "exData")?;
1514 for desc in ex.descendants() {
1515 if !desc.is_element() {
1516 continue;
1517 }
1518 let style = desc.attribute("style")?;
1519 for part in style.split(';') {
1520 let part = part.trim();
1521 if let Some(val) = part
1522 .strip_prefix("font-family:")
1523 .or_else(|| part.strip_prefix("font-family :"))
1524 {
1525 let val = val.trim();
1526 let first_family = val
1527 .split(',')
1528 .next()
1529 .map(|s| s.trim().trim_matches(['"', '\'']))
1530 .filter(|s| !s.is_empty())?;
1531 return Some(first_family.to_string());
1532 }
1533 }
1534 }
1535 None
1536}
1537
1538fn extract_exdata_text_align(elem: Node<'_, '_>) -> Option<TextAlign> {
1542 let value = find_first_child_by_name(elem, "value")?;
1543 let ex = find_first_child_by_name(value, "exData")?;
1544 for desc in ex.descendants() {
1545 if !desc.is_element() {
1546 continue;
1547 }
1548 let style = desc.attribute("style")?;
1549 for part in style.split(';') {
1550 let part = part.trim();
1551 if let Some(val) = part
1552 .strip_prefix("text-align:")
1553 .or_else(|| part.strip_prefix("text-align :"))
1554 {
1555 let val = val.trim();
1556 return Some(match val {
1557 "center" => TextAlign::Center,
1558 "right" => TextAlign::Right,
1559 "justify" => TextAlign::Justify,
1560 _ => TextAlign::Left,
1561 });
1562 }
1563 }
1564 }
1565 None
1566}
1567
1568fn extract_exdata_margins(elem: Node<'_, '_>) -> Option<(f64, f64)> {
1572 let value = find_first_child_by_name(elem, "value")?;
1573 let ex = find_first_child_by_name(value, "exData")?;
1574 let mut space_above: Option<f64> = None;
1575 let mut space_below: Option<f64> = None;
1576 for desc in ex.descendants() {
1577 if !desc.is_element() {
1578 continue;
1579 }
1580 let style = match desc.attribute("style") {
1581 Some(s) => s,
1582 None => continue,
1583 };
1584 for part in style.split(';') {
1585 let part = part.trim();
1586 if space_above.is_none() {
1587 if let Some(val) = part
1588 .strip_prefix("margin-top:")
1589 .or_else(|| part.strip_prefix("margin-top :"))
1590 {
1591 let val = val.trim();
1592 if let Some(m) = Measurement::parse(val) {
1593 let pt = m.to_points();
1594 if pt >= 0.0 {
1595 space_above = Some(pt);
1596 }
1597 }
1598 }
1599 }
1600 if space_below.is_none() {
1601 if let Some(val) = part
1602 .strip_prefix("margin-bottom:")
1603 .or_else(|| part.strip_prefix("margin-bottom :"))
1604 {
1605 let val = val.trim();
1606 if let Some(m) = Measurement::parse(val) {
1607 let pt = m.to_points();
1608 if pt >= 0.0 {
1609 space_below = Some(pt);
1610 }
1611 }
1612 }
1613 }
1614 }
1615 if space_above.is_some() && space_below.is_some() {
1616 break;
1617 }
1618 }
1619 match (space_above, space_below) {
1620 (Some(above), Some(below)) => Some((above, below)),
1621 (Some(above), None) => Some((above, 0.0)),
1622 (None, Some(below)) => Some((0.0, below)),
1623 (None, None) => None,
1624 }
1625}
1626
1627#[derive(Debug, Clone, Default)]
1629struct InheritedStyle {
1630 font_size: Option<f64>,
1631 font_family: Option<String>,
1632 font_weight: Option<String>,
1633 font_style: Option<String>,
1634 text_color: Option<(u8, u8, u8)>,
1635 underline: bool,
1636 line_through: bool,
1637 baseline_shift: f64,
1639}
1640
1641impl InheritedStyle {
1642 fn merge_with_css(&self, css: &str) -> Self {
1643 let mut child = self.clone();
1644 for part in css.split(';') {
1645 let part = part.trim();
1646 if let Some(val) = strip_css_prop(part, "font-size") {
1647 if let Some(pt) = val.strip_suffix("pt") {
1648 if let Ok(size) = pt.trim().parse::<f64>() {
1649 if size > 0.0 {
1650 child.font_size = Some(size);
1651 }
1652 }
1653 }
1654 } else if let Some(val) = strip_css_prop(part, "font-family") {
1655 let first = val
1656 .split(',')
1657 .next()
1658 .map(|s| s.trim().trim_matches(['"', '\'']))
1659 .filter(|s| !s.is_empty());
1660 if let Some(fam) = first {
1661 child.font_family = Some(fam.to_string());
1662 }
1663 } else if let Some(val) = strip_css_prop(part, "font-weight") {
1664 if val == "bold" || val == "700" || val == "800" || val == "900" {
1665 child.font_weight = Some("bold".to_string());
1666 } else if val == "normal" || val == "400" || val == "500" {
1667 child.font_weight = Some("normal".to_string());
1668 }
1669 } else if let Some(val) = strip_css_prop(part, "font-style") {
1670 if val == "italic" || val == "oblique" {
1671 child.font_style = Some("italic".to_string());
1672 } else if val == "normal" {
1673 child.font_style = Some("normal".to_string());
1674 }
1675 } else if let Some(val) = strip_css_prop(part, "text-decoration") {
1676 if val.contains("underline") {
1677 child.underline = true;
1678 } else if val.contains("line-through") {
1679 child.line_through = true;
1680 } else if val == "none" {
1681 child.underline = false;
1682 child.line_through = false;
1683 }
1684 } else if let Some(val) = strip_css_prop(part, "vertical-align") {
1685 match val {
1688 "super" => child.baseline_shift = 0.33,
1689 "sub" => child.baseline_shift = -0.20,
1690 "baseline" => child.baseline_shift = 0.0,
1691 _ => {}
1692 }
1693 } else if let Some(val) = strip_css_prop(part, "color") {
1694 if let Some(rgb) = parse_css_color(val) {
1695 child.text_color = Some(rgb);
1696 }
1697 }
1698 }
1699 child
1700 }
1701
1702 fn to_span(&self, text: String) -> RichTextSpan {
1703 RichTextSpan {
1704 text,
1705 font_size: self.font_size,
1706 font_family: self.font_family.clone(),
1707 font_weight: self.font_weight.clone(),
1708 font_style: self.font_style.clone(),
1709 text_color: self.text_color,
1710 underline: self.underline,
1711 line_through: self.line_through,
1712 baseline_shift: self.baseline_shift,
1713 }
1714 }
1715}
1716
1717fn strip_css_prop<'a>(decl: &'a str, prop: &str) -> Option<&'a str> {
1718 decl.strip_prefix(prop)
1719 .and_then(|rest| rest.trim_start().strip_prefix(':'))
1720 .map(|v| v.trim())
1721}
1722
1723fn parse_exdata_rich_text_spans(elem: Node<'_, '_>) -> Option<Vec<RichTextSpan>> {
1725 let value = find_first_child_by_name(elem, "value")?;
1726 let ex = find_first_child_by_name(value, "exData")?;
1727 let ct = ex.attribute("contentType").unwrap_or("");
1728 if ct != "text/html" && ct != "text/xml" {
1729 return None;
1730 }
1731 let body = ex
1732 .descendants()
1733 .find(|d| d.is_element() && d.tag_name().name() == "body")?;
1734 let mut spans: Vec<RichTextSpan> = Vec::new();
1735 let base_style = InheritedStyle::default();
1736 let mut first_para = true;
1737 for child in body.children() {
1738 if child.is_element() && child.tag_name().name() == "p" {
1739 if !first_para && !spans.is_empty() {
1740 spans.push(RichTextSpan {
1741 text: "\n".to_string(),
1742 font_size: None,
1743 font_family: None,
1744 font_weight: None,
1745 font_style: None,
1746 text_color: None,
1747 underline: false,
1748 line_through: false,
1749 baseline_shift: 0.0,
1750 });
1751 }
1752 first_para = false;
1753 let p_style = match child.attribute("style") {
1754 Some(css) => base_style.merge_with_css(css),
1755 None => base_style.clone(),
1756 };
1757 collect_inline_spans(child, &p_style, &mut spans);
1758 } else if child.is_text() {
1759 if let Some(t) = child.text() {
1760 let t = t.trim();
1761 if !t.is_empty() {
1762 spans.push(base_style.to_span(t.to_string()));
1763 }
1764 }
1765 }
1766 }
1767 if spans.is_empty() {
1768 return None;
1769 }
1770 let all_default = spans.iter().all(|s| {
1771 s.font_size.is_none()
1772 && s.font_family.is_none()
1773 && s.font_weight.is_none()
1774 && s.font_style.is_none()
1775 && s.text_color.is_none()
1776 && !s.underline
1777 });
1778 if all_default {
1779 return None;
1780 }
1781 Some(spans)
1782}
1783
1784fn collect_inline_spans(
1785 node: Node<'_, '_>,
1786 inherited: &InheritedStyle,
1787 spans: &mut Vec<RichTextSpan>,
1788) {
1789 for child in node.children() {
1790 if child.is_text() {
1791 if let Some(t) = child.text() {
1792 let t = t.trim();
1793 if !t.is_empty() {
1794 spans.push(inherited.to_span(t.to_string()));
1795 }
1796 }
1797 } else if child.is_element() {
1798 let tag = child.tag_name().name();
1799 match tag {
1800 "br" => {
1801 spans.push(RichTextSpan {
1802 text: "\n".to_string(),
1803 font_size: None,
1804 font_family: None,
1805 font_weight: None,
1806 font_style: None,
1807 text_color: None,
1808 underline: false,
1809 line_through: false,
1810 baseline_shift: 0.0,
1811 });
1812 }
1813 "span" => {
1814 let child_style = match child.attribute("style") {
1815 Some(css) => {
1816 if css.contains("xfa-spacerun:yes") {
1817 if let Some(t) = child.text() {
1818 if !t.is_empty() {
1819 spans.push(inherited.to_span(t.to_string()));
1820 }
1821 }
1822 collect_inline_spans(child, inherited, spans);
1823 continue;
1824 }
1825 inherited.merge_with_css(css)
1826 }
1827 None => inherited.clone(),
1828 };
1829 collect_inline_spans(child, &child_style, spans);
1830 }
1831 "b" | "strong" => {
1832 let mut s = inherited.clone();
1833 s.font_weight = Some("bold".to_string());
1834 if let Some(css) = child.attribute("style") {
1835 s = s.merge_with_css(css);
1836 }
1837 collect_inline_spans(child, &s, spans);
1838 }
1839 "i" | "em" => {
1840 let mut s = inherited.clone();
1841 s.font_style = Some("italic".to_string());
1842 if let Some(css) = child.attribute("style") {
1843 s = s.merge_with_css(css);
1844 }
1845 collect_inline_spans(child, &s, spans);
1846 }
1847 "sup" => {
1853 let mut s = inherited.clone();
1854 s.baseline_shift = 0.33;
1855 s.font_size = s.font_size.map(|fs| fs * 0.83);
1856 if let Some(css) = child.attribute("style") {
1857 s = s.merge_with_css(css);
1858 }
1859 collect_inline_spans(child, &s, spans);
1860 }
1861 "sub" => {
1862 let mut s = inherited.clone();
1863 s.baseline_shift = -0.20;
1864 s.font_size = s.font_size.map(|fs| fs * 0.83);
1865 if let Some(css) = child.attribute("style") {
1866 s = s.merge_with_css(css);
1867 }
1868 collect_inline_spans(child, &s, spans);
1869 }
1870 "u" => {
1871 let mut s = inherited.clone();
1872 s.underline = true;
1873 if let Some(css) = child.attribute("style") {
1874 s = s.merge_with_css(css);
1875 }
1876 collect_inline_spans(child, &s, spans);
1877 }
1878 _ => {
1879 let child_style = match child.attribute("style") {
1880 Some(css) => inherited.merge_with_css(css),
1881 None => inherited.clone(),
1882 };
1883 collect_inline_spans(child, &child_style, spans);
1884 }
1885 }
1886 }
1887 }
1888}
1889
1890fn parse_caption(elem: Node<'_, '_>) -> Option<Caption> {
1891 let cap_elem = find_first_child_by_name(elem, "caption")?;
1892 if is_hidden(cap_elem) {
1893 return None;
1894 }
1895 let text = extract_value_text(cap_elem)?;
1896 if text.is_empty() {
1897 return None;
1898 }
1899 let placement = match attr(cap_elem, "placement") {
1900 Some("right") => CaptionPlacement::Right,
1901 Some("top") => CaptionPlacement::Top,
1902 Some("bottom") => CaptionPlacement::Bottom,
1903 Some("inline") => CaptionPlacement::Inline,
1904 _ => CaptionPlacement::Left,
1905 };
1906 let reserve = attr(cap_elem, "reserve")
1907 .and_then(Measurement::parse)
1908 .map(|m| m.to_points());
1909 let cap_font = find_first_child_by_name(cap_elem, "font");
1913 let font_family = cap_font
1914 .and_then(|f| attr(f, "typeface"))
1915 .map(|s| s.to_string());
1916 let font_size = cap_font
1917 .and_then(|f| attr(f, "size"))
1918 .and_then(parse_font_size);
1919 Some(Caption {
1920 placement,
1921 reserve,
1922 text,
1923 font_family,
1924 font_size,
1925 })
1926}
1927
1928fn is_hidden(elem: Node<'_, '_>) -> bool {
1929 matches!(
1930 attr(elem, "presence"),
1931 Some("hidden") | Some("invisible") | Some("inactive")
1932 )
1933}
1934
1935fn parse_presence_str(s: &str) -> Presence {
1940 match s.trim() {
1941 "hidden" => Presence::Hidden,
1942 "invisible" => Presence::Invisible,
1943 "inactive" => Presence::Inactive,
1944 _ => Presence::Visible,
1945 }
1946}
1947
1948fn extract_value_image(
1949 elem: Node<'_, '_>,
1950 image_files: &std::collections::HashMap<String, Vec<u8>>,
1951) -> Option<(Vec<u8>, String)> {
1952 let value = find_first_child_by_name(elem, "value")?;
1953 let image = find_first_child_by_name(value, "image")?;
1954 let content_type = attr(image, "contentType")
1955 .unwrap_or("image/png")
1956 .to_string();
1957
1958 if let Some(href) = attr(image, "href") {
1961 if let Some(data) = image_files.get(href) {
1962 let decoded = data.clone();
1963 if decoded.starts_with(b"BM") || content_type == "image/bmp" {
1964 if let Some(png_data) = bmp_to_png(&decoded) {
1965 return Some((png_data, "image/png".to_string()));
1966 }
1967 return None;
1968 }
1969 return Some((decoded, content_type));
1970 }
1971 let normalized = href.trim_start_matches(".\\").trim_start_matches("./");
1973 for (k, data) in image_files {
1974 let k_norm = k.trim_start_matches(".\\").trim_start_matches("./");
1975 if k_norm == normalized {
1976 let decoded = data.clone();
1977 if decoded.starts_with(b"BM") || content_type == "image/bmp" {
1978 if let Some(png_data) = bmp_to_png(&decoded) {
1979 return Some((png_data, "image/png".to_string()));
1980 }
1981 return None;
1982 }
1983 return Some((decoded, content_type));
1984 }
1985 }
1986 }
1987
1988 let data = image.text().unwrap_or_default();
1989 let decoded = base64_decode(data);
1990
1991 if decoded.is_empty() {
1993 return None;
1994 }
1995
1996 if decoded.starts_with(b"BM") || content_type == "image/bmp" {
1997 if let Some(png_data) = bmp_to_png(&decoded) {
1998 return Some((png_data, "image/png".to_string()));
1999 }
2000 log::warn!("BMP to PNG conversion failed; skipping image");
2001 return None;
2002 }
2003
2004 Some((decoded, content_type))
2005}
2006
2007fn extract_draw_content(elem: Node<'_, '_>) -> Option<DrawContent> {
2008 let value = find_first_child_by_name(elem, "value")?;
2009
2010 if let Some(line) = find_first_child_by_name(value, "line") {
2011 let x1 = attr_as_f64(line, "x1").unwrap_or(0.0);
2012 let y1 = attr_as_f64(line, "y1").unwrap_or(0.0);
2013 let x2 = attr_as_f64(line, "x2").unwrap_or(0.0);
2014 let y2 = attr_as_f64(line, "y2").unwrap_or(0.0);
2015 return Some(DrawContent::Line { x1, y1, x2, y2 });
2016 }
2017
2018 if let Some(rect) = find_first_child_by_name(value, "rectangle") {
2019 let x = attr_as_f64(rect, "x").unwrap_or(0.0);
2020 let y = attr_as_f64(rect, "y").unwrap_or(0.0);
2021 let w = attr_as_f64(rect, "w").unwrap_or(attr_as_f64(rect, "width").unwrap_or(0.0));
2022 let h = attr_as_f64(rect, "h").unwrap_or(attr_as_f64(rect, "height").unwrap_or(0.0));
2023 let radius =
2024 attr_as_f64(rect, "r").unwrap_or(attr_as_f64(rect, "cornerRadius").unwrap_or(0.0));
2025 return Some(DrawContent::Rectangle { x, y, w, h, radius });
2026 }
2027
2028 if let Some(arc) = find_first_child_by_name(value, "arc") {
2029 let x = attr_as_f64(arc, "x").unwrap_or(0.0);
2030 let y = attr_as_f64(arc, "y").unwrap_or(0.0);
2031 let w = attr_as_f64(arc, "w").unwrap_or(attr_as_f64(arc, "width").unwrap_or(0.0));
2032 let h = attr_as_f64(arc, "h").unwrap_or(attr_as_f64(arc, "height").unwrap_or(0.0));
2033 let start_angle = attr_as_f64(arc, "startAngle").unwrap_or(0.0);
2034 let sweep_angle = attr_as_f64(arc, "sweepAngle").unwrap_or(0.0);
2035 return Some(DrawContent::Arc {
2036 x,
2037 y,
2038 w,
2039 h,
2040 start_angle,
2041 sweep_angle,
2042 });
2043 }
2044
2045 None
2046}
2047
2048fn base64_decode(input: &str) -> Vec<u8> {
2049 use base64::Engine;
2050 base64::engine::general_purpose::STANDARD
2051 .decode(input.trim())
2052 .unwrap_or_default()
2053}
2054
2055fn bmp_to_png(bmp_data: &[u8]) -> Option<Vec<u8>> {
2056 let img = image::load_from_memory_with_format(bmp_data, image::ImageFormat::Bmp).ok()?;
2057 let mut buf = Vec::new();
2058 img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
2059 .ok()?;
2060 Some(buf)
2061}
2062
2063fn attr_as_f64(elem: Node<'_, '_>, name: &str) -> Option<f64> {
2064 attr(elem, name)?.parse().ok()
2065}
2066
2067fn parse_col_span(elem: Node<'_, '_>) -> i32 {
2068 attr(elem, "colSpan")
2069 .and_then(|s| s.parse().ok())
2070 .unwrap_or(1)
2071}
2072
2073fn parse_node_meta(elem: Node<'_, '_>) -> FormNodeMeta {
2074 let tag = elem.tag_name().name();
2075 let presence = match attr(elem, "presence") {
2076 Some("hidden") => Presence::Hidden,
2077 Some("invisible") => Presence::Invisible,
2078 Some("inactive") => Presence::Inactive,
2079 _ => Presence::Visible,
2080 };
2081
2082 let (page_break_before, break_before_target) = detect_page_break_before(elem);
2083 let (page_break_after, break_after_target) = detect_page_break_after(elem);
2084 let content_area_break = detect_content_area_break(elem);
2085 let break_target = break_before_target.or(break_after_target);
2086
2087 let event_scripts = collect_event_scripts(elem);
2088 let (keep_next_content_area, keep_previous_content_area, keep_intact_content_area) =
2089 parse_keep(elem);
2090 let (overflow_leader, overflow_trailer) = parse_overflow(elem);
2091 let (bookend_leader, bookend_trailer) = parse_bookend(elem);
2092
2093 let group_kind = if tag == "exclGroup" {
2094 GroupKind::ExclusiveChoice
2095 } else {
2096 GroupKind::None
2097 };
2098
2099 let item_value = if tag == "field" {
2100 parse_item_value(elem)
2101 } else {
2102 None
2103 };
2104
2105 let (display_items, save_items) = if tag == "field" {
2106 parse_items_lists(elem)
2107 } else {
2108 (Vec::new(), Vec::new())
2109 };
2110
2111 let xfa_id = attr(elem, "id").map(|s| s.to_string());
2112 let field_kind = detect_field_kind(elem);
2113 let style = parse_node_style(elem);
2114 let (data_bind_ref, data_bind_none) = parse_bind(elem);
2115 let anchor_type = parse_anchor_type(elem);
2116 let access = attr(elem, "access").map(Access::parse);
2117 let multiline = parse_multiline(elem);
2118 let required = parse_required(elem);
2119
2120 FormNodeMeta {
2121 xfa_id,
2122 presence,
2123 access,
2124 multiline,
2125 required,
2126 page_break_before,
2127 page_break_after,
2128 break_target,
2129 content_area_break,
2130 overflow_leader,
2131 overflow_trailer,
2132 bookend_leader,
2133 bookend_trailer,
2134 keep_next_content_area,
2135 keep_previous_content_area,
2136 keep_intact_content_area,
2137 event_scripts,
2138 data_bind_ref,
2139 data_bind_none,
2140 group_kind,
2141 item_value,
2142 field_kind,
2143 style,
2144 display_items,
2145 save_items,
2146 anchor_type,
2147 ..Default::default()
2148 }
2149}
2150
2151fn detect_page_break_before(elem: Node<'_, '_>) -> (bool, Option<String>) {
2152 for child in elem.children().filter(|n| n.is_element()) {
2153 let tag = child.tag_name().name();
2154 if matches!(tag, "subform" | "field" | "draw" | "exclGroup") {
2155 break;
2156 }
2157 if tag == "breakBefore" && attr(child, "targetType") == Some("pageArea") {
2158 return (true, attr(child, "target").map(|s| s.to_string()));
2159 }
2160 if tag == "break" && attr(child, "before") == Some("pageArea") {
2161 return (true, attr(child, "target").map(|s| s.to_string()));
2162 }
2163 }
2164 (false, None)
2165}
2166
2167fn detect_page_break_after(elem: Node<'_, '_>) -> (bool, Option<String>) {
2168 let mut last_content_idx = 0;
2169 let children: Vec<_> = elem.children().filter(|n| n.is_element()).collect();
2170 for (i, child) in children.iter().enumerate() {
2171 let tag = child.tag_name().name();
2172 if matches!(tag, "subform" | "field" | "draw" | "exclGroup") {
2173 last_content_idx = i;
2174 }
2175 }
2176
2177 for child in children.iter().skip(last_content_idx) {
2178 let tag = child.tag_name().name();
2179 if tag == "breakAfter" && attr(*child, "targetType") == Some("pageArea") {
2180 return (true, attr(*child, "target").map(|s| s.to_string()));
2181 }
2182 if tag == "break" && attr(*child, "after") == Some("pageArea") {
2183 return (true, attr(*child, "target").map(|s| s.to_string()));
2184 }
2185 }
2186 (false, None)
2187}
2188
2189fn detect_content_area_break(elem: Node<'_, '_>) -> bool {
2190 for child in elem.children().filter(|n| n.is_element()) {
2191 let tag = child.tag_name().name();
2192 if tag == "breakBefore" && attr(child, "targetType") == Some("contentArea") {
2193 return true;
2194 }
2195 }
2196 false
2197}
2198
2199fn collect_event_scripts(elem: Node<'_, '_>) -> Vec<EventScript> {
2200 let mut scripts = Vec::new();
2201 for child in elem.children().filter(|n| n.is_element()) {
2202 let child_tag = child.tag_name().name();
2203 if child_tag == "event" {
2204 let activity = attr(child, "activity");
2205 let event_ref = attr(child, "ref");
2206 if activity == Some("ready") && event_ref == Some("$layout") {
2207 continue;
2208 }
2209 if let Some(script_elem) = find_first_child_by_name(child, "script") {
2210 if let Some(script) =
2211 build_event_script(script_elem, activity, event_ref, attr(script_elem, "runAt"))
2212 {
2213 scripts.push(script);
2214 }
2215 }
2216 } else if child_tag == "calculate" {
2217 if let Some(script_elem) = find_first_child_by_name(child, "script") {
2218 if let Some(script) = build_event_script(
2219 script_elem,
2220 Some("calculate"),
2221 None,
2222 attr(script_elem, "runAt"),
2223 ) {
2224 scripts.push(script);
2225 }
2226 }
2227 }
2228 }
2229 scripts
2230}
2231
2232fn build_event_script(
2233 script_elem: Node<'_, '_>,
2234 activity: Option<&str>,
2235 event_ref: Option<&str>,
2236 run_at: Option<&str>,
2237) -> Option<EventScript> {
2238 let text = script_elem.text()?.trim();
2239 if text.is_empty() {
2240 return None;
2241 }
2242
2243 Some(EventScript::new(
2244 text.to_string(),
2245 detect_script_language(attr(script_elem, "contentType")),
2246 activity.map(str::to_string),
2247 event_ref.map(str::to_string),
2248 run_at.map(str::to_string),
2249 ))
2250}
2251
2252fn detect_script_language(content_type: Option<&str>) -> ScriptLanguage {
2253 match content_type.map(|value| value.trim().to_ascii_lowercase()) {
2254 None => ScriptLanguage::FormCalc,
2255 Some(value) if value == "application/x-formcalc" || value.ends_with("/x-formcalc") => {
2256 ScriptLanguage::FormCalc
2257 }
2258 Some(value)
2259 if value == "application/x-javascript"
2260 || value == "application/javascript"
2261 || value == "text/javascript"
2262 || value.ends_with("/x-javascript") =>
2263 {
2264 ScriptLanguage::JavaScript
2265 }
2266 Some(_) => ScriptLanguage::Other,
2267 }
2268}
2269
2270fn parse_keep(elem: Node<'_, '_>) -> (bool, bool, bool) {
2271 if let Some(keep) = find_first_child_by_name(elem, "keep") {
2272 let next = attr(keep, "next") == Some("contentArea");
2273 let prev = attr(keep, "previous") == Some("contentArea");
2274 let intact = attr(keep, "intact") == Some("contentArea");
2275 (next, prev, intact)
2276 } else {
2277 (false, false, false)
2278 }
2279}
2280
2281fn parse_bookend(elem: Node<'_, '_>) -> (Option<String>, Option<String>) {
2288 match find_first_child_by_name(elem, "bookend") {
2289 Some(bookend) => (
2290 attr(bookend, "leader").map(|s| s.to_string()),
2291 attr(bookend, "trailer").map(|s| s.to_string()),
2292 ),
2293 None => (None, None),
2294 }
2295}
2296
2297fn parse_overflow(elem: Node<'_, '_>) -> (Option<String>, Option<String>) {
2298 if let Some(overflow) = find_first_child_by_name(elem, "overflow") {
2299 let leader = attr(overflow, "leader").map(|s| s.to_string());
2300 let trailer = attr(overflow, "trailer").map(|s| s.to_string());
2301 (leader, trailer)
2302 } else {
2303 (None, None)
2304 }
2305}
2306
2307fn parse_item_value(elem: Node<'_, '_>) -> Option<String> {
2308 let items = find_first_child_by_name(elem, "items")?;
2309 let text_elem = find_first_child_by_name(items, "text")?;
2310 let text = text_elem.text()?.trim();
2311 if text.is_empty() {
2312 None
2313 } else {
2314 Some(text.to_string())
2315 }
2316}
2317
2318fn collect_items_texts(items_elem: Node<'_, '_>) -> Vec<String> {
2320 items_elem
2321 .children()
2322 .filter(|n| n.is_element())
2323 .filter_map(|child| {
2324 let txt = child.text().unwrap_or("").trim().to_string();
2325 if txt.is_empty() {
2326 None
2327 } else {
2328 Some(txt)
2329 }
2330 })
2331 .collect()
2332}
2333
2334fn parse_items_lists(elem: Node<'_, '_>) -> (Vec<String>, Vec<String>) {
2336 let items_elems: Vec<_> = elem
2337 .children()
2338 .filter(|n| n.is_element() && n.tag_name().name() == "items")
2339 .collect();
2340 match items_elems.len() {
2341 0 => (Vec::new(), Vec::new()),
2342 1 => {
2343 let vals = collect_items_texts(items_elems[0]);
2344 (vals, Vec::new())
2345 }
2346 _ => {
2347 let first = items_elems[0];
2348 let second = items_elems[1];
2349 let first_is_save = attr(first, "save") == Some("1");
2350 if first_is_save {
2351 (collect_items_texts(second), collect_items_texts(first))
2352 } else {
2353 (collect_items_texts(first), collect_items_texts(second))
2354 }
2355 }
2356 }
2357}
2358
2359pub(crate) fn parse_multiline(elem: Node<'_, '_>) -> bool {
2365 let Some(ui) = find_first_child_by_name(elem, "ui") else {
2366 return false;
2367 };
2368 let Some(te) = find_first_child_by_name(ui, "textEdit") else {
2369 return false;
2370 };
2371 matches!(attr(te, "multiLine"), Some("1") | Some("true"))
2372}
2373
2374pub(crate) fn parse_required(elem: Node<'_, '_>) -> bool {
2378 find_first_child_by_name(elem, "validate")
2379 .and_then(|v| attr(v, "nullTest"))
2380 .map(|s| s == "error")
2381 .unwrap_or(false)
2382}
2383
2384fn detect_field_kind(elem: Node<'_, '_>) -> FieldKind {
2385 let Some(ui) = find_first_child_by_name(elem, "ui") else {
2386 return FieldKind::Text;
2387 };
2388 for child in ui.children().filter(|n| n.is_element()) {
2389 match child.tag_name().name() {
2390 "button" => return FieldKind::Button,
2391 "checkButton" => {
2392 let shape = attr(child, "shape").unwrap_or("square");
2396 return if shape == "round" {
2397 FieldKind::Radio
2398 } else {
2399 FieldKind::Checkbox
2400 };
2401 }
2402 "choiceList" => return FieldKind::Dropdown,
2403 "dateTimeEdit" => return FieldKind::DateTimePicker,
2404 "numericEdit" => return FieldKind::NumericEdit,
2405 "passwordEdit" => return FieldKind::PasswordEdit,
2406 "imageEdit" => return FieldKind::ImageEdit,
2407 "signature" => return FieldKind::Signature,
2408 "barcode" => return FieldKind::Barcode,
2409 _ => {}
2410 }
2411 }
2412 FieldKind::Text
2413}
2414
2415fn parse_node_style(elem: Node<'_, '_>) -> FormNodeStyle {
2416 let (check_on_value, check_off_value, check_neutral_value) = parse_check_button_values(elem);
2417 let mut style = FormNodeStyle {
2418 check_button_mark: parse_check_button_mark(elem),
2419 check_button_on_value: check_on_value,
2420 check_button_off_value: check_off_value,
2421 check_button_neutral_value: check_neutral_value,
2422 ..Default::default()
2423 };
2424 if let Some(fill) = find_first_child_by_name(elem, "fill") {
2425 if !is_hidden(fill) {
2426 style.bg_color = parse_fill_color(fill);
2427 }
2428 }
2429 let border = find_first_child_by_name(elem, "border").or_else(|| {
2431 let ui = find_first_child_by_name(elem, "ui")?;
2432 ui.children()
2433 .filter(|c| c.is_element() && c.tag_name().name() != "border")
2434 .find_map(|widget| find_first_child_by_name(widget, "border"))
2435 });
2436 if let Some(border) = border {
2437 let edge_elems: Vec<_> = border
2439 .children()
2440 .filter(|n| n.is_element() && n.tag_name().name() == "edge")
2441 .collect();
2442 if !edge_elems.is_empty() {
2443 let first_visible = edge_elems
2444 .iter()
2445 .find(|e| !is_hidden(**e) && attr(**e, "stroke").unwrap_or("solid") != "none")
2446 .or_else(|| edge_elems.first())
2447 .copied();
2448 let first = first_visible.unwrap_or(edge_elems[0]);
2449 if let Some(color) = find_first_child_by_name(first, "color") {
2450 if let Some(rgb) = parse_xfa_color(color) {
2451 style.border_color = Some(rgb);
2452 }
2453 }
2454 let stroke = attr(first, "stroke").unwrap_or("solid");
2455 if stroke != "none" {
2456 let thickness = attr(first, "thickness")
2457 .and_then(Measurement::parse)
2458 .map(|m: Measurement| m.to_points())
2459 .unwrap_or(0.5);
2460 if thickness > 0.0 {
2461 style.border_width_pt = Some(thickness);
2462 }
2463 }
2464
2465 let edge_visible = |e: &roxmltree::Node<'_, '_>| -> bool {
2470 !is_hidden(*e) && attr(*e, "stroke").unwrap_or("solid") != "none"
2471 };
2472 style.border_edges = match edge_elems.len() {
2473 0 => [true, true, true, true],
2474 1 => {
2475 let v = edge_visible(&edge_elems[0]);
2476 [v, v, v, v]
2477 }
2478 2 => {
2479 let even = edge_visible(&edge_elems[0]);
2480 let odd = edge_visible(&edge_elems[1]);
2481 [even, odd, even, odd]
2482 }
2483 3 => {
2484 let top = edge_visible(&edge_elems[0]);
2485 let rl = edge_visible(&edge_elems[1]);
2486 let bot = edge_visible(&edge_elems[2]);
2487 [top, rl, bot, rl]
2488 }
2489 _ => [
2490 edge_visible(&edge_elems[0]),
2491 edge_visible(&edge_elems[1]),
2492 edge_visible(&edge_elems[2]),
2493 edge_visible(&edge_elems[3]),
2494 ],
2495 };
2496
2497 if edge_elems.len() > 1 {
2498 let default_rgb = style.border_color.unwrap_or((0, 0, 0));
2499 let edge_color = |edge: roxmltree::Node<'_, '_>| -> (u8, u8, u8) {
2500 find_first_child_by_name(edge, "color")
2501 .and_then(parse_xfa_color)
2502 .unwrap_or(default_rgb)
2503 };
2504 let per_edge_colors = match edge_elems.len() {
2505 2 => Some([
2506 edge_color(edge_elems[0]),
2507 edge_color(edge_elems[1]),
2508 edge_color(edge_elems[0]),
2509 edge_color(edge_elems[1]),
2510 ]),
2511 3 => Some([
2512 edge_color(edge_elems[0]),
2513 edge_color(edge_elems[1]),
2514 edge_color(edge_elems[2]),
2515 edge_color(edge_elems[1]),
2516 ]),
2517 _ => Some([
2518 edge_color(edge_elems[0]),
2519 edge_color(edge_elems[1]),
2520 edge_color(edge_elems[2]),
2521 edge_color(edge_elems[3]),
2522 ]),
2523 };
2524 if let Some([top, right, bottom, left]) = per_edge_colors {
2525 if !(top == bottom && bottom == left && left == right) {
2526 style.border_colors = Some([top, right, bottom, left]);
2527 }
2528 }
2529
2530 let default_thickness = style.border_width_pt.unwrap_or(0.5);
2531 let edge_thickness = |edge: roxmltree::Node<'_, '_>| -> f64 {
2532 attr(edge, "thickness")
2533 .and_then(Measurement::parse)
2534 .map(|m: Measurement| m.to_points())
2535 .unwrap_or(default_thickness)
2536 };
2537 let per_edge_widths = match edge_elems.len() {
2538 2 => Some([
2539 edge_thickness(edge_elems[0]),
2540 edge_thickness(edge_elems[1]),
2541 edge_thickness(edge_elems[0]),
2542 edge_thickness(edge_elems[1]),
2543 ]),
2544 3 => Some([
2545 edge_thickness(edge_elems[0]),
2546 edge_thickness(edge_elems[1]),
2547 edge_thickness(edge_elems[2]),
2548 edge_thickness(edge_elems[1]),
2549 ]),
2550 _ => Some([
2551 edge_thickness(edge_elems[0]),
2552 edge_thickness(edge_elems[1]),
2553 edge_thickness(edge_elems[2]),
2554 edge_thickness(edge_elems[3]),
2555 ]),
2556 };
2557 if let Some([top_t, right_t, bottom_t, left_t]) = per_edge_widths {
2558 if !(top_t == bottom_t && bottom_t == left_t && left_t == right_t) {
2559 style.border_widths = Some([top_t, right_t, bottom_t, left_t]);
2560 }
2561 }
2562 }
2563 }
2564 if style.bg_color.is_none() {
2565 if let Some(fill) = find_first_child_by_name(border, "fill") {
2566 if !is_hidden(fill) {
2567 style.bg_color = parse_fill_color(fill);
2572 }
2573 }
2574 }
2575 }
2576 if style.border_width_pt.is_none() {
2584 if let Some(line) = find_first_child_by_name(elem, "value")
2585 .and_then(|value| find_first_child_by_name(value, "line"))
2586 {
2587 if let Some(edge) = find_first_child_by_name(line, "edge") {
2588 if attr(edge, "stroke").unwrap_or("solid") != "none" {
2589 if let Some(thickness) = attr(edge, "thickness")
2590 .and_then(Measurement::parse)
2591 .map(|m: Measurement| m.to_points())
2592 {
2593 if thickness > 0.0 {
2594 style.border_width_pt = Some(thickness);
2595 }
2596 }
2597 if style.border_color.is_none() {
2598 if let Some(rgb) =
2599 find_first_child_by_name(edge, "color").and_then(parse_xfa_color)
2600 {
2601 style.border_color = Some(rgb);
2602 }
2603 }
2604 }
2605 }
2606 }
2607 }
2608 if let Some(font) = find_first_child_by_name(elem, "font") {
2609 if let Some(typeface) = attr(font, "typeface") {
2610 style.font_family = Some(typeface.to_string());
2611 }
2612 if let Some(gf) = attr(font, "genericFamily") {
2613 style.generic_family = Some(gf.to_string());
2614 }
2615 if let Some(size_str) = attr(font, "size") {
2616 if let Some(m) = Measurement::parse(size_str) {
2617 let m: Measurement = m;
2618 style.font_size = Some(m.to_points());
2619 }
2620 }
2621 if let Some(weight) = attr(font, "weight") {
2622 style.font_weight = Some(weight.to_string());
2623 }
2624 if let Some(posture) = attr(font, "posture") {
2625 style.font_style = Some(posture.to_string());
2626 }
2627 if let Some(fill) = find_first_child_by_name(font, "fill") {
2628 if let Some(color) = find_first_child_by_name(fill, "color") {
2629 if let Some(rgb) = parse_xfa_color(color) {
2630 style.text_color = Some(rgb);
2631 }
2632 }
2633 }
2634 if style.text_color.is_none() {
2636 if let Some(color_str) = attr(font, "color") {
2637 if let Some(rgb) = parse_font_color_attr(color_str) {
2638 style.text_color = Some(rgb);
2639 }
2640 }
2641 }
2642 if let Some(scale_str) = attr(font, "fontHorizontalScale") {
2644 if let Some(v) = parse_percentage(scale_str) {
2645 style.font_horizontal_scale = Some(v);
2646 }
2647 }
2648 if let Some(ls_str) = attr(font, "letterSpacing") {
2650 if let Some(v) = parse_letter_spacing(ls_str, style.font_size.unwrap_or(10.0)) {
2651 style.letter_spacing_pt = Some(v);
2652 }
2653 }
2654 if let Some(underline_str) = attr(font, "underline") {
2656 style.underline = underline_str == "1" || underline_str == "2";
2657 }
2658 if let Some(line_through_str) = attr(font, "lineThrough") {
2660 style.line_through = line_through_str == "1";
2661 }
2662 }
2663
2664 if let Some(para) = find_first_child_by_name(elem, "para") {
2666 if let Some(v) = attr(para, "spaceAbove").and_then(Measurement::parse) {
2667 style.space_above_pt = Some(v.to_points());
2668 }
2669 if let Some(v) = attr(para, "spaceBelow").and_then(Measurement::parse) {
2670 style.space_below_pt = Some(v.to_points());
2671 }
2672 if let Some(v) = attr(para, "marginLeft").and_then(Measurement::parse) {
2673 style.margin_left_pt = Some(v.to_points());
2674 }
2675 if let Some(v) = attr(para, "marginRight").and_then(Measurement::parse) {
2676 style.margin_right_pt = Some(v.to_points());
2677 }
2678 if let Some(va) = attr(para, "vAlign") {
2679 style.v_align = Some(match va {
2680 "middle" => VerticalAlign::Middle,
2681 "bottom" => VerticalAlign::Bottom,
2682 _ => VerticalAlign::Top,
2683 });
2684 }
2685 }
2686
2687 if let Some(border) = border {
2689 if let Some(corner) = find_first_child_by_name(border, "corner") {
2690 if let Some(v) = attr(corner, "radius").and_then(Measurement::parse) {
2691 style.border_radius_pt = Some(v.to_points());
2692 }
2693 }
2694 let edge_elems2: Vec<_> = border
2695 .children()
2696 .filter(|n| n.is_element() && n.tag_name().name() == "edge")
2697 .collect();
2698 if !edge_elems2.is_empty() {
2699 if let Some(stroke) = attr(edge_elems2[0], "stroke") {
2700 if stroke != "none" {
2701 style.border_style = Some(stroke.to_string());
2702 }
2703 }
2704 }
2705 }
2706
2707 if let Some(cap) = parse_caption(elem) {
2709 style.caption_text = Some(cap.text);
2710 style.caption_placement = Some(
2711 match cap.placement {
2712 CaptionPlacement::Left => "left",
2713 CaptionPlacement::Right => "right",
2714 CaptionPlacement::Top => "top",
2715 CaptionPlacement::Bottom => "bottom",
2716 CaptionPlacement::Inline => "inline",
2717 }
2718 .to_string(),
2719 );
2720 style.caption_reserve = cap.reserve;
2721 style.caption_font_family = cap.font_family;
2722 style.caption_font_size = cap.font_size;
2723 }
2724
2725 style
2726}
2727
2728fn parse_fill_color(fill_node: Node<'_, '_>) -> Option<(u8, u8, u8)> {
2729 find_first_child_by_name(fill_node, "color")
2730 .and_then(parse_xfa_color)
2731 .or_else(|| {
2732 find_first_child_by_name(fill_node, "solid")
2733 .and_then(|solid| find_first_child_by_name(solid, "color"))
2734 .and_then(parse_xfa_color)
2735 })
2736}
2737
2738fn parse_check_button_mark(elem: Node<'_, '_>) -> Option<String> {
2739 let ui = find_first_child_by_name(elem, "ui")?;
2740 let check_button = ui
2741 .children()
2742 .find(|n| n.is_element() && n.tag_name().name() == "checkButton")?;
2743 let mark = attr(check_button, "mark")?.to_ascii_lowercase();
2744 match mark.as_str() {
2745 "check" | "circle" | "cross" | "diamond" | "square" | "star" => Some(mark),
2746 _ => None,
2747 }
2748}
2749
2750fn parse_check_button_values(
2751 elem: Node<'_, '_>,
2752) -> (Option<String>, Option<String>, Option<String>) {
2753 let ui = match find_first_child_by_name(elem, "ui") {
2754 Some(ui) => ui,
2755 None => return (None, None, None),
2756 };
2757 let check_button = match find_first_child_by_name(ui, "checkButton") {
2758 Some(check_button) => check_button,
2759 None => return (None, None, None),
2760 };
2761
2762 let items = find_first_child_by_name(elem, "items")
2767 .or_else(|| find_first_child_by_name(check_button, "items"));
2768 let Some(items) = items else {
2769 return (None, None, None);
2770 };
2771
2772 let values: Vec<String> = items
2773 .children()
2774 .filter(|n| n.is_element())
2775 .map(|child| child.text().unwrap_or("").trim().to_string())
2776 .collect();
2777
2778 (
2779 values.first().cloned(),
2780 values.get(1).cloned(),
2781 values.get(2).cloned(),
2782 )
2783}
2784
2785fn parse_xfa_color(color_node: Node<'_, '_>) -> Option<(u8, u8, u8)> {
2786 let value = attr(color_node, "value")?;
2787 let parts: Vec<&str> = value.split(',').collect();
2788 if parts.len() >= 3 {
2789 let r = parts[0].trim().parse::<u8>().ok()?;
2790 let g = parts[1].trim().parse::<u8>().ok()?;
2791 let b = parts[2].trim().parse::<u8>().ok()?;
2792 Some((r, g, b))
2793 } else {
2794 None
2795 }
2796}
2797
2798fn parse_font_color_attr(s: &str) -> Option<(u8, u8, u8)> {
2805 let s = s.trim();
2806 if let Some(hex) = s.strip_prefix('#') {
2807 parse_ascii_hex_color(hex)
2808 } else {
2809 let parts: Vec<&str> = s.split(',').collect();
2811 if parts.len() >= 3 {
2812 let r = parts[0].trim().parse::<u8>().ok()?;
2813 let g = parts[1].trim().parse::<u8>().ok()?;
2814 let b = parts[2].trim().parse::<u8>().ok()?;
2815 Some((r, g, b))
2816 } else {
2817 None
2818 }
2819 }
2820}
2821
2822#[allow(dead_code)]
2835fn parse_bind_data_name(elem: Node<'_, '_>) -> Option<String> {
2836 let bind = find_first_child_by_name(elem, "bind")?;
2837 let ref_val = attr(bind, "ref")?;
2838 let segment = ref_val.rsplit('.').next().unwrap_or(ref_val);
2841 let name = segment.split('[').next().unwrap_or(segment).trim();
2842 if name.is_empty() || name == "$" {
2843 None
2844 } else {
2845 Some(name.to_string())
2846 }
2847}
2848
2849fn parse_bind(elem: Node<'_, '_>) -> (Option<String>, bool) {
2858 let Some(bind) = find_first_child_by_name(elem, "bind") else {
2859 return (None, false);
2860 };
2861 let bind_none = attr(bind, "match") == Some("none");
2862 let bind_ref = if bind_none {
2863 None
2864 } else {
2865 attr(bind, "ref").map(|s| s.trim().to_string())
2866 };
2867 (bind_ref, bind_none)
2868}
2869
2870#[cfg(test)]
2871mod tests {
2872 use super::*;
2873 use xfa_dom_resolver::data_dom::DataDom;
2874
2875 #[test]
2876 fn parse_css_color_rejects_non_ascii_hex_without_panicking() {
2877 assert_eq!(parse_css_color("#€"), None);
2878 assert_eq!(parse_css_color("#€abc"), None);
2879 assert_eq!(parse_css_color("#0f8"), Some((0x00, 0xff, 0x88)));
2880 assert_eq!(parse_css_color("#00ff88"), Some((0x00, 0xff, 0x88)));
2881 }
2882
2883 #[test]
2884 fn parse_font_color_attr_rejects_non_ascii_hex_without_panicking() {
2885 assert_eq!(parse_font_color_attr("#€"), None);
2886 assert_eq!(parse_font_color_attr("#€abc"), None);
2887 assert_eq!(parse_font_color_attr("#0f8"), Some((0x00, 0xff, 0x88)));
2888 assert_eq!(parse_font_color_attr("#00ff88"), Some((0x00, 0xff, 0x88)));
2889 }
2890
2891 #[test]
2892 fn repeating_subform_expands_from_data() {
2893 let template = r#"<?xml version="1.0"?>
2894<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
2895 <subform name="form1" layout="tb">
2896 <pageSet>
2897 <pageArea name="Page1">
2898 <contentArea w="595pt" h="842pt"/>
2899 <medium short="595pt" long="842pt"/>
2900 </pageArea>
2901 </pageSet>
2902 <subform name="Orders" layout="tb" w="500pt">
2903 <subform name="Order" layout="position" w="500pt" h="60pt">
2904 <occur min="0" max="10" initial="1"/>
2905 <field name="Item" w="200pt" h="20pt" x="0pt" y="0pt"/>
2906 <field name="Qty" w="100pt" h="20pt" x="200pt" y="0pt"/>
2907 </subform>
2908 </subform>
2909 </subform>
2910</template>"#;
2911
2912 let data_xml = r#"<?xml version="1.0"?>
2913<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
2914 <xfa:data>
2915 <form1>
2916 <Order><Item>Widget A</Item><Qty>5</Qty></Order>
2917 <Order><Item>Widget B</Item><Qty>3</Qty></Order>
2918 <Order><Item>Widget C</Item><Qty>7</Qty></Order>
2919 </form1>
2920 </xfa:data>
2921</xfa:datasets>"#;
2922
2923 let data_dom = DataDom::from_xml(data_xml).unwrap();
2924 let merger = FormMerger::new(&data_dom);
2925 let (tree, _root_id) = merger.merge(template).unwrap();
2926
2927 let orders_id = tree
2928 .nodes
2929 .iter()
2930 .enumerate()
2931 .find(|(_, n)| n.name == "Orders")
2932 .map(|(i, _)| FormNodeId(i))
2933 .unwrap();
2934 let orders = tree.get(orders_id);
2935 assert_eq!(orders.children.len(), 3);
2936 assert!(orders
2937 .children
2938 .iter()
2939 .all(|&id| tree.get(id).name == "Order" && tree.get(id).occur.count() == 1));
2940 }
2941
2942 #[test]
2944 fn repeating_subform_double_wrapped_data() {
2945 let template = r#"<?xml version="1.0"?>
2946<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
2947 <subform name="form1" layout="tb">
2948 <pageSet>
2949 <pageArea name="Page">
2950 <contentArea w="595pt" h="842pt"/>
2951 <medium short="595pt" long="842pt"/>
2952 </pageArea>
2953 </pageSet>
2954 <subform name="Orders" layout="tb" w="559pt">
2955 <subform name="CoreOrders" layout="tb" w="361pt">
2956 <subform name="Order" layout="position" w="360pt" h="162pt">
2957 <occur min="0" max="3" initial="1"/>
2958 <field name="Item" w="200pt" h="25pt" x="9pt" y="9pt"/>
2959 </subform>
2960 </subform>
2961 </subform>
2962 </subform>
2963</template>"#;
2964
2965 let data_xml = concat!(
2966 r#"<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">"#,
2967 r#"<xfa:data><xfa:data xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">"#,
2968 r#"<form1>"#,
2969 r#"<Order><Item>A</Item></Order>"#,
2970 r#"<Order><Item>B</Item></Order>"#,
2971 r#"<Order><Item>C</Item></Order>"#,
2972 r#"</form1>"#,
2973 r#"</xfa:data></xfa:data></xfa:datasets>"#,
2974 );
2975
2976 let data_dom = DataDom::from_xml(data_xml).unwrap();
2977 let merger = FormMerger::new(&data_dom);
2978 let (tree, _root_id) = merger.merge(template).unwrap();
2979
2980 let core_orders_id = tree
2981 .nodes
2982 .iter()
2983 .enumerate()
2984 .find(|(_, n)| n.name == "CoreOrders")
2985 .map(|(i, _)| FormNodeId(i))
2986 .unwrap();
2987 let core_orders = tree.get(core_orders_id);
2988 assert_eq!(
2989 core_orders.children.len(),
2990 3,
2991 "Expected 3 Order instances from double-wrapped data"
2992 );
2993 }
2994
2995 #[test]
2998 fn repeating_subform_bind_ref_resolves_data_name() {
2999 let template = r#"<?xml version="1.0"?>
3000<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3001 <subform name="form" layout="tb">
3002 <pageSet>
3003 <pageArea name="Page1">
3004 <contentArea w="595pt" h="842pt"/>
3005 <medium short="595pt" long="842pt"/>
3006 </pageArea>
3007 </pageSet>
3008 <subform name="ListItems" layout="tb" w="500pt">
3009 <subform name="ItemGroup" layout="tb" w="500pt" h="50pt">
3010 <occur min="0" max="-1"/>
3011 <bind match="dataRef" ref="$.itemGroup[*]"/>
3012 <field name="title" w="200pt" h="20pt" x="0pt" y="0pt"/>
3013 </subform>
3014 </subform>
3015 </subform>
3016</template>"#;
3017
3018 let data_xml = r#"<?xml version="1.0"?>
3019<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
3020 <xfa:data>
3021 <form>
3022 <itemGroup><title>Group A</title></itemGroup>
3023 <itemGroup><title>Group B</title></itemGroup>
3024 </form>
3025 </xfa:data>
3026</xfa:datasets>"#;
3027
3028 let data_dom = DataDom::from_xml(data_xml).unwrap();
3029 let merger = FormMerger::new(&data_dom);
3030 let (tree, _root_id) = merger.merge(template).unwrap();
3031
3032 let list_items = tree
3033 .nodes
3034 .iter()
3035 .find(|n| n.name == "ListItems")
3036 .expect("ListItems must exist");
3037 assert_eq!(
3038 list_items.children.len(),
3039 2,
3040 "Expected 2 instances from bind ref $.itemGroup[*]"
3041 );
3042 }
3043
3044 #[test]
3045 fn repeating_table_rows_expand_as_sibling_rows() {
3046 let template = r#"<?xml version="1.0"?>
3047<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3048 <subform name="form1" layout="tb">
3049 <pageSet>
3050 <pageArea name="Page1">
3051 <contentArea w="595pt" h="842pt"/>
3052 <medium short="595pt" long="842pt"/>
3053 </pageArea>
3054 </pageSet>
3055 <subform name="Table1" layout="table" columnWidths="100pt 100pt 100pt">
3056 <subform name="HeaderRow" layout="row">
3057 <draw name="H1" w="100pt" h="20pt"/>
3058 <draw name="H2" w="100pt" h="20pt"/>
3059 <draw name="H3" w="100pt" h="20pt"/>
3060 </subform>
3061 <subform name="Row1" layout="row">
3062 <occur min="0" max="-1"/>
3063 <field name="Cell1" w="100pt" h="20pt"/>
3064 <field name="Cell2" w="100pt" h="20pt"/>
3065 <field name="Cell3" w="100pt" h="20pt"/>
3066 </subform>
3067 </subform>
3068 </subform>
3069</template>"#;
3070
3071 let data_xml = r#"<?xml version="1.0"?>
3072<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
3073 <xfa:data>
3074 <form1>
3075 <Table1>
3076 <Row1><Cell1>A1</Cell1><Cell2>B1</Cell2><Cell3>C1</Cell3></Row1>
3077 <Row1><Cell1>A2</Cell1><Cell2>B2</Cell2><Cell3>C2</Cell3></Row1>
3078 <Row1><Cell1>A3</Cell1><Cell2>B3</Cell2><Cell3>C3</Cell3></Row1>
3079 <Row1><Cell1>A4</Cell1><Cell2>B4</Cell2><Cell3>C4</Cell3></Row1>
3080 </Table1>
3081 </form1>
3082 </xfa:data>
3083</xfa:datasets>"#;
3084
3085 let data_dom = DataDom::from_xml(data_xml).unwrap();
3086 let merger = FormMerger::new(&data_dom);
3087 let (tree, _root_id) = merger.merge(template).unwrap();
3088
3089 let table_id = tree
3090 .nodes
3091 .iter()
3092 .enumerate()
3093 .find(|(_, n)| n.name == "Table1")
3094 .map(|(i, _)| FormNodeId(i))
3095 .unwrap();
3096 let table = tree.get(table_id);
3097 assert_eq!(table.children.len(), 5, "header + 4 repeated data rows");
3098 assert_eq!(tree.get(table.children[0]).name, "HeaderRow");
3099 assert!(table.children[1..]
3100 .iter()
3101 .all(|&id| tree.get(id).name == "Row1" && tree.get(id).children.len() == 3));
3102 }
3103
3104 #[test]
3105 fn repeating_subform_clamps_to_occur_max() {
3106 let template = r#"<?xml version="1.0"?>
3107<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3108 <subform name="form1" layout="tb">
3109 <subform name="Orders" layout="tb">
3110 <subform name="Order" layout="position" w="200pt" h="20pt">
3111 <occur min="0" max="2"/>
3112 <field name="Item" w="100pt" h="20pt"/>
3113 </subform>
3114 </subform>
3115 </subform>
3116</template>"#;
3117
3118 let data_xml = r#"<?xml version="1.0"?>
3119<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
3120 <xfa:data>
3121 <form1>
3122 <Order><Item>A</Item></Order>
3123 <Order><Item>B</Item></Order>
3124 <Order><Item>C</Item></Order>
3125 <Order><Item>D</Item></Order>
3126 <Order><Item>E</Item></Order>
3127 </form1>
3128 </xfa:data>
3129</xfa:datasets>"#;
3130
3131 let data_dom = DataDom::from_xml(data_xml).unwrap();
3132 let merger = FormMerger::new(&data_dom);
3133 let (tree, _root_id) = merger.merge(template).unwrap();
3134
3135 let orders_id = tree
3136 .nodes
3137 .iter()
3138 .enumerate()
3139 .find(|(_, n)| n.name == "Orders")
3140 .map(|(i, _)| FormNodeId(i))
3141 .unwrap();
3142 assert_eq!(tree.get(orders_id).children.len(), 2);
3143 }
3144
3145 #[test]
3146 fn repeating_subform_unbounded_uses_data_count() {
3147 let template = r#"<?xml version="1.0"?>
3148<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3149 <subform name="form1" layout="tb">
3150 <subform name="Orders" layout="tb">
3151 <subform name="Order" layout="position" w="200pt" h="20pt">
3152 <occur min="0" max="-1"/>
3153 <field name="Item" w="100pt" h="20pt"/>
3154 </subform>
3155 </subform>
3156 </subform>
3157</template>"#;
3158
3159 let data_xml = r#"<?xml version="1.0"?>
3160<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
3161 <xfa:data>
3162 <form1>
3163 <Order><Item>A</Item></Order>
3164 <Order><Item>B</Item></Order>
3165 <Order><Item>C</Item></Order>
3166 </form1>
3167 </xfa:data>
3168</xfa:datasets>"#;
3169
3170 let data_dom = DataDom::from_xml(data_xml).unwrap();
3171 let merger = FormMerger::new(&data_dom);
3172 let (tree, _root_id) = merger.merge(template).unwrap();
3173
3174 let orders_id = tree
3175 .nodes
3176 .iter()
3177 .enumerate()
3178 .find(|(_, n)| n.name == "Orders")
3179 .map(|(i, _)| FormNodeId(i))
3180 .unwrap();
3181 assert_eq!(tree.get(orders_id).children.len(), 3);
3182 }
3183
3184 #[test]
3185 fn repeating_subform_respects_occur_min() {
3186 let template = r#"<?xml version="1.0"?>
3187<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3188 <subform name="form1" layout="tb">
3189 <subform name="Orders" layout="tb">
3190 <subform name="Order" layout="position" w="200pt" h="20pt">
3191 <occur min="2" max="-1"/>
3192 <field name="Item" w="100pt" h="20pt"/>
3193 </subform>
3194 </subform>
3195 </subform>
3196</template>"#;
3197
3198 let data_xml = r#"<?xml version="1.0"?>
3199<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
3200 <xfa:data>
3201 <form1/>
3202 </xfa:data>
3203</xfa:datasets>"#;
3204
3205 let data_dom = DataDom::from_xml(data_xml).unwrap();
3206 let merger = FormMerger::new(&data_dom);
3207 let (tree, _root_id) = merger.merge(template).unwrap();
3208
3209 let orders_id = tree
3210 .nodes
3211 .iter()
3212 .enumerate()
3213 .find(|(_, n)| n.name == "Orders")
3214 .map(|(i, _)| FormNodeId(i))
3215 .unwrap();
3216 let orders = tree.get(orders_id);
3217 assert_eq!(orders.children.len(), 2);
3218 assert!(orders
3219 .children
3220 .iter()
3221 .all(|&id| tree.get(id).name == "Order"));
3222 }
3223
3224 #[test]
3226 fn root_subform_binds_to_matching_data_root() {
3227 let template = r#"<?xml version="1.0"?>
3228<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3229 <subform name="form" layout="tb">
3230 <pageSet>
3231 <pageArea name="Page1">
3232 <contentArea w="595pt" h="842pt"/>
3233 <medium short="595pt" long="842pt"/>
3234 </pageArea>
3235 </pageSet>
3236 <field name="title" w="200pt" h="20pt" x="0pt" y="0pt"/>
3237 <field name="code" w="200pt" h="20pt" x="0pt" y="20pt"/>
3238 </subform>
3239</template>"#;
3240
3241 let data_xml = r#"<?xml version="1.0"?>
3242<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
3243 <xfa:data>
3244 <form>
3245 <title>Hello World</title>
3246 <code>42</code>
3247 </form>
3248 </xfa:data>
3249</xfa:datasets>"#;
3250
3251 let data_dom = DataDom::from_xml(data_xml).unwrap();
3252 let merger = FormMerger::new(&data_dom);
3253 let (tree, _root_id) = merger.merge(template).unwrap();
3254
3255 let title_node = tree
3256 .nodes
3257 .iter()
3258 .find(|n| n.name == "title")
3259 .expect("title field must exist");
3260 match &title_node.node_type {
3261 FormNodeType::Field { value } => {
3262 assert_eq!(value, "Hello World", "title should bind to data root");
3263 }
3264 _ => panic!("title should be a field"),
3265 }
3266 }
3267
3268 #[test]
3269 fn check_button_mark_parsed_into_style() {
3270 let template = r#"<?xml version="1.0"?>
3271<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3272 <subform name="form" layout="tb">
3273 <pageSet>
3274 <pageArea name="Page1">
3275 <contentArea w="595pt" h="842pt"/>
3276 <medium short="595pt" long="842pt"/>
3277 </pageArea>
3278 </pageSet>
3279 <field name="agree" w="20pt" h="20pt">
3280 <ui><checkButton mark="circle"/></ui>
3281 <value><text>1</text></value>
3282 <items><text>1</text><text>0</text><text>2</text></items>
3283 </field>
3284 </subform>
3285</template>"#;
3286
3287 let data_xml = r#"<?xml version="1.0"?>
3288<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
3289 <xfa:data>
3290 <form>
3291 <agree>1</agree>
3292 </form>
3293 </xfa:data>
3294</xfa:datasets>"#;
3295
3296 let data_dom = DataDom::from_xml(data_xml).unwrap();
3297 let merger = FormMerger::new(&data_dom);
3298 let (tree, _root_id) = merger.merge(template).unwrap();
3299
3300 let agree_id = tree
3301 .nodes
3302 .iter()
3303 .enumerate()
3304 .find(|(_, n)| n.name == "agree")
3305 .map(|(i, _)| FormNodeId(i))
3306 .expect("agree field must exist");
3307 let meta = tree.meta(agree_id);
3308 assert_eq!(meta.field_kind, FieldKind::Checkbox);
3309 assert_eq!(meta.style.check_button_mark.as_deref(), Some("circle"));
3310 assert_eq!(meta.style.check_button_on_value.as_deref(), Some("1"));
3311 assert_eq!(meta.style.check_button_off_value.as_deref(), Some("0"));
3312 assert_eq!(meta.style.check_button_neutral_value.as_deref(), Some("2"));
3313 }
3314
3315 #[test]
3316 fn nested_check_button_items_are_used_for_checkbox_values() {
3317 let template = r#"<?xml version="1.0"?>
3318<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3319 <subform name="form" layout="tb">
3320 <pageSet>
3321 <pageArea name="Page1">
3322 <contentArea w="595pt" h="842pt"/>
3323 <medium short="595pt" long="842pt"/>
3324 </pageArea>
3325 </pageSet>
3326 <field name="agree" w="20pt" h="20pt">
3327 <ui>
3328 <checkButton>
3329 <items><text>Yes</text><text>No</text></items>
3330 </checkButton>
3331 </ui>
3332 <value><text>Yes</text></value>
3333 </field>
3334 </subform>
3335</template>"#;
3336
3337 let data_xml = r#"<?xml version="1.0"?>
3338<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
3339 <xfa:data>
3340 <form>
3341 <agree>Yes</agree>
3342 </form>
3343 </xfa:data>
3344</xfa:datasets>"#;
3345
3346 let data_dom = DataDom::from_xml(data_xml).unwrap();
3347 let merger = FormMerger::new(&data_dom);
3348 let (tree, _root_id) = merger.merge(template).unwrap();
3349
3350 let agree_id = tree
3351 .nodes
3352 .iter()
3353 .enumerate()
3354 .find(|(_, n)| n.name == "agree")
3355 .map(|(i, _)| FormNodeId(i))
3356 .expect("agree field must exist");
3357 let meta = tree.meta(agree_id);
3358 assert_eq!(meta.field_kind, FieldKind::Checkbox);
3359 assert_eq!(meta.style.check_button_on_value.as_deref(), Some("Yes"));
3360 assert_eq!(meta.style.check_button_off_value.as_deref(), Some("No"));
3361 match &tree.get(agree_id).node_type {
3362 FormNodeType::Field { value } => assert_eq!(value, "Yes"),
3363 _ => panic!("agree should be a field"),
3364 }
3365 }
3366
3367 #[test]
3368 fn border_fill_solid_color_populates_background_color() {
3369 let template = r#"<?xml version="1.0"?>
3370<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3371 <subform name="form" layout="tb">
3372 <pageSet>
3373 <pageArea name="Page1">
3374 <contentArea w="595pt" h="842pt"/>
3375 <medium short="595pt" long="842pt"/>
3376 </pageArea>
3377 </pageSet>
3378 <field name="name" w="200pt" h="20pt">
3379 <ui><textEdit/></ui>
3380 <border>
3381 <fill>
3382 <solid>
3383 <color value="240,240,240"/>
3384 </solid>
3385 </fill>
3386 </border>
3387 </field>
3388 </subform>
3389</template>"#;
3390
3391 let data_xml = r#"<?xml version="1.0"?>
3392<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
3393 <xfa:data>
3394 <form>
3395 <name>Jane Doe</name>
3396 </form>
3397 </xfa:data>
3398</xfa:datasets>"#;
3399
3400 let data_dom = DataDom::from_xml(data_xml).unwrap();
3401 let merger = FormMerger::new(&data_dom);
3402 let (tree, _root_id) = merger.merge(template).unwrap();
3403
3404 let name_id = tree
3405 .nodes
3406 .iter()
3407 .enumerate()
3408 .find(|(_, n)| n.name == "name")
3409 .map(|(i, _)| FormNodeId(i))
3410 .expect("name field must exist");
3411 assert_eq!(tree.meta(name_id).style.bg_color, Some((240, 240, 240)));
3412 }
3413
3414 #[test]
3415 fn border_fill_direct_color_populates_button_bg_color() {
3416 let template = r#"<?xml version="1.0"?>
3420<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3421 <subform name="form" layout="tb">
3422 <pageSet>
3423 <pageArea name="Page1">
3424 <contentArea w="595pt" h="842pt"/>
3425 <medium short="595pt" long="842pt"/>
3426 </pageArea>
3427 </pageSet>
3428 <field name="Btn" w="25.4mm" h="9.525mm">
3429 <ui><button highlight="inverted"/></ui>
3430 <caption><value><text>E-Mail</text></value></caption>
3431 <border hand="right">
3432 <edge stroke="raised"/>
3433 <fill><color value="255,255,153"/></fill>
3434 </border>
3435 </field>
3436 </subform>
3437</template>"#;
3438 let data_xml = r#"<?xml version="1.0"?>
3439<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
3440 <xfa:data><form/></xfa:data>
3441</xfa:datasets>"#;
3442 let data_dom = DataDom::from_xml(data_xml).unwrap();
3443 let merger = FormMerger::new(&data_dom);
3444 let (tree, _root_id) = merger.merge(template).unwrap();
3445 let btn_id = tree
3446 .nodes
3447 .iter()
3448 .enumerate()
3449 .find(|(_, n)| n.name == "Btn")
3450 .map(|(i, _)| FormNodeId(i))
3451 .expect("button field must exist");
3452 assert_eq!(tree.meta(btn_id).style.bg_color, Some((255, 255, 153)));
3453 }
3454
3455 #[test]
3456 fn rich_text_exdata_captions_are_collapsed_to_plain_text() {
3457 let template = r#"<?xml version="1.0"?>
3458<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3459 <subform name="form" layout="tb">
3460 <pageSet>
3461 <pageArea name="Page1">
3462 <contentArea w="595pt" h="842pt"/>
3463 <medium short="595pt" long="842pt"/>
3464 </pageArea>
3465 </pageSet>
3466 <field name="patient_name" w="200pt" h="20pt">
3467 <ui><textEdit/></ui>
3468 <caption placement="top" reserve="0.230972in">
3469 <value>
3470 <exData contentType="text/html" maxLength="0">
3471 <body xmlns="http://www.w3.org/1999/xhtml"
3472 xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"
3473 xfa:APIVersion="1.4.4136.0">
3474 <p>NAME<span style="xfa-spacerun:yes"> </span><span style="font-style:italic">(Last, First, Middle Initial)</span></p>
3475 </body>
3476 </exData>
3477 </value>
3478 </caption>
3479 </field>
3480 <field name="telephone" w="200pt" h="20pt">
3481 <ui><textEdit/></ui>
3482 <caption placement="top" reserve="0.230972in">
3483 <value>
3484 <exData contentType="text/html" maxLength="0">
3485 <body xmlns="http://www.w3.org/1999/xhtml"
3486 xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"
3487 xfa:APIVersion="1.4.4136.0">
3488 <p>TELEPHONE<span style="font-style:italic"> (Include area code)</span></p>
3489 </body>
3490 </exData>
3491 </value>
3492 </caption>
3493 </field>
3494 </subform>
3495</template>"#;
3496
3497 let data_dom = DataDom::new();
3498 let merger = FormMerger::new(&data_dom);
3499 let (tree, _root_id) = merger.merge(template).unwrap();
3500
3501 let patient_name_id = tree
3502 .nodes
3503 .iter()
3504 .enumerate()
3505 .find(|(_, n)| n.name == "patient_name")
3506 .map(|(i, _)| FormNodeId(i))
3507 .expect("patient_name field must exist");
3508 let patient_name = tree.meta(patient_name_id);
3509 assert_eq!(
3510 patient_name.style.caption_text.as_deref(),
3511 Some("NAME (Last, First, Middle Initial)")
3512 );
3513 assert_eq!(patient_name.style.caption_placement.as_deref(), Some("top"));
3514 assert_eq!(
3515 tree.get(patient_name_id)
3516 .box_model
3517 .caption
3518 .as_ref()
3519 .map(|caption| caption.text.as_str()),
3520 Some("NAME (Last, First, Middle Initial)")
3521 );
3522
3523 let telephone_id = tree
3524 .nodes
3525 .iter()
3526 .enumerate()
3527 .find(|(_, n)| n.name == "telephone")
3528 .map(|(i, _)| FormNodeId(i))
3529 .expect("telephone field must exist");
3530 let telephone = tree.meta(telephone_id);
3531 assert_eq!(
3532 telephone.style.caption_text.as_deref(),
3533 Some("TELEPHONE (Include area code)")
3534 );
3535 assert_eq!(telephone.style.caption_placement.as_deref(), Some("top"));
3536 assert_eq!(
3537 tree.get(telephone_id)
3538 .box_model
3539 .caption
3540 .as_ref()
3541 .map(|caption| caption.text.as_str()),
3542 Some("TELEPHONE (Include area code)")
3543 );
3544 }
3545
3546 #[test]
3547 fn border_edge_order_keeps_bottom_only_underline() {
3548 let template = r#"<?xml version="1.0"?>
3549<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3550 <subform name="form" layout="tb">
3551 <pageSet>
3552 <pageArea name="Page1">
3553 <contentArea w="595pt" h="842pt"/>
3554 <medium short="595pt" long="842pt"/>
3555 </pageArea>
3556 </pageSet>
3557 <field name="underlined" w="200pt" h="20pt">
3558 <ui><textEdit/></ui>
3559 <border>
3560 <edge stroke="none" thickness="1pt"/>
3561 <edge stroke="none" thickness="2pt"/>
3562 <edge stroke="solid" thickness="3pt"/>
3563 <edge stroke="none" thickness="4pt"/>
3564 </border>
3565 </field>
3566 </subform>
3567</template>"#;
3568
3569 let data_dom = DataDom::new();
3570 let merger = FormMerger::new(&data_dom);
3571 let (tree, _root_id) = merger.merge(template).unwrap();
3572
3573 let underlined_id = tree
3574 .nodes
3575 .iter()
3576 .enumerate()
3577 .find(|(_, n)| n.name == "underlined")
3578 .map(|(i, _)| FormNodeId(i))
3579 .expect("underlined field must exist");
3580 let style = &tree.meta(underlined_id).style;
3581
3582 assert_eq!(style.border_edges, [false, false, true, false]);
3584 assert_eq!(style.border_widths, Some([1.0, 2.0, 3.0, 4.0]));
3585 assert_eq!(style.border_width_pt, Some(3.0));
3586 }
3587
3588 #[test]
3589 fn round_check_button_parsed_as_radio() {
3590 let template = r#"<?xml version="1.0"?>
3591<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3592 <subform name="form" layout="tb">
3593 <pageSet>
3594 <pageArea name="Page1">
3595 <contentArea w="595pt" h="842pt"/>
3596 <medium short="595pt" long="842pt"/>
3597 </pageArea>
3598 </pageSet>
3599 <field name="choice" w="20pt" h="20pt">
3600 <ui><checkButton shape="round"/></ui>
3601 <items><text>Y</text><text>N</text></items>
3602 </field>
3603 </subform>
3604</template>"#;
3605
3606 let data_dom = DataDom::new();
3607 let merger = FormMerger::new(&data_dom);
3608 let (tree, _root_id) = merger.merge(template).unwrap();
3609
3610 let choice_id = tree
3611 .nodes
3612 .iter()
3613 .enumerate()
3614 .find(|(_, n)| n.name == "choice")
3615 .map(|(i, _)| FormNodeId(i))
3616 .expect("choice field must exist");
3617 assert_eq!(tree.meta(choice_id).field_kind, FieldKind::Radio);
3618 }
3619
3620 #[test]
3621 fn excl_group_value_selects_matching_child() {
3622 let template = r#"<?xml version="1.0"?>
3623<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3624 <subform name="form" layout="tb">
3625 <pageSet>
3626 <pageArea name="Page1">
3627 <contentArea w="595pt" h="842pt"/>
3628 <medium short="595pt" long="842pt"/>
3629 </pageArea>
3630 </pageSet>
3631 <exclGroup name="choice">
3632 <field name="yes" w="20pt" h="20pt">
3633 <ui><checkButton shape="round"/></ui>
3634 <items><text>Y</text><text>N</text></items>
3635 </field>
3636 <field name="no" w="20pt" h="20pt">
3637 <ui><checkButton shape="round"/></ui>
3638 <items><text>N</text><text>Y</text></items>
3639 </field>
3640 </exclGroup>
3641 </subform>
3642</template>"#;
3643
3644 let data_xml = r#"<?xml version="1.0"?>
3645<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
3646 <xfa:data>
3647 <form>
3648 <choice>Y</choice>
3649 </form>
3650 </xfa:data>
3651</xfa:datasets>"#;
3652
3653 let data_dom = DataDom::from_xml(data_xml).unwrap();
3654 let merger = FormMerger::new(&data_dom);
3655 let (tree, _root_id) = merger.merge(template).unwrap();
3656
3657 let yes_node = tree
3658 .nodes
3659 .iter()
3660 .find(|n| n.name == "yes")
3661 .expect("yes field must exist");
3662 let no_node = tree
3663 .nodes
3664 .iter()
3665 .find(|n| n.name == "no")
3666 .expect("no field must exist");
3667
3668 match (&yes_node.node_type, &no_node.node_type) {
3669 (FormNodeType::Field { value: yes }, FormNodeType::Field { value: no }) => {
3670 assert_eq!(yes, "Y");
3671 assert!(
3672 no.is_empty(),
3673 "non-selected exclGroup child should be cleared"
3674 );
3675 }
3676 _ => panic!("exclGroup children must be fields"),
3677 }
3678 }
3679
3680 #[test]
3681 fn draw_with_image_creates_image_node() {
3682 let image_b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
3684 let template = format!(
3685 r#"<?xml version="1.0"?>
3686<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3687 <subform name="form1" layout="tb">
3688 <pageSet>
3689 <pageArea name="Page1">
3690 <contentArea w="595pt" h="842pt"/>
3691 <medium short="595pt" long="842pt"/>
3692 </pageArea>
3693 </pageSet>
3694 <draw name="logo" x="0pt" y="0pt" w="100pt" h="100pt">
3695 <value>
3696 <image contentType="image/png">{}</image>
3697 </value>
3698 </draw>
3699 </subform>
3700</template>"#,
3701 image_b64
3702 );
3703
3704 let data_dom = DataDom::new();
3705 let merger = FormMerger::new(&data_dom);
3706 let (tree, _root_id) = merger.merge(&template).unwrap();
3707
3708 let logo_node = tree
3709 .nodes
3710 .iter()
3711 .find(|n| n.name == "logo")
3712 .expect("logo draw must exist");
3713
3714 match &logo_node.node_type {
3715 FormNodeType::Image { data, mime_type } => {
3716 assert_eq!(mime_type, "image/png");
3717 assert!(!data.is_empty(), "image data should not be empty");
3718 assert!(
3720 data.starts_with(&[0x89, 0x50, 0x4E, 0x47]),
3721 "decoded data should be PNG"
3722 );
3723 }
3724 _ => panic!(
3725 "draw with image should create Image node, got {:?}",
3726 logo_node.node_type
3727 ),
3728 }
3729 }
3730
3731 #[test]
3732 fn draw_with_rectangle_creates_draw_node() {
3733 let template = r#"<?xml version="1.0"?>
3734<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3735 <subform name="form1" layout="tb">
3736 <pageSet>
3737 <pageArea name="Page1">
3738 <contentArea w="595pt" h="842pt"/>
3739 <medium short="595pt" long="842pt"/>
3740 </pageArea>
3741 </pageSet>
3742 <draw name="box" x="0pt" y="0pt" w="100pt" h="100pt">
3743 <value>
3744 <rectangle/>
3745 </value>
3746 </draw>
3747 </subform>
3748</template>"#;
3749
3750 let data_dom = DataDom::new();
3751 let merger = FormMerger::new(&data_dom);
3752 let (tree, _root_id) = merger.merge(template).unwrap();
3753
3754 let box_node = tree
3755 .nodes
3756 .iter()
3757 .find(|n| n.name == "box")
3758 .expect("box draw must exist");
3759
3760 match &box_node.node_type {
3761 FormNodeType::Draw(DrawContent::Rectangle { .. }) => {}
3762 other => panic!(
3763 "draw with rectangle should create Rectangle draw, got {:?}",
3764 other
3765 ),
3766 }
3767 }
3768
3769 #[test]
3770 fn field_anchor_type_is_preserved_in_merged_meta() {
3771 let template = r#"<?xml version="1.0"?>
3772<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3773 <subform name="form1" layout="tb">
3774 <pageSet>
3775 <pageArea name="Page1">
3776 <contentArea w="595pt" h="842pt"/>
3777 <medium short="595pt" long="842pt"/>
3778 </pageArea>
3779 </pageSet>
3780 <field name="centeredBox" layout="position" anchorType="middleCenter" w="20pt" h="10pt" x="100pt" y="50pt"/>
3781 </subform>
3782</template>"#;
3783
3784 let data_dom = DataDom::new();
3785 let merger = FormMerger::new(&data_dom);
3786 let (tree, _root_id) = merger.merge(template).unwrap();
3787
3788 let centered_id = tree
3789 .nodes
3790 .iter()
3791 .enumerate()
3792 .find(|(_, n)| n.name == "centeredBox")
3793 .map(|(i, _)| FormNodeId(i))
3794 .expect("centeredBox field must exist");
3795
3796 assert_eq!(tree.meta(centered_id).anchor_type, AnchorType::MiddleCenter);
3797 }
3798
3799 #[test]
3800 fn area_without_layout_uses_positioned_when_children_have_coordinates() {
3801 let template = r#"<?xml version="1.0"?>
3802<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3803 <subform name="form1" layout="tb">
3804 <pageSet>
3805 <pageArea name="Page1">
3806 <contentArea w="595pt" h="842pt"/>
3807 <medium short="595pt" long="842pt"/>
3808 </pageArea>
3809 </pageSet>
3810 <area name="Nagl">
3811 <draw name="headerLine" x="10pt" y="12pt" w="100pt" h="1pt">
3812 <value><rectangle/></value>
3813 </draw>
3814 </area>
3815 </subform>
3816</template>"#;
3817
3818 let data_dom = DataDom::new();
3819 let merger = FormMerger::new(&data_dom);
3820 let (tree, _root_id) = merger.merge(template).unwrap();
3821
3822 let nagl = tree
3823 .nodes
3824 .iter()
3825 .find(|n| n.name == "Nagl")
3826 .expect("Nagl area must exist");
3827
3828 assert_eq!(nagl.layout, LayoutStrategy::Positioned);
3829 }
3830
3831 #[test]
3835 fn merge_mode_defaults_to_consume_data() {
3836 let template = r#"<?xml version="1.0"?>
3837<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3838 <subform name="form1" layout="tb"/>
3839</template>"#;
3840 assert_eq!(detect_merge_mode(template), MergeMode::ConsumeData);
3841 }
3842
3843 #[test]
3845 fn merge_mode_detects_match_template() {
3846 let template = r#"<?xml version="1.0"?>
3847<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3848 <!-- config: matchTemplate -->
3849 <subform name="form1" layout="tb"/>
3850</template>"#;
3851 assert_eq!(detect_merge_mode(template), MergeMode::MatchTemplate);
3852 }
3853
3854 #[test]
3857 fn match_template_binds_flat_data_to_named_fields() {
3858 let template = r#"<?xml version="1.0"?>
3860<!-- matchTemplate -->
3861<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3862 <subform name="form1" layout="tb">
3863 <pageSet>
3864 <pageArea name="Page1">
3865 <contentArea w="595pt" h="842pt"/>
3866 <medium short="595pt" long="842pt"/>
3867 </pageArea>
3868 </pageSet>
3869 <subform name="section" layout="tb">
3870 <field name="firstName" w="200pt" h="20pt" x="0pt" y="0pt"/>
3871 <field name="lastName" w="200pt" h="20pt" x="0pt" y="20pt"/>
3872 </subform>
3873 </subform>
3874</template>"#;
3875
3876 let data_xml = r#"<?xml version="1.0"?>
3878<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
3879 <xfa:data>
3880 <form1>
3881 <firstName>John</firstName>
3882 <lastName>Doe</lastName>
3883 </form1>
3884 </xfa:data>
3885</xfa:datasets>"#;
3886
3887 let data_dom = DataDom::from_xml(data_xml).unwrap();
3888 let merger = FormMerger::new(&data_dom);
3889 let (tree, _root_id) = merger.merge(template).unwrap();
3890
3891 let first = tree
3892 .nodes
3893 .iter()
3894 .find(|n| n.name == "firstName")
3895 .expect("firstName must exist");
3896 let last = tree
3897 .nodes
3898 .iter()
3899 .find(|n| n.name == "lastName")
3900 .expect("lastName must exist");
3901
3902 match &first.node_type {
3903 FormNodeType::Field { value } => assert_eq!(value, "John"),
3904 _ => panic!("firstName must be a field"),
3905 }
3906 match &last.node_type {
3907 FormNodeType::Field { value } => assert_eq!(value, "Doe"),
3908 _ => panic!("lastName must be a field"),
3909 }
3910 }
3911
3912 #[test]
3917 fn field_binds_via_sibling_scope() {
3918 let template = r#"<?xml version="1.0"?>
3919<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3920 <subform name="form1" layout="tb">
3921 <pageSet>
3922 <pageArea name="Page1">
3923 <contentArea w="595pt" h="842pt"/>
3924 <medium short="595pt" long="842pt"/>
3925 </pageArea>
3926 </pageSet>
3927 <subform name="address" layout="tb">
3928 <subform name="details" layout="tb">
3929 <field name="city" w="200pt" h="20pt" x="0pt" y="0pt"/>
3930 </subform>
3931 </subform>
3932 </subform>
3933</template>"#;
3934
3935 let data_xml = r#"<?xml version="1.0"?>
3937<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
3938 <xfa:data>
3939 <form1>
3940 <address>
3941 <city>Rotterdam</city>
3942 <details/>
3943 </address>
3944 </form1>
3945 </xfa:data>
3946</xfa:datasets>"#;
3947
3948 let data_dom = DataDom::from_xml(data_xml).unwrap();
3949 let merger = FormMerger::new(&data_dom);
3950 let (tree, _root_id) = merger.merge(template).unwrap();
3951
3952 let city = tree
3953 .nodes
3954 .iter()
3955 .find(|n| n.name == "city")
3956 .expect("city field must exist");
3957 match &city.node_type {
3958 FormNodeType::Field { value } => assert_eq!(
3959 value, "Rotterdam",
3960 "city should be found via sibling scope resolution"
3961 ),
3962 _ => panic!("city should be a field"),
3963 }
3964 }
3965
3966 #[test]
3971 fn transparent_subform_passes_data_context_through() {
3972 let template = r#"<?xml version="1.0"?>
3973<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
3974 <subform name="form1" layout="tb">
3975 <pageSet>
3976 <pageArea name="Page1">
3977 <contentArea w="595pt" h="842pt"/>
3978 <medium short="595pt" long="842pt"/>
3979 </pageArea>
3980 </pageSet>
3981 <subform layout="tb">
3982 <field name="country" w="200pt" h="20pt" x="0pt" y="0pt"/>
3983 </subform>
3984 </subform>
3985</template>"#;
3986
3987 let data_xml = r#"<?xml version="1.0"?>
3989<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
3990 <xfa:data>
3991 <form1>
3992 <country>Netherlands</country>
3993 </form1>
3994 </xfa:data>
3995</xfa:datasets>"#;
3996
3997 let data_dom = DataDom::from_xml(data_xml).unwrap();
3998 let merger = FormMerger::new(&data_dom);
3999 let (tree, _root_id) = merger.merge(template).unwrap();
4000
4001 let country = tree
4002 .nodes
4003 .iter()
4004 .find(|n| n.name == "country")
4005 .expect("country field must exist");
4006 match &country.node_type {
4007 FormNodeType::Field { value } => assert_eq!(
4008 value, "Netherlands",
4009 "transparent subform should pass context through"
4010 ),
4011 _ => panic!("country should be a field"),
4012 }
4013 }
4014
4015 #[test]
4017 fn nested_transparent_subforms_bind_correctly() {
4018 let template = r#"<?xml version="1.0"?>
4019<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
4020 <subform name="form1" layout="tb">
4021 <pageSet>
4022 <pageArea name="Page1">
4023 <contentArea w="595pt" h="842pt"/>
4024 <medium short="595pt" long="842pt"/>
4025 </pageArea>
4026 </pageSet>
4027 <subform layout="tb">
4028 <subform layout="tb">
4029 <field name="postalCode" w="100pt" h="20pt" x="0pt" y="0pt"/>
4030 </subform>
4031 </subform>
4032 </subform>
4033</template>"#;
4034
4035 let data_xml = r#"<?xml version="1.0"?>
4036<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
4037 <xfa:data>
4038 <form1>
4039 <postalCode>1234AB</postalCode>
4040 </form1>
4041 </xfa:data>
4042</xfa:datasets>"#;
4043
4044 let data_dom = DataDom::from_xml(data_xml).unwrap();
4045 let merger = FormMerger::new(&data_dom);
4046 let (tree, _root_id) = merger.merge(template).unwrap();
4047
4048 let postal = tree
4049 .nodes
4050 .iter()
4051 .find(|n| n.name == "postalCode")
4052 .expect("postalCode field must exist");
4053 match &postal.node_type {
4054 FormNodeType::Field { value } => assert_eq!(value, "1234AB"),
4055 _ => panic!("postalCode should be a field"),
4056 }
4057 }
4058
4059 #[test]
4065 fn repeating_subform_three_items_creates_three_instances() {
4066 let template = r#"<?xml version="1.0"?>
4067<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
4068 <subform name="form1" layout="tb">
4069 <pageSet>
4070 <pageArea name="Page1">
4071 <contentArea w="595pt" h="842pt"/>
4072 <medium short="595pt" long="842pt"/>
4073 </pageArea>
4074 </pageSet>
4075 <subform name="row" layout="position" w="500pt" h="30pt">
4076 <occur min="0" max="-1" initial="0"/>
4077 <field name="label" w="200pt" h="20pt" x="0pt" y="0pt"/>
4078 </subform>
4079 </subform>
4080</template>"#;
4081
4082 let data_xml = r#"<?xml version="1.0"?>
4083<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
4084 <xfa:data>
4085 <form1>
4086 <row><label>Alpha</label></row>
4087 <row><label>Beta</label></row>
4088 <row><label>Gamma</label></row>
4089 </form1>
4090 </xfa:data>
4091</xfa:datasets>"#;
4092
4093 let data_dom = DataDom::from_xml(data_xml).unwrap();
4094 let merger = FormMerger::new(&data_dom);
4095 let (tree, _root_id) = merger.merge(template).unwrap();
4096
4097 let rows: Vec<_> = tree.nodes.iter().filter(|n| n.name == "row").collect();
4098 assert_eq!(rows.len(), 3, "must have exactly 3 row instances");
4099 }
4100
4101 #[test]
4104 fn occur_min1_max1_limits_to_single_instance() {
4105 let template = r#"<?xml version="1.0"?>
4106<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
4107 <subform name="form1" layout="tb">
4108 <pageSet>
4109 <pageArea name="Page1">
4110 <contentArea w="595pt" h="842pt"/>
4111 <medium short="595pt" long="842pt"/>
4112 </pageArea>
4113 </pageSet>
4114 <subform name="item" layout="position" w="500pt" h="30pt">
4115 <occur min="1" max="1"/>
4116 <field name="val" w="100pt" h="20pt" x="0pt" y="0pt"/>
4117 </subform>
4118 </subform>
4119</template>"#;
4120
4121 let data_xml = r#"<?xml version="1.0"?>
4122<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
4123 <xfa:data>
4124 <form1>
4125 <item><val>X</val></item>
4126 <item><val>Y</val></item>
4127 <item><val>Z</val></item>
4128 </form1>
4129 </xfa:data>
4130</xfa:datasets>"#;
4131
4132 let data_dom = DataDom::from_xml(data_xml).unwrap();
4133 let merger = FormMerger::new(&data_dom);
4134 let (tree, _root_id) = merger.merge(template).unwrap();
4135
4136 let items: Vec<_> = tree.nodes.iter().filter(|n| n.name == "item").collect();
4137 assert_eq!(items.len(), 1, "occur max=1 must limit to one instance");
4138 }
4139
4140 #[test]
4145 fn field_with_presence_hidden_stays_hidden() {
4146 let template = r#"<?xml version="1.0"?>
4147<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
4148 <subform name="form1" layout="tb">
4149 <pageSet>
4150 <pageArea name="Page1">
4151 <contentArea w="595pt" h="842pt"/>
4152 <medium short="595pt" long="842pt"/>
4153 </pageArea>
4154 </pageSet>
4155 <field name="secretField" presence="hidden" w="100pt" h="20pt" x="0pt" y="0pt"/>
4156 </subform>
4157</template>"#;
4158
4159 let data_dom = DataDom::new();
4160 let merger = FormMerger::new(&data_dom);
4161 let (tree, _root_id) = merger.merge(template).unwrap();
4162
4163 let secret_id = tree
4164 .nodes
4165 .iter()
4166 .enumerate()
4167 .find(|(_, n)| n.name == "secretField")
4168 .map(|(i, _)| FormNodeId(i))
4169 .expect("secretField must exist");
4170
4171 assert_eq!(
4172 tree.meta(secret_id).presence,
4173 Presence::Hidden,
4174 "presence=hidden in template must be preserved"
4175 );
4176 }
4177
4178 #[test]
4180 fn field_without_presence_data_defaults_to_visible() {
4181 let template = r#"<?xml version="1.0"?>
4182<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
4183 <subform name="form1" layout="tb">
4184 <pageSet>
4185 <pageArea name="Page1">
4186 <contentArea w="595pt" h="842pt"/>
4187 <medium short="595pt" long="842pt"/>
4188 </pageArea>
4189 </pageSet>
4190 <field name="normalField" w="100pt" h="20pt" x="0pt" y="0pt"/>
4191 </subform>
4192</template>"#;
4193
4194 let data_xml = r#"<?xml version="1.0"?>
4195<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
4196 <xfa:data>
4197 <form1>
4198 <normalField>hello</normalField>
4199 </form1>
4200 </xfa:data>
4201</xfa:datasets>"#;
4202
4203 let data_dom = DataDom::from_xml(data_xml).unwrap();
4204 let merger = FormMerger::new(&data_dom);
4205 let (tree, _root_id) = merger.merge(template).unwrap();
4206
4207 let field_id = tree
4208 .nodes
4209 .iter()
4210 .enumerate()
4211 .find(|(_, n)| n.name == "normalField")
4212 .map(|(i, _)| FormNodeId(i))
4213 .expect("normalField must exist");
4214
4215 assert_eq!(
4216 tree.meta(field_id).presence,
4217 Presence::Visible,
4218 "field with no presence data must default to Visible"
4219 );
4220 }
4221
4222 #[test]
4227 fn field_binds_via_ancestor_scope_walk() {
4228 let template = r#"<?xml version="1.0"?>
4229<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
4230 <subform name="form1" layout="tb">
4231 <pageSet>
4232 <pageArea name="Page1">
4233 <contentArea w="595pt" h="842pt"/>
4234 <medium short="595pt" long="842pt"/>
4235 </pageArea>
4236 </pageSet>
4237 <subform name="details" layout="tb">
4238 <field name="city" w="200pt" h="20pt" x="0pt" y="0pt"/>
4239 </subform>
4240 </subform>
4241</template>"#;
4242
4243 let data_xml = r#"<?xml version="1.0"?>
4246<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
4247 <xfa:data>
4248 <form1>
4249 <city>Amsterdam</city>
4250 <details/>
4251 </form1>
4252 </xfa:data>
4253</xfa:datasets>"#;
4254
4255 let data_dom = DataDom::from_xml(data_xml).unwrap();
4256 let merger = FormMerger::new(&data_dom);
4257 let (tree, _root_id) = merger.merge(template).unwrap();
4258
4259 let city = tree
4260 .nodes
4261 .iter()
4262 .find(|n| n.name == "city")
4263 .expect("city field must exist");
4264 match &city.node_type {
4265 FormNodeType::Field { value } => {
4266 assert_eq!(
4267 value, "Amsterdam",
4268 "city should be found via ancestor scope walk"
4269 );
4270 }
4271 _ => panic!("city should be a field"),
4272 }
4273 }
4274
4275 #[test]
4289 fn occur_initial_yields_to_data_binding_when_records_exist() {
4290 let template = r#"<?xml version="1.0"?>
4291<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
4292 <subform name="form1" layout="tb">
4293 <pageSet>
4294 <pageArea name="Page1">
4295 <contentArea w="595pt" h="842pt"/>
4296 <medium short="595pt" long="842pt"/>
4297 </pageArea>
4298 </pageSet>
4299 <subform name="Row" layout="position" w="200pt" h="20pt">
4300 <occur min="0" max="10" initial="2"/>
4301 <field name="Item" w="100pt" h="20pt" x="0pt" y="0pt"/>
4302 </subform>
4303 </subform>
4304</template>"#;
4305
4306 let data_xml = r#"<?xml version="1.0"?>
4307<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
4308 <xfa:data>
4309 <form1>
4310 <Row><Item>A</Item></Row>
4311 <Row><Item>B</Item></Row>
4312 <Row><Item>C</Item></Row>
4313 </form1>
4314 </xfa:data>
4315</xfa:datasets>"#;
4316
4317 let data_dom = DataDom::from_xml(data_xml).unwrap();
4318 let merger = FormMerger::new(&data_dom);
4319 let (tree, _root_id) = merger.merge(template).unwrap();
4320
4321 let rows: Vec<_> = tree.nodes.iter().filter(|n| n.name == "Row").collect();
4322 assert_eq!(
4323 rows.len(),
4324 3,
4325 "data binding must win over occur.initial when records exist"
4326 );
4327 }
4328
4329 #[test]
4332 fn occur_initial_drives_count_when_no_data_records() {
4333 let template = r#"<?xml version="1.0"?>
4334<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
4335 <subform name="form1" layout="tb">
4336 <pageSet>
4337 <pageArea name="Page1">
4338 <contentArea w="595pt" h="842pt"/>
4339 <medium short="595pt" long="842pt"/>
4340 </pageArea>
4341 </pageSet>
4342 <subform name="Row" layout="position" w="200pt" h="20pt">
4343 <occur min="0" max="10" initial="5"/>
4344 <field name="Item" w="100pt" h="20pt" x="0pt" y="0pt"/>
4345 </subform>
4346 </subform>
4347</template>"#;
4348
4349 let data_xml = r#"<?xml version="1.0"?>
4351<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
4352 <xfa:data>
4353 <form1/>
4354 </xfa:data>
4355</xfa:datasets>"#;
4356
4357 let data_dom = DataDom::from_xml(data_xml).unwrap();
4358 let merger = FormMerger::new(&data_dom);
4359 let (tree, _root_id) = merger.merge(template).unwrap();
4360
4361 let rows: Vec<_> = tree.nodes.iter().filter(|n| n.name == "Row").collect();
4362 assert_eq!(
4363 rows.len(),
4364 5,
4365 "occur.initial must drive instance count when no data records bind"
4366 );
4367 }
4368
4369 #[test]
4373 fn occur_default_initial_keeps_zero_instance_skeleton_when_no_data() {
4374 let template = r#"<?xml version="1.0"?>
4375<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
4376 <subform name="form1" layout="tb">
4377 <pageSet>
4378 <pageArea name="Page1">
4379 <contentArea w="595pt" h="842pt"/>
4380 <medium short="595pt" long="842pt"/>
4381 </pageArea>
4382 </pageSet>
4383 <subform name="Row" layout="position" w="200pt" h="20pt">
4384 <occur min="0" max="10"/>
4385 <field name="Item" w="100pt" h="20pt" x="0pt" y="0pt"/>
4386 </subform>
4387 </subform>
4388</template>"#;
4389
4390 let data_xml = r#"<?xml version="1.0"?>
4391<xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
4392 <xfa:data>
4393 <form1/>
4394 </xfa:data>
4395</xfa:datasets>"#;
4396
4397 let data_dom = DataDom::from_xml(data_xml).unwrap();
4398 let merger = FormMerger::new(&data_dom);
4399 let (tree, _root_id) = merger.merge(template).unwrap();
4400
4401 let rows: Vec<_> = tree.nodes.iter().filter(|n| n.name == "Row").collect();
4402 assert_eq!(
4403 rows.len(),
4404 1,
4405 "default initial==min path must keep producing a single zero-instance skeleton"
4406 );
4407 let row_id = tree
4410 .nodes
4411 .iter()
4412 .enumerate()
4413 .find(|(_, n)| n.name == "Row")
4414 .map(|(i, _)| FormNodeId(i))
4415 .unwrap();
4416 assert_eq!(tree.meta(row_id).presence, Presence::Hidden);
4417 assert!(tree.meta(row_id).is_zero_instance_prototype);
4418 }
4419
4420 fn spans_of(html: &str) -> Vec<xfa_layout_engine::form::RichTextSpan> {
4423 let xml = format!(r#"<body xmlns="http://www.w3.org/1999/xhtml">{html}</body>"#);
4424 let doc = roxmltree::Document::parse(&xml).expect("xhtml does not parse");
4425 let mut spans = Vec::new();
4426 collect_inline_spans(doc.root_element(), &InheritedStyle::default(), &mut spans);
4427 spans
4428 }
4429
4430 #[test]
4431 fn sup_shifts_up_and_shrinks() {
4432 let spans = spans_of("plain<sup>1</sup>");
4436 let sup = spans
4437 .iter()
4438 .find(|s| s.text.contains('1'))
4439 .expect("no span for the superscript");
4440 assert!(
4441 sup.baseline_shift > 0.0,
4442 "superscript does not shift up: {}",
4443 sup.baseline_shift
4444 );
4445 let plain = spans.iter().find(|s| s.text.contains("plain")).unwrap();
4446 assert_eq!(plain.baseline_shift, 0.0, "the plain text shifted along");
4447 }
4448
4449 #[test]
4450 fn sub_shifts_down() {
4451 let spans = spans_of("H<sub>2</sub>O");
4452 let sub = spans
4453 .iter()
4454 .find(|s| s.text.contains('2'))
4455 .expect("no span");
4456 assert!(sub.baseline_shift < 0.0, "subscript does not shift down");
4457 for s in spans.iter().filter(|s| !s.text.contains('2')) {
4460 assert_eq!(s.baseline_shift, 0.0, "{:?} shifted along", s.text);
4461 }
4462 }
4463
4464 #[test]
4465 fn vertical_align_in_css_works_too() {
4466 let spans = spans_of(r#"<span style="vertical-align: super">x</span>"#);
4469 let s = spans
4470 .iter()
4471 .find(|s| s.text.contains('x'))
4472 .expect("no span");
4473 assert!(s.baseline_shift > 0.0, "vertical-align: super did nothing");
4474 }
4475
4476 #[test]
4477 fn a_shift_does_not_inherit_to_siblings() {
4478 let spans = spans_of("a<sup>b</sup>c");
4480 let c = spans.iter().find(|s| s.text.contains('c'));
4481 if let Some(c) = c {
4482 assert_eq!(
4483 c.baseline_shift, 0.0,
4484 "the text after the superscript shifted along"
4485 );
4486 }
4487 }
4488}