Skip to main content

fop_core/tree/builder/
mod.rs

1//! FO tree builder - constructs the FO tree from XML
2//!
3//! Splits into:
4//! - `mod.rs` (this file): `FoTreeBuilder` struct, XML parsing loop, element lifecycle
5//! - `node_factory`: FO node creation from element names/attributes
6//! - `property_parser`: Property value parsing (length, color, gradient, etc.)
7
8mod 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
18/// Builder for constructing FO trees from XML
19pub struct FoTreeBuilder<'a> {
20    arena: FoArena<'a>,
21    current_node: Option<NodeId>,
22    /// Depth counter for nested elements inside instream-foreign-object
23    foreign_object_depth: usize,
24    /// Buffer to collect raw XML content of instream-foreign-object
25    foreign_xml_buffer: String,
26    /// NodeId of the instream-foreign-object node being built
27    foreign_object_node: Option<NodeId>,
28}
29
30impl<'a> FoTreeBuilder<'a> {
31    /// Create a new tree builder
32    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    /// Parse an XSL-FO document and build the tree
43    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 we are inside a foreign object child element, capture raw XML
50            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                        // When depth returns to 0, the child root element is closed
80                    }
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 it was an empty element, immediately close it
100                        if matches!(event, Event::Empty(_)) {
101                            self.end_element()?;
102                        }
103                    } else if self.foreign_object_node.is_some() {
104                        // Non-FO element inside instream-foreign-object: capture as raw XML
105                        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                        // This End event closes the fo:instream-foreign-object itself
121                        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                    // CDATA sections preserve content exactly
134                    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    /// Finalize the foreign object: store captured XML and clear state
148    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    /// Handle start of an element
160    fn start_element(
161        &mut self,
162        name: &str,
163        start: &quick_xml::events::BytesStart,
164        parser: &XmlParser<impl BufRead>,
165    ) -> Result<()> {
166        // Create property list (inheritance will be resolved when properties are accessed)
167        let mut properties = PropertyList::new();
168
169        // Parse attributes into properties
170        let attributes = parser.extract_attributes(start)?;
171
172        // Extract the "id" attribute if present
173        let element_id = attributes
174            .iter()
175            .find(|(k, _)| k == "id")
176            .map(|(_, v)| v.clone());
177
178        // Populate properties from attributes
179        node_factory::populate_properties(&mut properties, &attributes)?;
180
181        // Validate all properties after parsing
182        properties.validate()?;
183
184        // Handle xml:lang on fo:root for document language metadata
185        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        // Create the appropriate FO node
195        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        // Register the ID in the registry if present
200        if let Some(id) = element_id {
201            self.arena.id_registry_mut().register_id(id, node_id)?;
202        }
203
204        // Set up parent-child relationship
205        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 this is an instream-foreign-object, track the node for XML capture
212        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        // Update current node
219        self.current_node = Some(node_id);
220
221        Ok(())
222    }
223
224    /// Handle end of an element
225    fn end_element(&mut self) -> Result<()> {
226        if let Some(current) = self.current_node {
227            // Move back to parent
228            self.current_node = self.arena.get(current).and_then(|n| n.parent);
229        }
230        Ok(())
231    }
232
233    /// Add text content to current node
234    fn add_text(&mut self, text: &str) -> Result<()> {
235        if let Some(parent_id) = self.current_node {
236            // Check if parent can contain text
237            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); // root, layout-master-set, simple-page-master, region-body
280    }
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        // Should have: root, layout-master-set, simple-page-master, region-body,
303        //              page-sequence, flow, block, text
304        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        // Check that properties were parsed
323        for (_, node) in arena.iter() {
324            if let Some(props) = node.data.properties() {
325                // Properties should be accessible
326                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        // Should have root, layout-master-set, simple-page-master, region-body,
355        // page-sequence, flow, block, inline, text nodes
356        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        // At least root, layout-master-set, simple-page-master, region-body,
382        // page-sequence, flow, 3 blocks (text nodes may or may not be separate)
383        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        // CDATA sections should be parsed without error
466        assert!(
467            result.is_ok(),
468            "Should parse CDATA sections: {:?}",
469            result.err()
470        );
471
472        let arena = result.expect("test: should succeed");
473        // Find text node with CDATA content
474        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        // Invalid XML (unclosed element) should return an error
499        // (Behavior depends on parser leniency)
500        let result = builder.parse(cursor);
501        // Just verify it doesn't panic - may succeed or fail
502        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        // Find the text node
623        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        // Whitespace-only text nodes should be stripped
645        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        // Processing instructions should not cause parse errors
670        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        // Find the block node and verify its font-size
718        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        // Use rgb() format to avoid issues with # in raw strings
757        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// ===== ADDITIONAL TESTS (new tests for builder) =====
779#[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}