Skip to main content

fop_core/tree/
validation.rs

1//! Element nesting validation
2//!
3//! Validates that FO elements are nested according to XSL-FO 1.1 specification rules.
4
5use crate::tree::FoNodeData;
6use crate::{FopError, Result};
7
8/// Validator for FO element nesting
9pub struct NestingValidator;
10
11impl NestingValidator {
12    /// Check if a child element can be nested inside a parent element
13    pub fn can_contain(parent: &FoNodeData, child: &FoNodeData) -> Result<()> {
14        let parent_name = parent.element_name();
15        let child_name = child.element_name();
16
17        let allowed = match parent_name {
18            "root" => matches!(
19                child_name,
20                "layout-master-set" | "declarations" | "page-sequence"
21            ),
22            "layout-master-set" => {
23                matches!(child_name, "simple-page-master" | "page-sequence-master")
24            }
25            "simple-page-master" => matches!(
26                child_name,
27                "region-body" | "region-before" | "region-after" | "region-start" | "region-end"
28            ),
29            "page-sequence" => matches!(child_name, "flow" | "static-content"),
30            "flow" | "static-content" => {
31                Self::is_block_level(child_name) || child_name == "multi-switch"
32            }
33            "block" => {
34                Self::is_block_or_inline(child_name)
35                    || child_name == "#text"
36                    || child_name == "multi-switch"
37                    || child_name == "multi-toggle"
38            }
39            "inline" => Self::is_inline_level(child_name) || child_name == "#text",
40            "list-block" => child_name == "list-item",
41            "list-item" => matches!(child_name, "list-item-label" | "list-item-body"),
42            "list-item-label" | "list-item-body" => Self::is_block_level(child_name),
43            "table" => matches!(
44                child_name,
45                "table-column" | "table-header" | "table-footer" | "table-body"
46            ),
47            "table-header" | "table-footer" | "table-body" => child_name == "table-row",
48            "table-row" => child_name == "table-cell",
49            "table-cell" => Self::is_block_level(child_name),
50            "inline-container" => Self::is_block_level(child_name) || child_name == "#text",
51            "table-and-caption" => matches!(child_name, "table" | "table-caption"),
52            "table-caption" => Self::is_block_level(child_name),
53            "basic-link" => Self::is_inline_level(child_name) || child_name == "#text",
54            "multi-switch" => child_name == "multi-case",
55            "multi-case" => {
56                Self::is_block_or_inline(child_name)
57                    || child_name == "#text"
58                    || child_name == "multi-toggle"
59            }
60            "multi-toggle" => Self::is_inline_level(child_name) || child_name == "#text",
61            "multi-properties" => child_name == "multi-property-set",
62            "region-body" | "region-before" | "region-after" | "region-start" | "region-end" => {
63                false // Regions don't contain children directly
64            }
65            _ => true, // Unknown elements, allow for now
66        };
67
68        if allowed {
69            Ok(())
70        } else {
71            Err(FopError::InvalidNesting {
72                parent: parent_name.to_string(),
73                child: child_name.to_string(),
74            })
75        }
76    }
77
78    /// Check if element name is block-level
79    fn is_block_level(name: &str) -> bool {
80        matches!(
81            name,
82            "block" | "block-container" | "list-block" | "table" | "table-and-caption"
83        )
84    }
85
86    /// Check if element name is inline-level
87    fn is_inline_level(name: &str) -> bool {
88        matches!(
89            name,
90            "inline"
91                | "inline-container"
92                | "external-graphic"
93                | "instream-foreign-object"
94                | "basic-link"
95                | "character"
96                | "page-number"
97                | "page-number-citation"
98        )
99    }
100
101    /// Check if element can be block or inline
102    fn is_block_or_inline(name: &str) -> bool {
103        Self::is_block_level(name) || Self::is_inline_level(name)
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::PropertyList;
111
112    #[test]
113    fn test_valid_root_children() {
114        let root = FoNodeData::Root;
115        let layout = FoNodeData::LayoutMasterSet;
116        let page_seq = FoNodeData::PageSequence {
117            master_reference: String::from("test"),
118            format: "1".to_string(),
119            grouping_separator: None,
120            grouping_size: None,
121            properties: PropertyList::new(),
122        };
123
124        assert!(NestingValidator::can_contain(&root, &layout).is_ok());
125        assert!(NestingValidator::can_contain(&root, &page_seq).is_ok());
126    }
127
128    #[test]
129    fn test_invalid_root_child() {
130        let root = FoNodeData::Root;
131        let block = FoNodeData::Block {
132            properties: PropertyList::new(),
133        };
134
135        assert!(NestingValidator::can_contain(&root, &block).is_err());
136    }
137
138    #[test]
139    fn test_block_can_contain_text() {
140        let block = FoNodeData::Block {
141            properties: PropertyList::new(),
142        };
143        let text = FoNodeData::Text(String::from("Hello"));
144
145        assert!(NestingValidator::can_contain(&block, &text).is_ok());
146    }
147
148    #[test]
149    fn test_block_can_contain_inline() {
150        let block = FoNodeData::Block {
151            properties: PropertyList::new(),
152        };
153        let inline = FoNodeData::Inline {
154            properties: PropertyList::new(),
155        };
156
157        assert!(NestingValidator::can_contain(&block, &inline).is_ok());
158    }
159
160    #[test]
161    fn test_list_structure() {
162        let list_block = FoNodeData::ListBlock {
163            properties: PropertyList::new(),
164        };
165        let list_item = FoNodeData::ListItem {
166            properties: PropertyList::new(),
167        };
168        let list_label = FoNodeData::ListItemLabel {
169            properties: PropertyList::new(),
170        };
171        let block = FoNodeData::Block {
172            properties: PropertyList::new(),
173        };
174
175        // list-block can contain list-item
176        assert!(NestingValidator::can_contain(&list_block, &list_item).is_ok());
177
178        // list-item can contain list-item-label
179        assert!(NestingValidator::can_contain(&list_item, &list_label).is_ok());
180
181        // list-item-label can contain block
182        assert!(NestingValidator::can_contain(&list_label, &block).is_ok());
183
184        // list-block cannot contain block directly
185        assert!(NestingValidator::can_contain(&list_block, &block).is_err());
186    }
187
188    #[test]
189    fn test_table_structure() {
190        let table = FoNodeData::Table {
191            properties: PropertyList::new(),
192        };
193        let table_body = FoNodeData::TableBody {
194            properties: PropertyList::new(),
195        };
196        let table_row = FoNodeData::TableRow {
197            properties: PropertyList::new(),
198        };
199        let table_cell = FoNodeData::TableCell {
200            properties: PropertyList::new(),
201        };
202        let block = FoNodeData::Block {
203            properties: PropertyList::new(),
204        };
205
206        // Valid nesting
207        assert!(NestingValidator::can_contain(&table, &table_body).is_ok());
208        assert!(NestingValidator::can_contain(&table_body, &table_row).is_ok());
209        assert!(NestingValidator::can_contain(&table_row, &table_cell).is_ok());
210        assert!(NestingValidator::can_contain(&table_cell, &block).is_ok());
211
212        // Invalid nesting
213        assert!(NestingValidator::can_contain(&table, &table_row).is_err());
214        assert!(NestingValidator::can_contain(&table, &block).is_err());
215    }
216}
217
218#[cfg(test)]
219mod tests_extended {
220    use super::*;
221    use crate::PropertyList;
222
223    fn props() -> PropertyList<'static> {
224        PropertyList::new()
225    }
226
227    fn block() -> FoNodeData<'static> {
228        FoNodeData::Block {
229            properties: props(),
230        }
231    }
232
233    fn inline_node() -> FoNodeData<'static> {
234        FoNodeData::Inline {
235            properties: props(),
236        }
237    }
238
239    fn text() -> FoNodeData<'static> {
240        FoNodeData::Text("hello".to_string())
241    }
242
243    // -----------------------------------------------------------------------
244    // root element nesting
245    // -----------------------------------------------------------------------
246
247    #[test]
248    fn test_root_can_contain_declarations() {
249        assert!(
250            NestingValidator::can_contain(&FoNodeData::Root, &FoNodeData::Declarations).is_ok()
251        );
252    }
253
254    #[test]
255    fn test_root_cannot_contain_flow() {
256        let flow = FoNodeData::Flow {
257            flow_name: "xsl-region-body".to_string(),
258            properties: props(),
259        };
260        assert!(NestingValidator::can_contain(&FoNodeData::Root, &flow).is_err());
261    }
262
263    #[test]
264    fn test_root_cannot_contain_inline() {
265        assert!(NestingValidator::can_contain(&FoNodeData::Root, &inline_node()).is_err());
266    }
267
268    // -----------------------------------------------------------------------
269    // layout-master-set
270    // -----------------------------------------------------------------------
271
272    #[test]
273    fn test_layout_master_set_can_contain_simple_page_master() {
274        let lms = FoNodeData::LayoutMasterSet;
275        let spm = FoNodeData::SimplePageMaster {
276            master_name: "A4".to_string(),
277            properties: props(),
278        };
279        assert!(NestingValidator::can_contain(&lms, &spm).is_ok());
280    }
281
282    #[test]
283    fn test_layout_master_set_can_contain_page_sequence_master() {
284        let lms = FoNodeData::LayoutMasterSet;
285        let psm = FoNodeData::PageSequenceMaster {
286            master_name: "alternating".to_string(),
287        };
288        assert!(NestingValidator::can_contain(&lms, &psm).is_ok());
289    }
290
291    #[test]
292    fn test_layout_master_set_cannot_contain_block() {
293        let lms = FoNodeData::LayoutMasterSet;
294        assert!(NestingValidator::can_contain(&lms, &block()).is_err());
295    }
296
297    // -----------------------------------------------------------------------
298    // simple-page-master regions
299    // -----------------------------------------------------------------------
300
301    #[test]
302    fn test_simple_page_master_can_contain_all_regions() {
303        let spm = FoNodeData::SimplePageMaster {
304            master_name: "A4".to_string(),
305            properties: props(),
306        };
307        let regions = vec![
308            FoNodeData::RegionBody {
309                properties: props(),
310            },
311            FoNodeData::RegionBefore {
312                properties: props(),
313            },
314            FoNodeData::RegionAfter {
315                properties: props(),
316            },
317            FoNodeData::RegionStart {
318                properties: props(),
319            },
320            FoNodeData::RegionEnd {
321                properties: props(),
322            },
323        ];
324        for region in &regions {
325            assert!(NestingValidator::can_contain(&spm, region).is_ok());
326        }
327    }
328
329    #[test]
330    fn test_simple_page_master_cannot_contain_block() {
331        let spm = FoNodeData::SimplePageMaster {
332            master_name: "A4".to_string(),
333            properties: props(),
334        };
335        assert!(NestingValidator::can_contain(&spm, &block()).is_err());
336    }
337
338    // -----------------------------------------------------------------------
339    // page-sequence
340    // -----------------------------------------------------------------------
341
342    #[test]
343    fn test_page_sequence_can_contain_flow_and_static() {
344        let ps = FoNodeData::PageSequence {
345            master_reference: "A4".to_string(),
346            format: "1".to_string(),
347            grouping_separator: None,
348            grouping_size: None,
349            properties: props(),
350        };
351        let flow = FoNodeData::Flow {
352            flow_name: "xsl-region-body".to_string(),
353            properties: props(),
354        };
355        let sc = FoNodeData::StaticContent {
356            flow_name: "xsl-region-before".to_string(),
357            properties: props(),
358        };
359        assert!(NestingValidator::can_contain(&ps, &flow).is_ok());
360        assert!(NestingValidator::can_contain(&ps, &sc).is_ok());
361    }
362
363    #[test]
364    fn test_page_sequence_cannot_contain_block() {
365        let ps = FoNodeData::PageSequence {
366            master_reference: "A4".to_string(),
367            format: "1".to_string(),
368            grouping_separator: None,
369            grouping_size: None,
370            properties: props(),
371        };
372        assert!(NestingValidator::can_contain(&ps, &block()).is_err());
373    }
374
375    // -----------------------------------------------------------------------
376    // flow / static-content
377    // -----------------------------------------------------------------------
378
379    #[test]
380    fn test_flow_can_contain_block_level_elements() {
381        let flow = FoNodeData::Flow {
382            flow_name: "xsl-region-body".to_string(),
383            properties: props(),
384        };
385        let list_block = FoNodeData::ListBlock {
386            properties: props(),
387        };
388        let table = FoNodeData::Table {
389            properties: props(),
390        };
391        assert!(NestingValidator::can_contain(&flow, &block()).is_ok());
392        assert!(NestingValidator::can_contain(&flow, &list_block).is_ok());
393        assert!(NestingValidator::can_contain(&flow, &table).is_ok());
394    }
395
396    #[test]
397    fn test_flow_cannot_contain_text_directly() {
398        let flow = FoNodeData::Flow {
399            flow_name: "xsl-region-body".to_string(),
400            properties: props(),
401        };
402        assert!(NestingValidator::can_contain(&flow, &text()).is_err());
403    }
404
405    // -----------------------------------------------------------------------
406    // block
407    // -----------------------------------------------------------------------
408
409    #[test]
410    fn test_block_can_contain_block_container() {
411        let block_container = FoNodeData::BlockContainer {
412            properties: props(),
413        };
414        assert!(NestingValidator::can_contain(&block(), &block_container).is_ok());
415    }
416
417    #[test]
418    fn test_block_can_contain_nested_block() {
419        assert!(NestingValidator::can_contain(&block(), &block()).is_ok());
420    }
421
422    // -----------------------------------------------------------------------
423    // inline
424    // -----------------------------------------------------------------------
425
426    #[test]
427    fn test_inline_can_contain_inline() {
428        assert!(NestingValidator::can_contain(&inline_node(), &inline_node()).is_ok());
429    }
430
431    #[test]
432    fn test_inline_can_contain_text() {
433        assert!(NestingValidator::can_contain(&inline_node(), &text()).is_ok());
434    }
435
436    #[test]
437    fn test_inline_cannot_contain_block() {
438        assert!(NestingValidator::can_contain(&inline_node(), &block()).is_err());
439    }
440
441    // -----------------------------------------------------------------------
442    // table structure
443    // -----------------------------------------------------------------------
444
445    #[test]
446    fn test_table_can_contain_header_and_footer() {
447        let table = FoNodeData::Table {
448            properties: props(),
449        };
450        assert!(NestingValidator::can_contain(
451            &table,
452            &FoNodeData::TableHeader {
453                properties: props()
454            }
455        )
456        .is_ok());
457        assert!(NestingValidator::can_contain(
458            &table,
459            &FoNodeData::TableFooter {
460                properties: props()
461            }
462        )
463        .is_ok());
464        assert!(NestingValidator::can_contain(
465            &table,
466            &FoNodeData::TableColumn {
467                properties: props()
468            }
469        )
470        .is_ok());
471    }
472
473    #[test]
474    fn test_table_cannot_contain_table_cell_directly() {
475        let table = FoNodeData::Table {
476            properties: props(),
477        };
478        assert!(NestingValidator::can_contain(
479            &table,
480            &FoNodeData::TableCell {
481                properties: props()
482            }
483        )
484        .is_err());
485    }
486
487    // -----------------------------------------------------------------------
488    // basic-link
489    // -----------------------------------------------------------------------
490
491    #[test]
492    fn test_basic_link_can_contain_inline() {
493        let link = FoNodeData::BasicLink {
494            internal_destination: None,
495            external_destination: Some("http://x.com".to_string()),
496            properties: props(),
497        };
498        assert!(NestingValidator::can_contain(&link, &inline_node()).is_ok());
499        assert!(NestingValidator::can_contain(&link, &text()).is_ok());
500    }
501
502    #[test]
503    fn test_basic_link_cannot_contain_block() {
504        let link = FoNodeData::BasicLink {
505            internal_destination: None,
506            external_destination: Some("http://x.com".to_string()),
507            properties: props(),
508        };
509        assert!(NestingValidator::can_contain(&link, &block()).is_err());
510    }
511
512    // -----------------------------------------------------------------------
513    // multi-switch / multi-case
514    // -----------------------------------------------------------------------
515
516    #[test]
517    fn test_multi_switch_can_contain_multi_case() {
518        let ms = FoNodeData::MultiSwitch {
519            properties: props(),
520        };
521        let mc = FoNodeData::MultiCase {
522            starting_state: "visible".to_string(),
523            properties: props(),
524        };
525        assert!(NestingValidator::can_contain(&ms, &mc).is_ok());
526    }
527
528    #[test]
529    fn test_multi_switch_cannot_contain_block() {
530        let ms = FoNodeData::MultiSwitch {
531            properties: props(),
532        };
533        assert!(NestingValidator::can_contain(&ms, &block()).is_err());
534    }
535
536    #[test]
537    fn test_multi_case_can_contain_block_and_inline() {
538        let mc = FoNodeData::MultiCase {
539            starting_state: "visible".to_string(),
540            properties: props(),
541        };
542        assert!(NestingValidator::can_contain(&mc, &block()).is_ok());
543        assert!(NestingValidator::can_contain(&mc, &inline_node()).is_ok());
544        assert!(NestingValidator::can_contain(&mc, &text()).is_ok());
545    }
546
547    // -----------------------------------------------------------------------
548    // regions do not contain children
549    // -----------------------------------------------------------------------
550
551    #[test]
552    fn test_regions_cannot_contain_children() {
553        let regions: Vec<FoNodeData> = vec![
554            FoNodeData::RegionBody {
555                properties: props(),
556            },
557            FoNodeData::RegionBefore {
558                properties: props(),
559            },
560            FoNodeData::RegionAfter {
561                properties: props(),
562            },
563            FoNodeData::RegionStart {
564                properties: props(),
565            },
566            FoNodeData::RegionEnd {
567                properties: props(),
568            },
569        ];
570        for region in &regions {
571            assert!(NestingValidator::can_contain(region, &block()).is_err());
572            assert!(NestingValidator::can_contain(region, &inline_node()).is_err());
573        }
574    }
575
576    // -----------------------------------------------------------------------
577    // inline-container
578    // -----------------------------------------------------------------------
579
580    #[test]
581    fn test_inline_container_can_contain_block() {
582        let ic = FoNodeData::InlineContainer {
583            properties: props(),
584        };
585        assert!(NestingValidator::can_contain(&ic, &block()).is_ok());
586        assert!(NestingValidator::can_contain(&ic, &text()).is_ok());
587    }
588
589    // -----------------------------------------------------------------------
590    // table-and-caption
591    // -----------------------------------------------------------------------
592
593    #[test]
594    fn test_table_and_caption_structure() {
595        let tac = FoNodeData::TableAndCaption {
596            properties: props(),
597        };
598        let caption = FoNodeData::TableCaption {
599            properties: props(),
600        };
601        let table = FoNodeData::Table {
602            properties: props(),
603        };
604        assert!(NestingValidator::can_contain(&tac, &caption).is_ok());
605        assert!(NestingValidator::can_contain(&tac, &table).is_ok());
606        assert!(NestingValidator::can_contain(&tac, &block()).is_err());
607    }
608}