1use crate::tree::FoNodeData;
6use crate::{FopError, Result};
7
8pub struct NestingValidator;
10
11impl NestingValidator {
12 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 }
65 _ => true, };
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 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 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 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 assert!(NestingValidator::can_contain(&list_block, &list_item).is_ok());
177
178 assert!(NestingValidator::can_contain(&list_item, &list_label).is_ok());
180
181 assert!(NestingValidator::can_contain(&list_label, &block).is_ok());
183
184 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 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 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 #[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 #[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 #[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 ®ions {
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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 ®ions {
571 assert!(NestingValidator::can_contain(region, &block()).is_err());
572 assert!(NestingValidator::can_contain(region, &inline_node()).is_err());
573 }
574 }
575
576 #[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 #[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}