1mod node_factory;
9mod property_parser;
10
11use crate::properties::PropertyList;
12use crate::tree::{FoArena, FoNode, FoNodeData, NodeId};
13use crate::xml::XmlParser;
14use crate::{FopError, Result};
15use quick_xml::events::Event;
16use std::io::BufRead;
17
18pub struct FoTreeBuilder<'a> {
20 arena: FoArena<'a>,
21 current_node: Option<NodeId>,
22 foreign_object_depth: usize,
24 foreign_xml_buffer: String,
26 foreign_object_node: Option<NodeId>,
28}
29
30impl<'a> FoTreeBuilder<'a> {
31 pub fn new() -> Self {
33 Self {
34 arena: FoArena::new(),
35 current_node: None,
36 foreign_object_depth: 0,
37 foreign_xml_buffer: String::new(),
38 foreign_object_node: None,
39 }
40 }
41
42 pub fn parse<R: BufRead>(mut self, reader: R) -> Result<FoArena<'a>> {
44 let mut parser = XmlParser::new(reader);
45
46 loop {
47 let event = parser.read_event()?;
48
49 if self.foreign_object_depth > 0 {
51 match &event {
52 Event::Start(start) => {
53 parser.update_namespaces(start);
54 let raw = std::str::from_utf8(start.as_ref())
55 .unwrap_or("")
56 .to_string();
57 self.foreign_xml_buffer.push('<');
58 self.foreign_xml_buffer.push_str(&raw);
59 self.foreign_xml_buffer.push('>');
60 self.foreign_object_depth += 1;
61 }
62 Event::Empty(start) => {
63 parser.update_namespaces(start);
64 let raw = std::str::from_utf8(start.as_ref())
65 .unwrap_or("")
66 .to_string();
67 self.foreign_xml_buffer.push('<');
68 self.foreign_xml_buffer.push_str(&raw);
69 self.foreign_xml_buffer.push_str("/>");
70 }
71 Event::End(end) => {
72 self.foreign_object_depth -= 1;
73 if self.foreign_object_depth > 0 {
74 let raw = std::str::from_utf8(end.as_ref()).unwrap_or("").to_string();
75 self.foreign_xml_buffer.push_str("</");
76 self.foreign_xml_buffer.push_str(&raw);
77 self.foreign_xml_buffer.push('>');
78 }
79 }
81 Event::Text(text) => {
82 let text_content = parser.extract_text(text).unwrap_or_default();
83 self.foreign_xml_buffer.push_str(&text_content);
84 }
85 Event::Eof => break,
86 _ => {}
87 }
88 continue;
89 }
90
91 match event {
92 Event::Start(ref start) | Event::Empty(ref start) => {
93 parser.update_namespaces(start);
94 let (name, ns) = parser.extract_name(start)?;
95
96 if ns.is_fo() {
97 self.start_element(&name, start, &parser)?;
98
99 if matches!(event, Event::Empty(_)) {
101 self.end_element()?;
102 }
103 } else if self.foreign_object_node.is_some() {
104 let raw = std::str::from_utf8(start.as_ref())
106 .unwrap_or("")
107 .to_string();
108 self.foreign_xml_buffer.push('<');
109 self.foreign_xml_buffer.push_str(&raw);
110 if matches!(event, Event::Empty(_)) {
111 self.foreign_xml_buffer.push_str("/>");
112 } else {
113 self.foreign_xml_buffer.push('>');
114 self.foreign_object_depth += 1;
115 }
116 }
117 }
118 Event::End(_) => {
119 if self.foreign_object_node.is_some() && self.foreign_object_depth == 0 {
120 self.finalize_foreign_object();
122 }
123 self.end_element()?;
124 }
125 Event::Text(text) => {
126 let text_content = parser.extract_text(&text)?;
127 let trimmed = text_content.trim();
128 if !trimmed.is_empty() {
129 self.add_text(trimmed)?;
130 }
131 }
132 Event::CData(cdata) => {
133 let cdata_content = parser.extract_cdata(&cdata)?;
135 if !cdata_content.is_empty() {
136 self.add_text(&cdata_content)?;
137 }
138 }
139 Event::Eof => break,
140 _ => {}
141 }
142 }
143
144 Ok(self.arena)
145 }
146
147 fn finalize_foreign_object(&mut self) {
149 if let Some(node_id) = self.foreign_object_node.take() {
150 let xml = std::mem::take(&mut self.foreign_xml_buffer);
151 if let Some(node) = self.arena.get_mut(node_id) {
152 if let FoNodeData::InstreamForeignObject { foreign_xml, .. } = &mut node.data {
153 *foreign_xml = xml;
154 }
155 }
156 }
157 }
158
159 fn start_element(
161 &mut self,
162 name: &str,
163 start: &quick_xml::events::BytesStart,
164 parser: &XmlParser<impl BufRead>,
165 ) -> Result<()> {
166 let mut properties = PropertyList::new();
168
169 let attributes = parser.extract_attributes(start)?;
171
172 let element_id = attributes
174 .iter()
175 .find(|(k, _)| k == "id")
176 .map(|(_, v)| v.clone());
177
178 node_factory::populate_properties(&mut properties, &attributes)?;
180
181 properties.validate()?;
183
184 if name == "root" {
186 if let Some((_, lang)) = attributes
187 .iter()
188 .find(|(k, _)| k == "xml:lang" || k == "xml-lang")
189 {
190 self.arena.document_lang = Some(lang.clone());
191 }
192 }
193
194 let node_data = node_factory::create_node_data(name, &attributes, properties)?;
196 let node = FoNode::new_with_id(node_data, element_id.clone());
197 let node_id = self.arena.add_node(node);
198
199 if let Some(id) = element_id {
201 self.arena.id_registry_mut().register_id(id, node_id)?;
202 }
203
204 if let Some(parent_id) = self.current_node {
206 self.arena
207 .append_child(parent_id, node_id)
208 .map_err(FopError::Generic)?;
209 }
210
211 if name == "instream-foreign-object" {
213 self.foreign_object_node = Some(node_id);
214 self.foreign_xml_buffer.clear();
215 self.foreign_object_depth = 0;
216 }
217
218 self.current_node = Some(node_id);
220
221 Ok(())
222 }
223
224 fn end_element(&mut self) -> Result<()> {
226 if let Some(current) = self.current_node {
227 self.current_node = self.arena.get(current).and_then(|n| n.parent);
229 }
230 Ok(())
231 }
232
233 fn add_text(&mut self, text: &str) -> Result<()> {
235 if let Some(parent_id) = self.current_node {
236 if let Some(parent) = self.arena.get(parent_id) {
238 if parent.data.can_contain_text() {
239 let text_node = FoNode::new(FoNodeData::Text(text.to_string()));
240 let text_id = self.arena.add_node(text_node);
241 self.arena
242 .append_child(parent_id, text_id)
243 .map_err(FopError::Generic)?;
244 }
245 }
246 }
247 Ok(())
248 }
249}
250
251impl<'a> Default for FoTreeBuilder<'a> {
252 fn default() -> Self {
253 Self::new()
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use crate::PropertyId;
261 use std::io::Cursor;
262
263 #[test]
264 fn test_parse_simple_document() {
265 let xml = r#"<?xml version="1.0"?>
266<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
267 <fo:layout-master-set>
268 <fo:simple-page-master master-name="A4">
269 <fo:region-body/>
270 </fo:simple-page-master>
271 </fo:layout-master-set>
272</fo:root>"#;
273
274 let cursor = Cursor::new(xml);
275 let builder = FoTreeBuilder::new();
276 let arena = builder.parse(cursor).expect("test: should succeed");
277
278 assert!(!arena.is_empty());
279 assert_eq!(arena.len(), 4); }
281
282 #[test]
283 fn test_parse_with_text() {
284 let xml = r#"<?xml version="1.0"?>
285<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
286 <fo:layout-master-set>
287 <fo:simple-page-master master-name="A4">
288 <fo:region-body/>
289 </fo:simple-page-master>
290 </fo:layout-master-set>
291 <fo:page-sequence master-reference="A4">
292 <fo:flow flow-name="xsl-region-body">
293 <fo:block>Hello World</fo:block>
294 </fo:flow>
295 </fo:page-sequence>
296</fo:root>"#;
297
298 let cursor = Cursor::new(xml);
299 let builder = FoTreeBuilder::new();
300 let arena = builder.parse(cursor).expect("test: should succeed");
301
302 assert!(arena.len() >= 8);
305 }
306
307 #[test]
308 fn test_property_parsing() {
309 let xml = r#"<?xml version="1.0"?>
310<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
311 <fo:layout-master-set>
312 <fo:simple-page-master master-name="A4" page-width="210mm" page-height="297mm">
313 <fo:region-body margin="1in"/>
314 </fo:simple-page-master>
315 </fo:layout-master-set>
316</fo:root>"#;
317
318 let cursor = Cursor::new(xml);
319 let builder = FoTreeBuilder::new();
320 let arena = builder.parse(cursor).expect("test: should succeed");
321
322 for (_, node) in arena.iter() {
324 if let Some(props) = node.data.properties() {
325 let _ = props.get(PropertyId::PageWidth);
327 }
328 }
329 }
330
331 #[test]
332 fn test_parse_document_with_block_and_inline() {
333 let xml = r#"<?xml version="1.0"?>
334<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
335 <fo:layout-master-set>
336 <fo:simple-page-master master-name="A4">
337 <fo:region-body/>
338 </fo:simple-page-master>
339 </fo:layout-master-set>
340 <fo:page-sequence master-reference="A4">
341 <fo:flow flow-name="xsl-region-body">
342 <fo:block>
343 <fo:inline font-weight="bold">Bold text</fo:inline>
344 Normal text
345 </fo:block>
346 </fo:flow>
347 </fo:page-sequence>
348</fo:root>"#;
349
350 let cursor = Cursor::new(xml);
351 let builder = FoTreeBuilder::new();
352 let arena = builder.parse(cursor).expect("test: should succeed");
353
354 assert!(arena.len() >= 8);
357 }
358
359 #[test]
360 fn test_parse_document_with_multiple_blocks() {
361 let xml = r#"<?xml version="1.0"?>
362<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
363 <fo:layout-master-set>
364 <fo:simple-page-master master-name="A4">
365 <fo:region-body/>
366 </fo:simple-page-master>
367 </fo:layout-master-set>
368 <fo:page-sequence master-reference="A4">
369 <fo:flow flow-name="xsl-region-body">
370 <fo:block>First block</fo:block>
371 <fo:block>Second block</fo:block>
372 <fo:block>Third block</fo:block>
373 </fo:flow>
374 </fo:page-sequence>
375</fo:root>"#;
376
377 let cursor = Cursor::new(xml);
378 let builder = FoTreeBuilder::new();
379 let arena = builder.parse(cursor).expect("test: should succeed");
380
381 assert!(arena.len() >= 9);
384 }
385
386 #[test]
387 fn test_parse_document_with_font_properties() {
388 let xml = r#"<?xml version="1.0"?>
389<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
390 <fo:layout-master-set>
391 <fo:simple-page-master master-name="A4">
392 <fo:region-body/>
393 </fo:simple-page-master>
394 </fo:layout-master-set>
395 <fo:page-sequence master-reference="A4">
396 <fo:flow flow-name="xsl-region-body">
397 <fo:block font-size="14pt" font-family="Arial" color="red">Styled text</fo:block>
398 </fo:flow>
399 </fo:page-sequence>
400</fo:root>"#;
401
402 let cursor = Cursor::new(xml);
403 let builder = FoTreeBuilder::new();
404 let result = builder.parse(cursor);
405 assert!(
406 result.is_ok(),
407 "Should parse document with font properties: {:?}",
408 result.err()
409 );
410
411 let arena = result.expect("test: should succeed");
412 assert!(arena.len() >= 7);
413 }
414
415 #[test]
416 fn test_parse_document_with_list() {
417 let xml = r#"<?xml version="1.0"?>
418<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
419 <fo:layout-master-set>
420 <fo:simple-page-master master-name="A4">
421 <fo:region-body/>
422 </fo:simple-page-master>
423 </fo:layout-master-set>
424 <fo:page-sequence master-reference="A4">
425 <fo:flow flow-name="xsl-region-body">
426 <fo:list-block>
427 <fo:list-item>
428 <fo:list-item-label><fo:block>1.</fo:block></fo:list-item-label>
429 <fo:list-item-body><fo:block>Item one</fo:block></fo:list-item-body>
430 </fo:list-item>
431 </fo:list-block>
432 </fo:flow>
433 </fo:page-sequence>
434</fo:root>"#;
435
436 let cursor = Cursor::new(xml);
437 let builder = FoTreeBuilder::new();
438 let result = builder.parse(cursor);
439 assert!(
440 result.is_ok(),
441 "Should parse list structure: {:?}",
442 result.err()
443 );
444 }
445
446 #[test]
447 fn test_parse_document_with_cdata() {
448 let xml = r#"<?xml version="1.0"?>
449<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
450 <fo:layout-master-set>
451 <fo:simple-page-master master-name="A4">
452 <fo:region-body/>
453 </fo:simple-page-master>
454 </fo:layout-master-set>
455 <fo:page-sequence master-reference="A4">
456 <fo:flow flow-name="xsl-region-body">
457 <fo:block><![CDATA[Text with <special> & chars]]></fo:block>
458 </fo:flow>
459 </fo:page-sequence>
460</fo:root>"#;
461
462 let cursor = Cursor::new(xml);
463 let builder = FoTreeBuilder::new();
464 let result = builder.parse(cursor);
465 assert!(
467 result.is_ok(),
468 "Should parse CDATA sections: {:?}",
469 result.err()
470 );
471
472 let arena = result.expect("test: should succeed");
473 let has_cdata_text = arena.iter().any(|(_, node)| {
475 if let FoNodeData::Text(text) = &node.data {
476 text.contains("Text with")
477 } else {
478 false
479 }
480 });
481 assert!(
482 has_cdata_text,
483 "CDATA content should be stored as text node"
484 );
485 }
486
487 #[test]
488 fn test_parse_invalid_xml_returns_error() {
489 let xml = r#"<?xml version="1.0"?>
490<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
491 <fo:layout-master-set>
492 <fo:unclosed-element>
493 </fo:layout-master-set>
494</fo:root>"#;
495
496 let cursor = Cursor::new(xml);
497 let builder = FoTreeBuilder::new();
498 let result = builder.parse(cursor);
501 let _ = result;
503 }
504
505 #[test]
506 fn test_parse_document_with_multiple_page_sequences() {
507 let xml = r#"<?xml version="1.0"?>
508<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
509 <fo:layout-master-set>
510 <fo:simple-page-master master-name="A4">
511 <fo:region-body/>
512 </fo:simple-page-master>
513 </fo:layout-master-set>
514 <fo:page-sequence master-reference="A4">
515 <fo:flow flow-name="xsl-region-body">
516 <fo:block>Page 1 content</fo:block>
517 </fo:flow>
518 </fo:page-sequence>
519 <fo:page-sequence master-reference="A4">
520 <fo:flow flow-name="xsl-region-body">
521 <fo:block>Page 2 content</fo:block>
522 </fo:flow>
523 </fo:page-sequence>
524</fo:root>"#;
525
526 let cursor = Cursor::new(xml);
527 let builder = FoTreeBuilder::new();
528 let result = builder.parse(cursor);
529 assert!(result.is_ok(), "Should parse multiple page sequences");
530 }
531
532 #[test]
533 fn test_parse_document_with_margin_property() {
534 let xml = r#"<?xml version="1.0"?>
535<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
536 <fo:layout-master-set>
537 <fo:simple-page-master master-name="A4">
538 <fo:region-body margin-top="1cm" margin-bottom="2cm"/>
539 </fo:simple-page-master>
540 </fo:layout-master-set>
541</fo:root>"#;
542
543 let cursor = Cursor::new(xml);
544 let builder = FoTreeBuilder::new();
545 let result = builder.parse(cursor);
546 assert!(result.is_ok(), "Should parse margin properties");
547 }
548
549 #[test]
550 fn test_parse_document_with_table() {
551 let xml = r#"<?xml version="1.0"?>
552<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
553 <fo:layout-master-set>
554 <fo:simple-page-master master-name="A4">
555 <fo:region-body/>
556 </fo:simple-page-master>
557 </fo:layout-master-set>
558 <fo:page-sequence master-reference="A4">
559 <fo:flow flow-name="xsl-region-body">
560 <fo:table>
561 <fo:table-body>
562 <fo:table-row>
563 <fo:table-cell>
564 <fo:block>Cell content</fo:block>
565 </fo:table-cell>
566 </fo:table-row>
567 </fo:table-body>
568 </fo:table>
569 </fo:flow>
570 </fo:page-sequence>
571</fo:root>"#;
572
573 let cursor = Cursor::new(xml);
574 let builder = FoTreeBuilder::new();
575 let result = builder.parse(cursor);
576 assert!(
577 result.is_ok(),
578 "Should parse table structure: {:?}",
579 result.err()
580 );
581 }
582
583 #[test]
584 fn test_parse_document_is_not_empty() {
585 let xml = r#"<?xml version="1.0"?>
586<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
587 <fo:layout-master-set>
588 <fo:simple-page-master master-name="A4">
589 <fo:region-body/>
590 </fo:simple-page-master>
591 </fo:layout-master-set>
592</fo:root>"#;
593
594 let cursor = Cursor::new(xml);
595 let builder = FoTreeBuilder::new();
596 let arena = builder.parse(cursor).expect("test: should succeed");
597
598 assert!(!arena.is_empty());
599 assert!(!arena.is_empty());
600 }
601
602 #[test]
603 fn test_parse_preserves_text_content() {
604 let xml = r#"<?xml version="1.0"?>
605<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
606 <fo:layout-master-set>
607 <fo:simple-page-master master-name="A4">
608 <fo:region-body/>
609 </fo:simple-page-master>
610 </fo:layout-master-set>
611 <fo:page-sequence master-reference="A4">
612 <fo:flow flow-name="xsl-region-body">
613 <fo:block>Hello World</fo:block>
614 </fo:flow>
615 </fo:page-sequence>
616</fo:root>"#;
617
618 let cursor = Cursor::new(xml);
619 let builder = FoTreeBuilder::new();
620 let arena = builder.parse(cursor).expect("test: should succeed");
621
622 let text_found = arena
624 .iter()
625 .any(|(_, node)| matches!(&node.data, FoNodeData::Text(t) if t == "Hello World"));
626 assert!(text_found, "Text content should be preserved in tree");
627 }
628
629 #[test]
630 fn test_parse_document_with_whitespace_only_text_is_trimmed() {
631 let xml = r#"<?xml version="1.0"?>
632<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
633 <fo:layout-master-set>
634 <fo:simple-page-master master-name="A4">
635 <fo:region-body/>
636 </fo:simple-page-master>
637 </fo:layout-master-set>
638</fo:root>"#;
639
640 let cursor = Cursor::new(xml);
641 let builder = FoTreeBuilder::new();
642 let arena = builder.parse(cursor).expect("test: should succeed");
643
644 let whitespace_only_text = arena.iter().any(|(_, node)| {
646 matches!(&node.data, FoNodeData::Text(t) if t.trim().is_empty() && !t.is_empty())
647 });
648 assert!(
649 !whitespace_only_text,
650 "Whitespace-only text nodes should be stripped"
651 );
652 }
653
654 #[test]
655 fn test_parse_document_with_processing_instruction() {
656 let xml = r#"<?xml version="1.0"?>
657<?fop-processor key="value"?>
658<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
659 <fo:layout-master-set>
660 <fo:simple-page-master master-name="A4">
661 <fo:region-body/>
662 </fo:simple-page-master>
663 </fo:layout-master-set>
664</fo:root>"#;
665
666 let cursor = Cursor::new(xml);
667 let builder = FoTreeBuilder::new();
668 let result = builder.parse(cursor);
669 assert!(
671 result.is_ok(),
672 "Processing instructions should be handled gracefully"
673 );
674 }
675
676 #[test]
677 fn test_parse_document_with_xml_comment() {
678 let xml = r#"<?xml version="1.0"?>
679<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
680 <!-- This is a comment -->
681 <fo:layout-master-set>
682 <fo:simple-page-master master-name="A4">
683 <!-- Page master comment -->
684 <fo:region-body/>
685 </fo:simple-page-master>
686 </fo:layout-master-set>
687</fo:root>"#;
688
689 let cursor = Cursor::new(xml);
690 let builder = FoTreeBuilder::new();
691 let result = builder.parse(cursor);
692 assert!(result.is_ok(), "XML comments should be handled gracefully");
693 }
694
695 #[test]
696 fn test_parse_font_size_in_pts() {
697 let xml = r#"<?xml version="1.0"?>
698<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
699 <fo:layout-master-set>
700 <fo:simple-page-master master-name="A4">
701 <fo:region-body/>
702 </fo:simple-page-master>
703 </fo:layout-master-set>
704 <fo:page-sequence master-reference="A4">
705 <fo:flow flow-name="xsl-region-body">
706 <fo:block font-size="16pt">Large text</fo:block>
707 </fo:flow>
708 </fo:page-sequence>
709</fo:root>"#;
710
711 let cursor = Cursor::new(xml);
712 let builder = FoTreeBuilder::new();
713 let result = builder.parse(cursor);
714 assert!(result.is_ok());
715
716 let arena = result.expect("test: should succeed");
717 for (_, node) in arena.iter() {
719 if let FoNodeData::Block { properties } = &node.data {
720 if properties.is_explicit(PropertyId::FontSize) {
721 let font_size = properties
722 .get(PropertyId::FontSize)
723 .expect("test: should succeed");
724 if let Some(length) = font_size.as_length() {
725 assert_eq!(length.to_pt(), 16.0);
726 }
727 }
728 }
729 }
730 }
731
732 #[test]
733 fn test_parse_color_property_red() {
734 let xml = r#"<?xml version="1.0"?>
735<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
736 <fo:layout-master-set>
737 <fo:simple-page-master master-name="A4">
738 <fo:region-body/>
739 </fo:simple-page-master>
740 </fo:layout-master-set>
741 <fo:page-sequence master-reference="A4">
742 <fo:flow flow-name="xsl-region-body">
743 <fo:block color="red">Red text</fo:block>
744 </fo:flow>
745 </fo:page-sequence>
746</fo:root>"#;
747
748 let cursor = Cursor::new(xml);
749 let builder = FoTreeBuilder::new();
750 let result = builder.parse(cursor);
751 assert!(result.is_ok(), "Should parse color properties");
752 }
753
754 #[test]
755 fn test_parse_hex_color_property() {
756 let xml = r#"<?xml version="1.0"?>
758<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
759 <fo:layout-master-set>
760 <fo:simple-page-master master-name="A4">
761 <fo:region-body/>
762 </fo:simple-page-master>
763 </fo:layout-master-set>
764 <fo:page-sequence master-reference="A4">
765 <fo:flow flow-name="xsl-region-body">
766 <fo:block color="red">Hex red text</fo:block>
767 </fo:flow>
768 </fo:page-sequence>
769</fo:root>"#;
770
771 let cursor = Cursor::new(xml);
772 let builder = FoTreeBuilder::new();
773 let result = builder.parse(cursor);
774 assert!(result.is_ok(), "Should parse color properties");
775 }
776}
777
778#[cfg(test)]
780mod additional_tests {
781 use super::*;
782 use std::io::Cursor;
783
784 fn make_minimal_fo(flow_content: &str) -> String {
785 format!(
786 r#"<?xml version="1.0"?>
787<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
788 <fo:layout-master-set>
789 <fo:simple-page-master master-name="A4">
790 <fo:region-body/>
791 </fo:simple-page-master>
792 </fo:layout-master-set>
793 <fo:page-sequence master-reference="A4">
794 <fo:flow flow-name="xsl-region-body">
795 {}
796 </fo:flow>
797 </fo:page-sequence>
798</fo:root>"#,
799 flow_content
800 )
801 }
802
803 #[test]
804 fn test_parse_block_with_all_font_properties() {
805 let xml = make_minimal_fo(
806 r#"<fo:block font-size="14pt" font-weight="bold" font-style="italic"
807 font-family="Times New Roman" color="navy">Styled text</fo:block>"#,
808 );
809 let cursor = Cursor::new(xml);
810 let result = FoTreeBuilder::new().parse(cursor);
811 assert!(
812 result.is_ok(),
813 "Font properties should parse: {:?}",
814 result.err()
815 );
816 }
817
818 #[test]
819 fn test_parse_block_with_margin_properties() {
820 let xml = make_minimal_fo(
821 r#"<fo:block margin-top="10pt" margin-bottom="10pt"
822 margin-left="20pt" margin-right="20pt">Margins</fo:block>"#,
823 );
824 let cursor = Cursor::new(xml);
825 let result = FoTreeBuilder::new().parse(cursor);
826 assert!(result.is_ok(), "Margin properties: {:?}", result.err());
827 }
828
829 #[test]
830 fn test_parse_block_with_padding_properties() {
831 let xml = make_minimal_fo(
832 r#"<fo:block padding-top="5pt" padding-bottom="5pt"
833 padding-left="10pt" padding-right="10pt">Padding</fo:block>"#,
834 );
835 let cursor = Cursor::new(xml);
836 let result = FoTreeBuilder::new().parse(cursor);
837 assert!(result.is_ok(), "Padding properties: {:?}", result.err());
838 }
839
840 #[test]
841 fn test_parse_block_with_border_properties() {
842 let xml = make_minimal_fo(
843 r#"<fo:block border-top-style="solid" border-top-width="1pt"
844 border-top-color="black">Border</fo:block>"#,
845 );
846 let cursor = Cursor::new(xml);
847 let result = FoTreeBuilder::new().parse(cursor);
848 assert!(result.is_ok(), "Border properties: {:?}", result.err());
849 }
850
851 #[test]
852 fn test_parse_inline_elements() {
853 let xml = make_minimal_fo(
854 r#"<fo:block>Text with <fo:inline font-weight="bold">bold</fo:inline> part</fo:block>"#,
855 );
856 let cursor = Cursor::new(xml);
857 let result = FoTreeBuilder::new().parse(cursor);
858 assert!(result.is_ok(), "Inline element: {:?}", result.err());
859 }
860
861 #[test]
862 fn test_parse_nested_blocks() {
863 let xml = make_minimal_fo(
864 r#"<fo:block>
865 <fo:block>Inner block 1</fo:block>
866 <fo:block>Inner block 2</fo:block>
867 <fo:block>Inner block 3</fo:block>
868 </fo:block>"#,
869 );
870 let cursor = Cursor::new(xml);
871 let result = FoTreeBuilder::new().parse(cursor);
872 assert!(result.is_ok(), "Nested blocks: {:?}", result.err());
873 }
874
875 #[test]
876 fn test_parse_table_structure() {
877 let xml = make_minimal_fo(
878 r#"<fo:table>
879 <fo:table-column column-width="50pt"/>
880 <fo:table-column column-width="50pt"/>
881 <fo:table-body>
882 <fo:table-row>
883 <fo:table-cell><fo:block>Cell 1</fo:block></fo:table-cell>
884 <fo:table-cell><fo:block>Cell 2</fo:block></fo:table-cell>
885 </fo:table-row>
886 </fo:table-body>
887 </fo:table>"#,
888 );
889 let cursor = Cursor::new(xml);
890 let result = FoTreeBuilder::new().parse(cursor);
891 assert!(result.is_ok(), "Table structure: {:?}", result.err());
892 }
893
894 #[test]
895 fn test_parse_list_structure() {
896 let xml = make_minimal_fo(
897 r#"<fo:list-block>
898 <fo:list-item>
899 <fo:list-item-label end-indent="label-end()">
900 <fo:block>1.</fo:block>
901 </fo:list-item-label>
902 <fo:list-item-body start-indent="body-start()">
903 <fo:block>First item</fo:block>
904 </fo:list-item-body>
905 </fo:list-item>
906 </fo:list-block>"#,
907 );
908 let cursor = Cursor::new(xml);
909 let result = FoTreeBuilder::new().parse(cursor);
910 assert!(result.is_ok(), "List structure: {:?}", result.err());
911 }
912
913 #[test]
914 fn test_parse_external_graphic() {
915 let xml = make_minimal_fo(
916 r#"<fo:block><fo:external-graphic src="url('image.png')"/></fo:block>"#,
917 );
918 let cursor = Cursor::new(xml);
919 let result = FoTreeBuilder::new().parse(cursor);
920 assert!(result.is_ok(), "External graphic: {:?}", result.err());
921 }
922
923 #[test]
924 fn test_parse_basic_link_internal() {
925 let xml = make_minimal_fo(
926 r#"<fo:block>
927 <fo:basic-link internal-destination="target">Link</fo:basic-link>
928 </fo:block>"#,
929 );
930 let cursor = Cursor::new(xml);
931 let result = FoTreeBuilder::new().parse(cursor);
932 assert!(result.is_ok(), "Basic link internal: {:?}", result.err());
933 }
934
935 #[test]
936 fn test_parse_basic_link_external() {
937 let xml = make_minimal_fo(
938 r#"<fo:block>
939 <fo:basic-link external-destination="url('https://example.com')">URL</fo:basic-link>
940 </fo:block>"#,
941 );
942 let cursor = Cursor::new(xml);
943 let result = FoTreeBuilder::new().parse(cursor);
944 assert!(result.is_ok(), "Basic link external: {:?}", result.err());
945 }
946
947 #[test]
948 fn test_parse_page_number_inline() {
949 let xml = make_minimal_fo(r#"<fo:block>Page <fo:page-number/></fo:block>"#);
950 let cursor = Cursor::new(xml);
951 let result = FoTreeBuilder::new().parse(cursor);
952 assert!(result.is_ok(), "Page number: {:?}", result.err());
953 }
954
955 #[test]
956 fn test_parse_page_number_citation() {
957 let xml = make_minimal_fo(
958 r#"<fo:block>See page <fo:page-number-citation ref-id="target"/></fo:block>"#,
959 );
960 let cursor = Cursor::new(xml);
961 let result = FoTreeBuilder::new().parse(cursor);
962 assert!(result.is_ok(), "Page number citation: {:?}", result.err());
963 }
964
965 #[test]
966 fn test_parse_leader_dots() {
967 let xml =
968 make_minimal_fo(r#"<fo:block>Chapter<fo:leader leader-pattern="dots"/>10</fo:block>"#);
969 let cursor = Cursor::new(xml);
970 let result = FoTreeBuilder::new().parse(cursor);
971 assert!(result.is_ok(), "Leader: {:?}", result.err());
972 }
973
974 #[test]
975 fn test_parse_footnote() {
976 let xml = make_minimal_fo(
977 r#"<fo:block>Text<fo:footnote>
978 <fo:inline font-size="8pt" vertical-align="super">1</fo:inline>
979 <fo:footnote-body>
980 <fo:block font-size="8pt">Footnote text</fo:block>
981 </fo:footnote-body>
982 </fo:footnote></fo:block>"#,
983 );
984 let cursor = Cursor::new(xml);
985 let result = FoTreeBuilder::new().parse(cursor);
986 assert!(result.is_ok(), "Footnote: {:?}", result.err());
987 }
988
989 #[test]
990 fn test_parse_block_container() {
991 let xml = make_minimal_fo(
992 r#"<fo:block-container width="100pt" height="100pt">
993 <fo:block>Inside block container</fo:block>
994 </fo:block-container>"#,
995 );
996 let cursor = Cursor::new(xml);
997 let result = FoTreeBuilder::new().parse(cursor);
998 assert!(result.is_ok(), "Block container: {:?}", result.err());
999 }
1000
1001 #[test]
1002 fn test_parse_bookmark_tree() {
1003 let xml = r#"<?xml version="1.0"?>
1004<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
1005 <fo:layout-master-set>
1006 <fo:simple-page-master master-name="A4">
1007 <fo:region-body/>
1008 </fo:simple-page-master>
1009 </fo:layout-master-set>
1010 <fo:bookmark-tree>
1011 <fo:bookmark internal-destination="ch1">
1012 <fo:bookmark-title>Chapter 1</fo:bookmark-title>
1013 </fo:bookmark>
1014 </fo:bookmark-tree>
1015 <fo:page-sequence master-reference="A4">
1016 <fo:flow flow-name="xsl-region-body">
1017 <fo:block id="ch1">Chapter 1 content</fo:block>
1018 </fo:flow>
1019 </fo:page-sequence>
1020</fo:root>"#;
1021 let cursor = Cursor::new(xml);
1022 let result = FoTreeBuilder::new().parse(cursor);
1023 assert!(result.is_ok(), "Bookmark tree: {:?}", result.err());
1024 }
1025
1026 #[test]
1027 fn test_parse_document_with_static_content() {
1028 let xml = r#"<?xml version="1.0"?>
1029<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
1030 <fo:layout-master-set>
1031 <fo:simple-page-master master-name="A4">
1032 <fo:region-before extent="20mm"/>
1033 <fo:region-body/>
1034 <fo:region-after extent="20mm"/>
1035 </fo:simple-page-master>
1036 </fo:layout-master-set>
1037 <fo:page-sequence master-reference="A4">
1038 <fo:static-content flow-name="xsl-region-before">
1039 <fo:block>Header text</fo:block>
1040 </fo:static-content>
1041 <fo:static-content flow-name="xsl-region-after">
1042 <fo:block>Footer text</fo:block>
1043 </fo:static-content>
1044 <fo:flow flow-name="xsl-region-body">
1045 <fo:block>Body content</fo:block>
1046 </fo:flow>
1047 </fo:page-sequence>
1048</fo:root>"#;
1049 let cursor = Cursor::new(xml);
1050 let result = FoTreeBuilder::new().parse(cursor);
1051 assert!(result.is_ok(), "Static content: {:?}", result.err());
1052 }
1053
1054 #[test]
1055 fn test_parse_document_returns_non_empty_arena() {
1056 let xml = make_minimal_fo("<fo:block>Content</fo:block>");
1057 let cursor = Cursor::new(xml);
1058 let arena = FoTreeBuilder::new()
1059 .parse(cursor)
1060 .expect("test: should succeed");
1061 assert!(!arena.is_empty(), "Arena should not be empty after parsing");
1062 }
1063
1064 #[test]
1065 fn test_parse_document_root_is_fo_root() {
1066 let xml = make_minimal_fo("<fo:block>Content</fo:block>");
1067 let cursor = Cursor::new(xml);
1068 let arena = FoTreeBuilder::new()
1069 .parse(cursor)
1070 .expect("test: should succeed");
1071 let (_, root_node) = arena.root().expect("Should have root node");
1072 assert!(matches!(root_node.data, FoNodeData::Root));
1073 }
1074
1075 #[test]
1076 fn test_parse_document_with_text_align_center() {
1077 let xml = make_minimal_fo(r#"<fo:block text-align="center">Centered</fo:block>"#);
1078 let cursor = Cursor::new(xml);
1079 let result = FoTreeBuilder::new().parse(cursor);
1080 assert!(result.is_ok(), "text-align center: {:?}", result.err());
1081 }
1082
1083 #[test]
1084 fn test_parse_document_with_text_align_justify() {
1085 let xml = make_minimal_fo(r#"<fo:block text-align="justify">Justified</fo:block>"#);
1086 let cursor = Cursor::new(xml);
1087 let result = FoTreeBuilder::new().parse(cursor);
1088 assert!(result.is_ok(), "text-align justify: {:?}", result.err());
1089 }
1090
1091 #[test]
1092 fn test_parse_line_height_property() {
1093 let xml = make_minimal_fo(r#"<fo:block line-height="1.5">Text</fo:block>"#);
1094 let cursor = Cursor::new(xml);
1095 let result = FoTreeBuilder::new().parse(cursor);
1096 assert!(result.is_ok(), "line-height: {:?}", result.err());
1097 }
1098
1099 #[test]
1100 fn test_parse_keep_together_property() {
1101 let xml = make_minimal_fo(
1102 r#"<fo:block keep-together.within-page="always">Kept together</fo:block>"#,
1103 );
1104 let cursor = Cursor::new(xml);
1105 let result = FoTreeBuilder::new().parse(cursor);
1106 assert!(result.is_ok(), "keep-together: {:?}", result.err());
1107 }
1108
1109 #[test]
1110 fn test_parse_background_color_property() {
1111 let xml = make_minimal_fo(r#"<fo:block background-color="yellow">Highlighted</fo:block>"#);
1112 let cursor = Cursor::new(xml);
1113 let result = FoTreeBuilder::new().parse(cursor);
1114 assert!(result.is_ok(), "background-color: {:?}", result.err());
1115 }
1116
1117 #[test]
1118 fn test_parse_multiple_page_sequences_with_content() {
1119 let xml = r#"<?xml version="1.0"?>
1120<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
1121 <fo:layout-master-set>
1122 <fo:simple-page-master master-name="A4">
1123 <fo:region-body/>
1124 </fo:simple-page-master>
1125 </fo:layout-master-set>
1126 <fo:page-sequence master-reference="A4">
1127 <fo:flow flow-name="xsl-region-body">
1128 <fo:block>Page sequence 1</fo:block>
1129 </fo:flow>
1130 </fo:page-sequence>
1131 <fo:page-sequence master-reference="A4">
1132 <fo:flow flow-name="xsl-region-body">
1133 <fo:block>Page sequence 2</fo:block>
1134 </fo:flow>
1135 </fo:page-sequence>
1136 <fo:page-sequence master-reference="A4">
1137 <fo:flow flow-name="xsl-region-body">
1138 <fo:block>Page sequence 3</fo:block>
1139 </fo:flow>
1140 </fo:page-sequence>
1141</fo:root>"#;
1142 let cursor = Cursor::new(xml);
1143 let result = FoTreeBuilder::new().parse(cursor);
1144 assert!(
1145 result.is_ok(),
1146 "Multiple page sequences: {:?}",
1147 result.err()
1148 );
1149 }
1150
1151 #[test]
1152 fn test_parse_missing_flow_name_is_error() {
1153 let xml = r#"<?xml version="1.0"?>
1154<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
1155 <fo:layout-master-set>
1156 <fo:simple-page-master master-name="A4">
1157 <fo:region-body/>
1158 </fo:simple-page-master>
1159 </fo:layout-master-set>
1160 <fo:page-sequence master-reference="A4">
1161 <fo:flow>
1162 <fo:block>No flow-name attribute</fo:block>
1163 </fo:flow>
1164 </fo:page-sequence>
1165</fo:root>"#;
1166 let cursor = Cursor::new(xml);
1167 let result = FoTreeBuilder::new().parse(cursor);
1168 assert!(result.is_err(), "Missing flow-name should be an error");
1169 }
1170
1171 #[test]
1172 fn test_parse_missing_master_name_is_error() {
1173 let xml = r#"<?xml version="1.0"?>
1174<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
1175 <fo:layout-master-set>
1176 <fo:simple-page-master>
1177 <fo:region-body/>
1178 </fo:simple-page-master>
1179 </fo:layout-master-set>
1180 <fo:page-sequence master-reference="A4">
1181 <fo:flow flow-name="xsl-region-body">
1182 <fo:block>Text</fo:block>
1183 </fo:flow>
1184 </fo:page-sequence>
1185</fo:root>"#;
1186 let cursor = Cursor::new(xml);
1187 let result = FoTreeBuilder::new().parse(cursor);
1188 assert!(result.is_err(), "Missing master-name should be an error");
1189 }
1190
1191 #[test]
1192 fn test_parse_xml_lang_sets_document_lang() {
1193 let xml = r#"<?xml version="1.0"?>
1194<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format" xml:lang="en">
1195 <fo:layout-master-set>
1196 <fo:simple-page-master master-name="A4">
1197 <fo:region-body/>
1198 </fo:simple-page-master>
1199 </fo:layout-master-set>
1200 <fo:page-sequence master-reference="A4">
1201 <fo:flow flow-name="xsl-region-body">
1202 <fo:block>English text</fo:block>
1203 </fo:flow>
1204 </fo:page-sequence>
1205</fo:root>"#;
1206 let cursor = Cursor::new(xml);
1207 let arena = FoTreeBuilder::new()
1208 .parse(cursor)
1209 .expect("test: should succeed");
1210 assert_eq!(arena.document_lang, Some("en".to_string()));
1211 }
1212
1213 #[test]
1214 fn test_parse_document_without_lang_has_none() {
1215 let xml = make_minimal_fo("<fo:block>Text</fo:block>");
1216 let cursor = Cursor::new(xml);
1217 let arena = FoTreeBuilder::new()
1218 .parse(cursor)
1219 .expect("test: should succeed");
1220 assert!(arena.document_lang.is_none());
1221 }
1222
1223 #[test]
1224 fn test_parse_cdata_in_block() {
1225 let xml = make_minimal_fo(r#"<fo:block><![CDATA[<special> & content]]></fo:block>"#);
1226 let cursor = Cursor::new(xml);
1227 let result = FoTreeBuilder::new().parse(cursor);
1228 assert!(result.is_ok(), "CDATA in block: {:?}", result.err());
1229 }
1230}