Skip to main content

fop_core/tree/
node.rs

1//! FO tree node data structures
2
3use super::arena::NodeId;
4use crate::properties::PropertyList;
5
6/// A node in the FO tree
7pub struct FoNode<'a> {
8    /// The FO data for this node
9    pub data: FoNodeData<'a>,
10
11    /// Optional element ID for cross-references and linking
12    /// Per XSL-FO spec section 5.2.2, the "id" property uniquely identifies an element
13    pub id: Option<String>,
14
15    /// Parent node ID (None for root)
16    pub parent: Option<NodeId>,
17
18    /// First child node ID
19    pub first_child: Option<NodeId>,
20
21    /// Next sibling node ID
22    pub next_sibling: Option<NodeId>,
23}
24
25impl<'a> FoNode<'a> {
26    /// Create a new node with the given data
27    pub fn new(data: FoNodeData<'a>) -> Self {
28        Self {
29            data,
30            id: None,
31            parent: None,
32            first_child: None,
33            next_sibling: None,
34        }
35    }
36
37    /// Create a new node with the given data and ID
38    pub fn new_with_id(data: FoNodeData<'a>, id: Option<String>) -> Self {
39        Self {
40            data,
41            id,
42            parent: None,
43            first_child: None,
44            next_sibling: None,
45        }
46    }
47
48    /// Get the element ID if present
49    pub fn id(&self) -> Option<&str> {
50        self.id.as_deref()
51    }
52
53    /// Set the element ID
54    pub fn set_id(&mut self, id: Option<String>) {
55        self.id = id;
56    }
57
58    /// Check if this node has children
59    #[inline]
60    pub fn has_children(&self) -> bool {
61        self.first_child.is_some()
62    }
63
64    /// Check if this node is a root node
65    #[inline]
66    pub fn is_root(&self) -> bool {
67        self.parent.is_none()
68    }
69}
70
71/// FO node data - the actual formatting object information
72pub enum FoNodeData<'a> {
73    /// fo:root - document root
74    Root,
75
76    /// fo:layout-master-set - page layout definitions
77    LayoutMasterSet,
78
79    /// fo:simple-page-master - single page layout
80    SimplePageMaster {
81        master_name: String,
82        properties: PropertyList<'a>,
83    },
84
85    /// fo:repeatable-page-master-alternatives - conditional page master selection
86    RepeatablePageMasterAlternatives {
87        /// Maximum number of times this can be used (None = unlimited)
88        maximum_repeats: Option<i32>,
89    },
90
91    /// fo:conditional-page-master-reference - conditional reference to page master
92    ConditionalPageMasterReference {
93        /// Reference to the simple-page-master to use
94        master_reference: String,
95        /// Page position constraint (first, last, rest, any)
96        page_position: PagePosition,
97        /// Odd or even constraint (odd, even, any)
98        odd_or_even: OddOrEven,
99        /// Blank or not blank constraint (blank, not-blank, any)
100        blank_or_not_blank: BlankOrNotBlank,
101    },
102
103    /// fo:region-body - main content region
104    RegionBody { properties: PropertyList<'a> },
105
106    /// fo:region-before - header region
107    RegionBefore { properties: PropertyList<'a> },
108
109    /// fo:region-after - footer region
110    RegionAfter { properties: PropertyList<'a> },
111
112    /// fo:region-start - left sidebar region
113    RegionStart { properties: PropertyList<'a> },
114
115    /// fo:region-end - right sidebar region
116    RegionEnd { properties: PropertyList<'a> },
117
118    /// fo:page-sequence - sequence of pages
119    PageSequence {
120        master_reference: String,
121        format: String,
122        grouping_separator: Option<char>,
123        grouping_size: Option<usize>,
124        properties: PropertyList<'a>,
125    },
126
127    /// fo:flow - flowing content
128    Flow {
129        flow_name: String,
130        properties: PropertyList<'a>,
131    },
132
133    /// fo:static-content - static content (headers/footers)
134    StaticContent {
135        flow_name: String,
136        properties: PropertyList<'a>,
137    },
138
139    /// fo:block - block-level element
140    Block { properties: PropertyList<'a> },
141
142    /// fo:inline - inline-level element
143    Inline { properties: PropertyList<'a> },
144
145    /// fo:list-block - list container
146    ListBlock { properties: PropertyList<'a> },
147
148    /// fo:list-item - list item
149    ListItem { properties: PropertyList<'a> },
150
151    /// fo:list-item-label - list item label (bullet/number)
152    ListItemLabel { properties: PropertyList<'a> },
153
154    /// fo:list-item-body - list item content
155    ListItemBody { properties: PropertyList<'a> },
156
157    /// fo:table - table container
158    Table { properties: PropertyList<'a> },
159
160    /// fo:table-column - table column definition
161    TableColumn { properties: PropertyList<'a> },
162
163    /// fo:table-header - table header
164    TableHeader { properties: PropertyList<'a> },
165
166    /// fo:table-footer - table footer
167    TableFooter { properties: PropertyList<'a> },
168
169    /// fo:table-body - table body
170    TableBody { properties: PropertyList<'a> },
171
172    /// fo:table-row - table row
173    TableRow { properties: PropertyList<'a> },
174
175    /// fo:table-cell - table cell
176    TableCell { properties: PropertyList<'a> },
177
178    /// fo:external-graphic - image reference
179    ExternalGraphic {
180        src: String,
181        /// content-width: explicit length, "scale-to-fit", "scale-down-to-fit", "scale-up-to-fit", or "auto"
182        content_width: Option<String>,
183        /// content-height: explicit length, "scale-to-fit", "scale-down-to-fit", "scale-up-to-fit", or "auto"
184        content_height: Option<String>,
185        /// scaling: "uniform" or "non-uniform"
186        scaling: Option<String>,
187        properties: PropertyList<'a>,
188    },
189
190    /// fo:instream-foreign-object - embedded content (SVG, etc.)
191    InstreamForeignObject {
192        content_width: Option<String>,
193        content_height: Option<String>,
194        scaling: Option<String>,
195        foreign_xml: String,
196        properties: PropertyList<'a>,
197    },
198
199    /// fo:basic-link - hyperlink
200    BasicLink {
201        internal_destination: Option<String>,
202        external_destination: Option<String>,
203        properties: PropertyList<'a>,
204    },
205
206    /// fo:bookmark-tree - document outline root
207    BookmarkTree { properties: PropertyList<'a> },
208
209    /// fo:bookmark - bookmark entry
210    Bookmark {
211        internal_destination: Option<String>,
212        external_destination: Option<String>,
213        properties: PropertyList<'a>,
214    },
215
216    /// fo:bookmark-title - bookmark title text
217    BookmarkTitle { properties: PropertyList<'a> },
218
219    /// fo:page-number-citation - reference to page number of an element
220    PageNumberCitation {
221        ref_id: String,
222        properties: PropertyList<'a>,
223    },
224
225    /// fo:page-number-citation-last - reference to last page of an element (Section 8.10)
226    PageNumberCitationLast {
227        ref_id: String,
228        properties: PropertyList<'a>,
229    },
230
231    /// fo:leader - visual separator (dots, lines, etc.)
232    Leader { properties: PropertyList<'a> },
233
234    /// fo:marker - content for running headers/footers
235    Marker {
236        marker_class_name: String,
237        properties: PropertyList<'a>,
238    },
239
240    /// fo:retrieve-marker - retrieves marker content
241    RetrieveMarker {
242        retrieve_class_name: String,
243        retrieve_position: RetrievePosition,
244        properties: PropertyList<'a>,
245    },
246
247    /// fo:footnote - footnote container
248    Footnote { properties: PropertyList<'a> },
249
250    /// fo:footnote-body - footnote content
251    FootnoteBody { properties: PropertyList<'a> },
252
253    /// fo:float - floating element (like CSS floats)
254    Float { properties: PropertyList<'a> },
255
256    /// fo:page-number - inline element inserting the current page number
257    PageNumber { properties: PropertyList<'a> },
258
259    /// fo:block-container - block-level container with absolute/fixed positioning
260    BlockContainer { properties: PropertyList<'a> },
261
262    /// fo:inline-container - inline-level block container (Section 8.13)
263    InlineContainer { properties: PropertyList<'a> },
264
265    /// fo:table-and-caption - table with an associated caption (Section 9.3.3)
266    TableAndCaption { properties: PropertyList<'a> },
267
268    /// fo:table-caption - caption for a table (Section 9.4)
269    TableCaption { properties: PropertyList<'a> },
270
271    /// fo:page-sequence-master - sequence of page master alternatives
272    PageSequenceMaster { master_name: String },
273
274    /// fo:declarations - document-level declarations (color profiles, etc.)
275    Declarations,
276
277    /// fo:color-profile - ICC color profile declaration
278    ColorProfile {
279        src: String,
280        color_profile_name: String,
281    },
282
283    /// Placeholder for recognized but unsupported XSL-FO elements
284    UnsupportedElement {
285        /// The element name, for diagnostic purposes
286        element_name: String,
287    },
288
289    /// fo:multi-switch - container for multiple alternatives (Section 11.1)
290    MultiSwitch { properties: PropertyList<'a> },
291
292    /// fo:multi-case - one alternative within a multi-switch (Section 11.2)
293    MultiCase {
294        /// Whether this case is visible ("visible" or "hidden")
295        starting_state: String,
296        properties: PropertyList<'a>,
297    },
298
299    /// fo:multi-toggle - interactive trigger within multi-case (Section 11.3)
300    MultiToggle { properties: PropertyList<'a> },
301
302    /// fo:multi-properties - property-switching element (Section 11.4)
303    MultiProperties { properties: PropertyList<'a> },
304
305    /// fo:multi-property-set - one set of properties within multi-properties (Section 11.5)
306    MultiPropertySet { properties: PropertyList<'a> },
307
308    /// fo:wrapper - transparent property-setting container (Section 8.12)
309    Wrapper { properties: PropertyList<'a> },
310
311    /// fo:character - single character with full property control (Section 8.5)
312    Character {
313        /// The character to render
314        character: char,
315        properties: PropertyList<'a>,
316    },
317
318    /// fo:bidi-override - bidirectional text override (Section 8.11)
319    BidiOverride {
320        /// Direction: "ltr" or "rtl"
321        direction: String,
322        properties: PropertyList<'a>,
323    },
324
325    /// fo:initial-property-set - first line property overrides (Section 8.6)
326    InitialPropertySet { properties: PropertyList<'a> },
327
328    /// fo:change-bar-begin - marks start of changed region (Section 12.1)
329    ChangeBarBegin {
330        /// Unique identifier linking begin/end pair
331        change_bar_class: String,
332        properties: PropertyList<'a>,
333    },
334
335    /// fo:change-bar-end - marks end of changed region (Section 12.2)
336    ChangeBarEnd {
337        /// Unique identifier linking begin/end pair
338        change_bar_class: String,
339    },
340
341    /// Text content
342    Text(String),
343}
344
345/// Position for retrieving markers from the page
346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
347pub enum RetrievePosition {
348    /// First marker starting on the current page
349    FirstStartingWithinPage,
350    /// First marker including those carried over from previous pages
351    FirstIncludingCarryover,
352    /// Last marker starting on the current page
353    LastStartingWithinPage,
354    /// Last marker ending on the current page
355    LastEndingWithinPage,
356}
357
358/// Page position for conditional page master selection
359#[derive(Debug, Clone, Copy, PartialEq, Eq)]
360pub enum PagePosition {
361    /// First page in the page sequence
362    First,
363    /// Last page in the page sequence
364    Last,
365    /// Any page except first and last
366    Rest,
367    /// Any page (default)
368    Any,
369}
370
371/// Odd or even page for conditional page master selection
372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
373pub enum OddOrEven {
374    /// Odd-numbered pages
375    Odd,
376    /// Even-numbered pages
377    Even,
378    /// Any page (default)
379    Any,
380}
381
382/// Blank or not blank page for conditional page master selection
383#[derive(Debug, Clone, Copy, PartialEq, Eq)]
384pub enum BlankOrNotBlank {
385    /// Blank pages (pages with no content)
386    Blank,
387    /// Non-blank pages (pages with content)
388    NotBlank,
389    /// Any page (default)
390    Any,
391}
392
393impl<'a> FoNodeData<'a> {
394    /// Get the element name for this node
395    pub fn element_name(&self) -> &str {
396        match self {
397            FoNodeData::Root => "root",
398            FoNodeData::LayoutMasterSet => "layout-master-set",
399            FoNodeData::SimplePageMaster { .. } => "simple-page-master",
400            FoNodeData::RepeatablePageMasterAlternatives { .. } => {
401                "repeatable-page-master-alternatives"
402            }
403            FoNodeData::ConditionalPageMasterReference { .. } => {
404                "conditional-page-master-reference"
405            }
406            FoNodeData::RegionBody { .. } => "region-body",
407            FoNodeData::RegionBefore { .. } => "region-before",
408            FoNodeData::RegionAfter { .. } => "region-after",
409            FoNodeData::RegionStart { .. } => "region-start",
410            FoNodeData::RegionEnd { .. } => "region-end",
411            FoNodeData::PageSequence { .. } => "page-sequence",
412            FoNodeData::Flow { .. } => "flow",
413            FoNodeData::StaticContent { .. } => "static-content",
414            FoNodeData::Block { .. } => "block",
415            FoNodeData::Inline { .. } => "inline",
416            FoNodeData::ListBlock { .. } => "list-block",
417            FoNodeData::ListItem { .. } => "list-item",
418            FoNodeData::ListItemLabel { .. } => "list-item-label",
419            FoNodeData::ListItemBody { .. } => "list-item-body",
420            FoNodeData::Table { .. } => "table",
421            FoNodeData::TableColumn { .. } => "table-column",
422            FoNodeData::TableHeader { .. } => "table-header",
423            FoNodeData::TableFooter { .. } => "table-footer",
424            FoNodeData::TableBody { .. } => "table-body",
425            FoNodeData::TableRow { .. } => "table-row",
426            FoNodeData::TableCell { .. } => "table-cell",
427            FoNodeData::ExternalGraphic { .. } => "external-graphic",
428            FoNodeData::InstreamForeignObject { .. } => "instream-foreign-object",
429            FoNodeData::BasicLink { .. } => "basic-link",
430            FoNodeData::BookmarkTree { .. } => "bookmark-tree",
431            FoNodeData::Bookmark { .. } => "bookmark",
432            FoNodeData::BookmarkTitle { .. } => "bookmark-title",
433            FoNodeData::PageNumberCitation { .. } => "page-number-citation",
434            FoNodeData::PageNumberCitationLast { .. } => "page-number-citation-last",
435            FoNodeData::Leader { .. } => "leader",
436            FoNodeData::Marker { .. } => "marker",
437            FoNodeData::RetrieveMarker { .. } => "retrieve-marker",
438            FoNodeData::Footnote { .. } => "footnote",
439            FoNodeData::FootnoteBody { .. } => "footnote-body",
440            FoNodeData::Float { .. } => "float",
441            FoNodeData::PageNumber { .. } => "page-number",
442            FoNodeData::BlockContainer { .. } => "block-container",
443            FoNodeData::InlineContainer { .. } => "inline-container",
444            FoNodeData::TableAndCaption { .. } => "table-and-caption",
445            FoNodeData::TableCaption { .. } => "table-caption",
446            FoNodeData::PageSequenceMaster { .. } => "page-sequence-master",
447            FoNodeData::Declarations => "declarations",
448            FoNodeData::ColorProfile { .. } => "color-profile",
449            FoNodeData::UnsupportedElement { .. } => "unsupported-element",
450            FoNodeData::MultiSwitch { .. } => "multi-switch",
451            FoNodeData::MultiCase { .. } => "multi-case",
452            FoNodeData::MultiToggle { .. } => "multi-toggle",
453            FoNodeData::MultiProperties { .. } => "multi-properties",
454            FoNodeData::MultiPropertySet { .. } => "multi-property-set",
455            FoNodeData::Wrapper { .. } => "wrapper",
456            FoNodeData::Character { .. } => "character",
457            FoNodeData::BidiOverride { .. } => "bidi-override",
458            FoNodeData::InitialPropertySet { .. } => "initial-property-set",
459            FoNodeData::ChangeBarBegin { .. } => "change-bar-begin",
460            FoNodeData::ChangeBarEnd { .. } => "change-bar-end",
461            FoNodeData::Text(_) => "#text",
462        }
463    }
464
465    /// Get mutable property list if this node has one
466    pub fn properties_mut(&mut self) -> Option<&mut PropertyList<'a>> {
467        match self {
468            FoNodeData::SimplePageMaster { properties, .. }
469            | FoNodeData::RegionBody { properties }
470            | FoNodeData::RegionBefore { properties }
471            | FoNodeData::RegionAfter { properties }
472            | FoNodeData::RegionStart { properties }
473            | FoNodeData::RegionEnd { properties }
474            | FoNodeData::PageSequence { properties, .. }
475            | FoNodeData::Flow { properties, .. }
476            | FoNodeData::StaticContent { properties, .. }
477            | FoNodeData::Block { properties }
478            | FoNodeData::Inline { properties }
479            | FoNodeData::ListBlock { properties }
480            | FoNodeData::ListItem { properties }
481            | FoNodeData::ListItemLabel { properties }
482            | FoNodeData::ListItemBody { properties }
483            | FoNodeData::Table { properties }
484            | FoNodeData::TableColumn { properties }
485            | FoNodeData::TableHeader { properties }
486            | FoNodeData::TableFooter { properties }
487            | FoNodeData::TableBody { properties }
488            | FoNodeData::TableRow { properties }
489            | FoNodeData::TableCell { properties }
490            | FoNodeData::ExternalGraphic { properties, .. }
491            | FoNodeData::InstreamForeignObject { properties, .. }
492            | FoNodeData::BasicLink { properties, .. }
493            | FoNodeData::BookmarkTree { properties }
494            | FoNodeData::Bookmark { properties, .. }
495            | FoNodeData::BookmarkTitle { properties }
496            | FoNodeData::PageNumberCitation { properties, .. }
497            | FoNodeData::PageNumberCitationLast { properties, .. }
498            | FoNodeData::Leader { properties }
499            | FoNodeData::Marker { properties, .. }
500            | FoNodeData::RetrieveMarker { properties, .. }
501            | FoNodeData::Footnote { properties }
502            | FoNodeData::FootnoteBody { properties }
503            | FoNodeData::Float { properties }
504            | FoNodeData::Wrapper { properties }
505            | FoNodeData::Character { properties, .. }
506            | FoNodeData::BidiOverride { properties, .. }
507            | FoNodeData::InitialPropertySet { properties }
508            | FoNodeData::PageNumber { properties }
509            | FoNodeData::BlockContainer { properties }
510            | FoNodeData::InlineContainer { properties }
511            | FoNodeData::TableAndCaption { properties }
512            | FoNodeData::TableCaption { properties }
513            | FoNodeData::MultiSwitch { properties }
514            | FoNodeData::MultiCase { properties, .. }
515            | FoNodeData::MultiToggle { properties }
516            | FoNodeData::MultiProperties { properties }
517            | FoNodeData::MultiPropertySet { properties }
518            | FoNodeData::ChangeBarBegin { properties, .. } => Some(properties),
519            FoNodeData::ChangeBarEnd { .. } => None,
520            _ => None,
521        }
522    }
523
524    /// Get immutable property list if this node has one
525    pub fn properties(&self) -> Option<&PropertyList<'a>> {
526        match self {
527            FoNodeData::SimplePageMaster { properties, .. }
528            | FoNodeData::RegionBody { properties }
529            | FoNodeData::RegionBefore { properties }
530            | FoNodeData::RegionAfter { properties }
531            | FoNodeData::RegionStart { properties }
532            | FoNodeData::RegionEnd { properties }
533            | FoNodeData::PageSequence { properties, .. }
534            | FoNodeData::Flow { properties, .. }
535            | FoNodeData::StaticContent { properties, .. }
536            | FoNodeData::Block { properties }
537            | FoNodeData::Inline { properties }
538            | FoNodeData::ListBlock { properties }
539            | FoNodeData::ListItem { properties }
540            | FoNodeData::ListItemLabel { properties }
541            | FoNodeData::ListItemBody { properties }
542            | FoNodeData::Table { properties }
543            | FoNodeData::TableColumn { properties }
544            | FoNodeData::TableHeader { properties }
545            | FoNodeData::TableFooter { properties }
546            | FoNodeData::TableBody { properties }
547            | FoNodeData::TableRow { properties }
548            | FoNodeData::TableCell { properties }
549            | FoNodeData::ExternalGraphic { properties, .. }
550            | FoNodeData::InstreamForeignObject { properties, .. }
551            | FoNodeData::BasicLink { properties, .. }
552            | FoNodeData::BookmarkTree { properties }
553            | FoNodeData::Bookmark { properties, .. }
554            | FoNodeData::BookmarkTitle { properties }
555            | FoNodeData::PageNumberCitation { properties, .. }
556            | FoNodeData::PageNumberCitationLast { properties, .. }
557            | FoNodeData::Leader { properties }
558            | FoNodeData::Marker { properties, .. }
559            | FoNodeData::RetrieveMarker { properties, .. }
560            | FoNodeData::Footnote { properties }
561            | FoNodeData::FootnoteBody { properties }
562            | FoNodeData::Float { properties }
563            | FoNodeData::Wrapper { properties }
564            | FoNodeData::Character { properties, .. }
565            | FoNodeData::BidiOverride { properties, .. }
566            | FoNodeData::InitialPropertySet { properties }
567            | FoNodeData::PageNumber { properties }
568            | FoNodeData::BlockContainer { properties }
569            | FoNodeData::InlineContainer { properties }
570            | FoNodeData::TableAndCaption { properties }
571            | FoNodeData::TableCaption { properties }
572            | FoNodeData::MultiSwitch { properties }
573            | FoNodeData::MultiCase { properties, .. }
574            | FoNodeData::MultiToggle { properties }
575            | FoNodeData::MultiProperties { properties }
576            | FoNodeData::MultiPropertySet { properties }
577            | FoNodeData::ChangeBarBegin { properties, .. } => Some(properties),
578            FoNodeData::ChangeBarEnd { .. } => None,
579            _ => None,
580        }
581    }
582
583    /// Check if this node type can contain text
584    pub fn can_contain_text(&self) -> bool {
585        matches!(
586            self,
587            FoNodeData::Block { .. }
588                | FoNodeData::Inline { .. }
589                | FoNodeData::BasicLink { .. }
590                | FoNodeData::BookmarkTitle { .. }
591                | FoNodeData::Marker { .. }
592                | FoNodeData::FootnoteBody { .. }
593        )
594    }
595
596    /// Check if this node type is a layout master
597    pub fn is_layout_master(&self) -> bool {
598        matches!(
599            self,
600            FoNodeData::LayoutMasterSet
601                | FoNodeData::SimplePageMaster { .. }
602                | FoNodeData::RepeatablePageMasterAlternatives { .. }
603                | FoNodeData::ConditionalPageMasterReference { .. }
604        )
605    }
606
607    /// Check if this node type is a region
608    pub fn is_region(&self) -> bool {
609        matches!(
610            self,
611            FoNodeData::RegionBody { .. }
612                | FoNodeData::RegionBefore { .. }
613                | FoNodeData::RegionAfter { .. }
614                | FoNodeData::RegionStart { .. }
615                | FoNodeData::RegionEnd { .. }
616        )
617    }
618}
619
620#[cfg(test)]
621mod tests {
622    use super::*;
623
624    #[test]
625    fn test_node_creation() {
626        let node = FoNode::new(FoNodeData::Root);
627        assert!(node.is_root());
628        assert!(!node.has_children());
629    }
630
631    #[test]
632    fn test_element_names() {
633        assert_eq!(FoNodeData::Root.element_name(), "root");
634        assert_eq!(
635            FoNodeData::LayoutMasterSet.element_name(),
636            "layout-master-set"
637        );
638        assert_eq!(FoNodeData::Text(String::new()).element_name(), "#text");
639    }
640
641    #[test]
642    fn test_property_access() {
643        let props = PropertyList::new();
644        let mut data = FoNodeData::Block { properties: props };
645
646        assert!(data.properties().is_some());
647        assert!(data.properties_mut().is_some());
648
649        let root = FoNodeData::Root;
650        assert!(root.properties().is_none());
651    }
652
653    #[test]
654    fn test_node_type_checks() {
655        let layout = FoNodeData::LayoutMasterSet;
656        assert!(layout.is_layout_master());
657
658        let region = FoNodeData::RegionBody {
659            properties: PropertyList::new(),
660        };
661        assert!(region.is_region());
662
663        let block = FoNodeData::Block {
664            properties: PropertyList::new(),
665        };
666        assert!(block.can_contain_text());
667    }
668}
669
670#[cfg(test)]
671mod tests_extended {
672    use super::*;
673
674    fn props() -> PropertyList<'static> {
675        PropertyList::new()
676    }
677
678    // -----------------------------------------------------------------------
679    // element_name() coverage for every variant
680    // -----------------------------------------------------------------------
681
682    #[test]
683    fn test_element_name_root() {
684        assert_eq!(FoNodeData::Root.element_name(), "root");
685    }
686
687    #[test]
688    fn test_element_name_layout_master_set() {
689        assert_eq!(
690            FoNodeData::LayoutMasterSet.element_name(),
691            "layout-master-set"
692        );
693    }
694
695    #[test]
696    fn test_element_name_simple_page_master() {
697        let data = FoNodeData::SimplePageMaster {
698            master_name: "A4".to_string(),
699            properties: props(),
700        };
701        assert_eq!(data.element_name(), "simple-page-master");
702    }
703
704    #[test]
705    fn test_element_name_page_sequence() {
706        let data = FoNodeData::PageSequence {
707            master_reference: "A4".to_string(),
708            format: "1".to_string(),
709            grouping_separator: None,
710            grouping_size: None,
711            properties: props(),
712        };
713        assert_eq!(data.element_name(), "page-sequence");
714    }
715
716    #[test]
717    fn test_element_name_flow() {
718        let data = FoNodeData::Flow {
719            flow_name: "xsl-region-body".to_string(),
720            properties: props(),
721        };
722        assert_eq!(data.element_name(), "flow");
723    }
724
725    #[test]
726    fn test_element_name_static_content() {
727        let data = FoNodeData::StaticContent {
728            flow_name: "xsl-region-before".to_string(),
729            properties: props(),
730        };
731        assert_eq!(data.element_name(), "static-content");
732    }
733
734    #[test]
735    fn test_element_name_block() {
736        assert_eq!(
737            FoNodeData::Block {
738                properties: props()
739            }
740            .element_name(),
741            "block"
742        );
743    }
744
745    #[test]
746    fn test_element_name_inline() {
747        assert_eq!(
748            FoNodeData::Inline {
749                properties: props()
750            }
751            .element_name(),
752            "inline"
753        );
754    }
755
756    #[test]
757    fn test_element_name_list_elements() {
758        assert_eq!(
759            FoNodeData::ListBlock {
760                properties: props()
761            }
762            .element_name(),
763            "list-block"
764        );
765        assert_eq!(
766            FoNodeData::ListItem {
767                properties: props()
768            }
769            .element_name(),
770            "list-item"
771        );
772        assert_eq!(
773            FoNodeData::ListItemLabel {
774                properties: props()
775            }
776            .element_name(),
777            "list-item-label"
778        );
779        assert_eq!(
780            FoNodeData::ListItemBody {
781                properties: props()
782            }
783            .element_name(),
784            "list-item-body"
785        );
786    }
787
788    #[test]
789    fn test_element_name_table_elements() {
790        assert_eq!(
791            FoNodeData::Table {
792                properties: props()
793            }
794            .element_name(),
795            "table"
796        );
797        assert_eq!(
798            FoNodeData::TableColumn {
799                properties: props()
800            }
801            .element_name(),
802            "table-column"
803        );
804        assert_eq!(
805            FoNodeData::TableHeader {
806                properties: props()
807            }
808            .element_name(),
809            "table-header"
810        );
811        assert_eq!(
812            FoNodeData::TableFooter {
813                properties: props()
814            }
815            .element_name(),
816            "table-footer"
817        );
818        assert_eq!(
819            FoNodeData::TableBody {
820                properties: props()
821            }
822            .element_name(),
823            "table-body"
824        );
825        assert_eq!(
826            FoNodeData::TableRow {
827                properties: props()
828            }
829            .element_name(),
830            "table-row"
831        );
832        assert_eq!(
833            FoNodeData::TableCell {
834                properties: props()
835            }
836            .element_name(),
837            "table-cell"
838        );
839    }
840
841    #[test]
842    fn test_element_name_graphic() {
843        let data = FoNodeData::ExternalGraphic {
844            src: "img.png".to_string(),
845            content_width: None,
846            content_height: None,
847            scaling: None,
848            properties: props(),
849        };
850        assert_eq!(data.element_name(), "external-graphic");
851    }
852
853    #[test]
854    fn test_element_name_basic_link() {
855        let data = FoNodeData::BasicLink {
856            internal_destination: None,
857            external_destination: Some("http://example.com".to_string()),
858            properties: props(),
859        };
860        assert_eq!(data.element_name(), "basic-link");
861    }
862
863    #[test]
864    fn test_element_name_page_number() {
865        assert_eq!(
866            FoNodeData::PageNumber {
867                properties: props()
868            }
869            .element_name(),
870            "page-number"
871        );
872    }
873
874    #[test]
875    fn test_element_name_page_number_citation() {
876        let data = FoNodeData::PageNumberCitation {
877            ref_id: "sec1".to_string(),
878            properties: props(),
879        };
880        assert_eq!(data.element_name(), "page-number-citation");
881    }
882
883    #[test]
884    fn test_element_name_page_number_citation_last() {
885        let data = FoNodeData::PageNumberCitationLast {
886            ref_id: "sec1".to_string(),
887            properties: props(),
888        };
889        assert_eq!(data.element_name(), "page-number-citation-last");
890    }
891
892    #[test]
893    fn test_element_name_block_container() {
894        assert_eq!(
895            FoNodeData::BlockContainer {
896                properties: props()
897            }
898            .element_name(),
899            "block-container"
900        );
901    }
902
903    #[test]
904    fn test_element_name_inline_container() {
905        assert_eq!(
906            FoNodeData::InlineContainer {
907                properties: props()
908            }
909            .element_name(),
910            "inline-container"
911        );
912    }
913
914    #[test]
915    fn test_element_name_wrapper() {
916        assert_eq!(
917            FoNodeData::Wrapper {
918                properties: props()
919            }
920            .element_name(),
921            "wrapper"
922        );
923    }
924
925    #[test]
926    fn test_element_name_character() {
927        let data = FoNodeData::Character {
928            character: 'A',
929            properties: props(),
930        };
931        assert_eq!(data.element_name(), "character");
932    }
933
934    #[test]
935    fn test_element_name_bidi_override() {
936        let data = FoNodeData::BidiOverride {
937            direction: "rtl".to_string(),
938            properties: props(),
939        };
940        assert_eq!(data.element_name(), "bidi-override");
941    }
942
943    #[test]
944    fn test_element_name_initial_property_set() {
945        assert_eq!(
946            FoNodeData::InitialPropertySet {
947                properties: props()
948            }
949            .element_name(),
950            "initial-property-set"
951        );
952    }
953
954    #[test]
955    fn test_element_name_declarations() {
956        assert_eq!(FoNodeData::Declarations.element_name(), "declarations");
957    }
958
959    #[test]
960    fn test_element_name_color_profile() {
961        let data = FoNodeData::ColorProfile {
962            src: "profile.icc".to_string(),
963            color_profile_name: "cmyk".to_string(),
964        };
965        assert_eq!(data.element_name(), "color-profile");
966    }
967
968    #[test]
969    fn test_element_name_unsupported() {
970        let data = FoNodeData::UnsupportedElement {
971            element_name: "fo:multi-properties".to_string(),
972        };
973        assert_eq!(data.element_name(), "unsupported-element");
974    }
975
976    #[test]
977    fn test_element_name_text() {
978        assert_eq!(
979            FoNodeData::Text("hello".to_string()).element_name(),
980            "#text"
981        );
982    }
983
984    #[test]
985    fn test_element_name_leader() {
986        assert_eq!(
987            FoNodeData::Leader {
988                properties: props()
989            }
990            .element_name(),
991            "leader"
992        );
993    }
994
995    #[test]
996    fn test_element_name_footnote() {
997        assert_eq!(
998            FoNodeData::Footnote {
999                properties: props()
1000            }
1001            .element_name(),
1002            "footnote"
1003        );
1004        assert_eq!(
1005            FoNodeData::FootnoteBody {
1006                properties: props()
1007            }
1008            .element_name(),
1009            "footnote-body"
1010        );
1011    }
1012
1013    #[test]
1014    fn test_element_name_bookmark() {
1015        assert_eq!(
1016            FoNodeData::BookmarkTree {
1017                properties: props()
1018            }
1019            .element_name(),
1020            "bookmark-tree"
1021        );
1022        let bm = FoNodeData::Bookmark {
1023            internal_destination: Some("intro".to_string()),
1024            external_destination: None,
1025            properties: props(),
1026        };
1027        assert_eq!(bm.element_name(), "bookmark");
1028        assert_eq!(
1029            FoNodeData::BookmarkTitle {
1030                properties: props()
1031            }
1032            .element_name(),
1033            "bookmark-title"
1034        );
1035    }
1036
1037    #[test]
1038    fn test_element_name_marker() {
1039        let data = FoNodeData::Marker {
1040            marker_class_name: "header".to_string(),
1041            properties: props(),
1042        };
1043        assert_eq!(data.element_name(), "marker");
1044    }
1045
1046    #[test]
1047    fn test_element_name_retrieve_marker() {
1048        let data = FoNodeData::RetrieveMarker {
1049            retrieve_class_name: "header".to_string(),
1050            retrieve_position: RetrievePosition::FirstStartingWithinPage,
1051            properties: props(),
1052        };
1053        assert_eq!(data.element_name(), "retrieve-marker");
1054    }
1055
1056    #[test]
1057    fn test_element_name_page_sequence_master() {
1058        let data = FoNodeData::PageSequenceMaster {
1059            master_name: "alternating".to_string(),
1060        };
1061        assert_eq!(data.element_name(), "page-sequence-master");
1062    }
1063
1064    #[test]
1065    fn test_element_name_change_bar() {
1066        let begin = FoNodeData::ChangeBarBegin {
1067            change_bar_class: "c1".to_string(),
1068            properties: props(),
1069        };
1070        let end = FoNodeData::ChangeBarEnd {
1071            change_bar_class: "c1".to_string(),
1072        };
1073        assert_eq!(begin.element_name(), "change-bar-begin");
1074        assert_eq!(end.element_name(), "change-bar-end");
1075    }
1076
1077    // -----------------------------------------------------------------------
1078    // FoNode struct API
1079    // -----------------------------------------------------------------------
1080
1081    #[test]
1082    fn test_fo_node_new_with_id() {
1083        let node = FoNode::new_with_id(FoNodeData::Root, Some("root-id".to_string()));
1084        assert_eq!(node.id(), Some("root-id"));
1085        assert!(node.is_root());
1086    }
1087
1088    #[test]
1089    fn test_fo_node_new_no_id() {
1090        let node = FoNode::new(FoNodeData::LayoutMasterSet);
1091        assert!(node.id().is_none());
1092    }
1093
1094    #[test]
1095    fn test_fo_node_set_id() {
1096        let mut node = FoNode::new(FoNodeData::Root);
1097        assert!(node.id().is_none());
1098        node.set_id(Some("my-id".to_string()));
1099        assert_eq!(node.id(), Some("my-id"));
1100        node.set_id(None);
1101        assert!(node.id().is_none());
1102    }
1103
1104    #[test]
1105    fn test_fo_node_has_children() {
1106        use super::super::arena::NodeId;
1107        let mut node = FoNode::new(FoNodeData::Root);
1108        assert!(!node.has_children());
1109        // Simulate having a first_child
1110        node.first_child = Some(NodeId::from_index(1));
1111        assert!(node.has_children());
1112    }
1113
1114    #[test]
1115    fn test_fo_node_is_root_with_parent() {
1116        use super::super::arena::NodeId;
1117        let mut node = FoNode::new(FoNodeData::Block {
1118            properties: props(),
1119        });
1120        assert!(node.is_root()); // no parent set yet
1121        node.parent = Some(NodeId::from_index(0));
1122        assert!(!node.is_root());
1123    }
1124
1125    // -----------------------------------------------------------------------
1126    // can_contain_text(), is_layout_master(), is_region()
1127    // -----------------------------------------------------------------------
1128
1129    #[test]
1130    fn test_can_contain_text_block_level() {
1131        assert!(FoNodeData::Block {
1132            properties: props()
1133        }
1134        .can_contain_text());
1135        assert!(FoNodeData::Inline {
1136            properties: props()
1137        }
1138        .can_contain_text());
1139    }
1140
1141    #[test]
1142    fn test_cannot_contain_text_non_text_containers() {
1143        assert!(!FoNodeData::Table {
1144            properties: props()
1145        }
1146        .can_contain_text());
1147        assert!(!FoNodeData::TableRow {
1148            properties: props()
1149        }
1150        .can_contain_text());
1151        assert!(!FoNodeData::Root.can_contain_text());
1152        assert!(!FoNodeData::LayoutMasterSet.can_contain_text());
1153        assert!(!FoNodeData::ListBlock {
1154            properties: props()
1155        }
1156        .can_contain_text());
1157    }
1158
1159    #[test]
1160    fn test_is_layout_master_variants() {
1161        assert!(FoNodeData::LayoutMasterSet.is_layout_master());
1162        assert!(FoNodeData::SimplePageMaster {
1163            master_name: "A4".to_string(),
1164            properties: props(),
1165        }
1166        .is_layout_master());
1167        assert!(FoNodeData::RepeatablePageMasterAlternatives {
1168            maximum_repeats: None,
1169        }
1170        .is_layout_master());
1171        assert!(FoNodeData::ConditionalPageMasterReference {
1172            master_reference: "A4".to_string(),
1173            page_position: PagePosition::Any,
1174            odd_or_even: OddOrEven::Any,
1175            blank_or_not_blank: BlankOrNotBlank::Any,
1176        }
1177        .is_layout_master());
1178    }
1179
1180    #[test]
1181    fn test_is_not_layout_master() {
1182        assert!(!FoNodeData::Root.is_layout_master());
1183        assert!(!FoNodeData::Block {
1184            properties: props()
1185        }
1186        .is_layout_master());
1187    }
1188
1189    #[test]
1190    fn test_is_region_all_variants() {
1191        assert!(FoNodeData::RegionBody {
1192            properties: props()
1193        }
1194        .is_region());
1195        assert!(FoNodeData::RegionBefore {
1196            properties: props()
1197        }
1198        .is_region());
1199        assert!(FoNodeData::RegionAfter {
1200            properties: props()
1201        }
1202        .is_region());
1203        assert!(FoNodeData::RegionStart {
1204            properties: props()
1205        }
1206        .is_region());
1207        assert!(FoNodeData::RegionEnd {
1208            properties: props()
1209        }
1210        .is_region());
1211    }
1212
1213    #[test]
1214    fn test_is_not_region() {
1215        assert!(!FoNodeData::Root.is_region());
1216        assert!(!FoNodeData::Block {
1217            properties: props()
1218        }
1219        .is_region());
1220        assert!(!FoNodeData::LayoutMasterSet.is_region());
1221    }
1222
1223    // -----------------------------------------------------------------------
1224    // properties() / properties_mut()
1225    // -----------------------------------------------------------------------
1226
1227    #[test]
1228    fn test_properties_none_for_propertyless_variants() {
1229        assert!(FoNodeData::Root.properties().is_none());
1230        assert!(FoNodeData::LayoutMasterSet.properties().is_none());
1231        assert!(FoNodeData::Declarations.properties().is_none());
1232        assert!(FoNodeData::Text("hi".to_string()).properties().is_none());
1233        let cbe = FoNodeData::ChangeBarEnd {
1234            change_bar_class: "c1".to_string(),
1235        };
1236        assert!(cbe.properties().is_none());
1237    }
1238
1239    #[test]
1240    fn test_properties_some_for_property_bearing_variants() {
1241        assert!(FoNodeData::Block {
1242            properties: props()
1243        }
1244        .properties()
1245        .is_some());
1246        assert!(FoNodeData::Inline {
1247            properties: props()
1248        }
1249        .properties()
1250        .is_some());
1251        assert!(FoNodeData::Table {
1252            properties: props()
1253        }
1254        .properties()
1255        .is_some());
1256        assert!(FoNodeData::PageNumber {
1257            properties: props()
1258        }
1259        .properties()
1260        .is_some());
1261        assert!(FoNodeData::BlockContainer {
1262            properties: props()
1263        }
1264        .properties()
1265        .is_some());
1266        assert!(FoNodeData::Wrapper {
1267            properties: props()
1268        }
1269        .properties()
1270        .is_some());
1271    }
1272
1273    #[test]
1274    fn test_properties_mut_some() {
1275        let mut data = FoNodeData::Inline {
1276            properties: props(),
1277        };
1278        assert!(data.properties_mut().is_some());
1279    }
1280
1281    #[test]
1282    fn test_properties_mut_none_for_text() {
1283        let mut data = FoNodeData::Text("hello".to_string());
1284        assert!(data.properties_mut().is_none());
1285    }
1286
1287    // -----------------------------------------------------------------------
1288    // Enum helper types
1289    // -----------------------------------------------------------------------
1290
1291    #[test]
1292    fn test_page_position_equality() {
1293        assert_eq!(PagePosition::First, PagePosition::First);
1294        assert_ne!(PagePosition::First, PagePosition::Last);
1295        assert_ne!(PagePosition::Rest, PagePosition::Any);
1296    }
1297
1298    #[test]
1299    fn test_odd_or_even_equality() {
1300        assert_eq!(OddOrEven::Odd, OddOrEven::Odd);
1301        assert_ne!(OddOrEven::Odd, OddOrEven::Even);
1302    }
1303
1304    #[test]
1305    fn test_blank_or_not_blank_equality() {
1306        assert_eq!(BlankOrNotBlank::Blank, BlankOrNotBlank::Blank);
1307        assert_ne!(BlankOrNotBlank::Blank, BlankOrNotBlank::NotBlank);
1308    }
1309
1310    #[test]
1311    fn test_retrieve_position_copy() {
1312        let p1 = RetrievePosition::FirstStartingWithinPage;
1313        let p2 = p1;
1314        assert_eq!(p1, p2);
1315    }
1316
1317    #[test]
1318    fn test_region_element_names() {
1319        assert_eq!(
1320            FoNodeData::RegionBody {
1321                properties: props()
1322            }
1323            .element_name(),
1324            "region-body"
1325        );
1326        assert_eq!(
1327            FoNodeData::RegionBefore {
1328                properties: props()
1329            }
1330            .element_name(),
1331            "region-before"
1332        );
1333        assert_eq!(
1334            FoNodeData::RegionAfter {
1335                properties: props()
1336            }
1337            .element_name(),
1338            "region-after"
1339        );
1340        assert_eq!(
1341            FoNodeData::RegionStart {
1342                properties: props()
1343            }
1344            .element_name(),
1345            "region-start"
1346        );
1347        assert_eq!(
1348            FoNodeData::RegionEnd {
1349                properties: props()
1350            }
1351            .element_name(),
1352            "region-end"
1353        );
1354    }
1355
1356    #[test]
1357    fn test_multi_element_names() {
1358        assert_eq!(
1359            FoNodeData::MultiSwitch {
1360                properties: props()
1361            }
1362            .element_name(),
1363            "multi-switch"
1364        );
1365        assert_eq!(
1366            FoNodeData::MultiCase {
1367                starting_state: "visible".to_string(),
1368                properties: props(),
1369            }
1370            .element_name(),
1371            "multi-case"
1372        );
1373        assert_eq!(
1374            FoNodeData::MultiToggle {
1375                properties: props()
1376            }
1377            .element_name(),
1378            "multi-toggle"
1379        );
1380        assert_eq!(
1381            FoNodeData::MultiProperties {
1382                properties: props()
1383            }
1384            .element_name(),
1385            "multi-properties"
1386        );
1387        assert_eq!(
1388            FoNodeData::MultiPropertySet {
1389                properties: props()
1390            }
1391            .element_name(),
1392            "multi-property-set"
1393        );
1394    }
1395
1396    #[test]
1397    fn test_float_element_name() {
1398        assert_eq!(
1399            FoNodeData::Float {
1400                properties: props()
1401            }
1402            .element_name(),
1403            "float"
1404        );
1405    }
1406
1407    #[test]
1408    fn test_table_and_caption_element_names() {
1409        assert_eq!(
1410            FoNodeData::TableAndCaption {
1411                properties: props()
1412            }
1413            .element_name(),
1414            "table-and-caption"
1415        );
1416        assert_eq!(
1417            FoNodeData::TableCaption {
1418                properties: props()
1419            }
1420            .element_name(),
1421            "table-caption"
1422        );
1423    }
1424
1425    #[test]
1426    fn test_instream_foreign_object_element_name() {
1427        let data = FoNodeData::InstreamForeignObject {
1428            content_width: None,
1429            content_height: None,
1430            scaling: None,
1431            foreign_xml: "<svg/>".to_string(),
1432            properties: props(),
1433        };
1434        assert_eq!(data.element_name(), "instream-foreign-object");
1435    }
1436
1437    #[test]
1438    fn test_repeatable_page_master_alternatives_element_name() {
1439        let data = FoNodeData::RepeatablePageMasterAlternatives {
1440            maximum_repeats: Some(3),
1441        };
1442        assert_eq!(data.element_name(), "repeatable-page-master-alternatives");
1443    }
1444
1445    #[test]
1446    fn test_conditional_page_master_reference_element_name() {
1447        let data = FoNodeData::ConditionalPageMasterReference {
1448            master_reference: "A4".to_string(),
1449            page_position: PagePosition::First,
1450            odd_or_even: OddOrEven::Odd,
1451            blank_or_not_blank: BlankOrNotBlank::NotBlank,
1452        };
1453        assert_eq!(data.element_name(), "conditional-page-master-reference");
1454    }
1455}