Skip to main content

fop_layout/layout/engine/
mod.rs

1//! Main layout engine
2//!
3//! Coordinates the layout process from FO tree to area tree.
4
5mod block_layout;
6mod inline_layout;
7mod page_layout;
8mod page_master;
9mod table_layout;
10pub(super) mod types;
11
12use crate::area::{Area, AreaContent, AreaTree, AreaType, TraitSet};
13use crate::layout::{
14    extract_clear, extract_column_count, extract_column_gap, extract_space_after, extract_traits,
15    BlockLayoutContext, BorderCollapse, ColumnInfo, ColumnWidth, ListLayout, PageNumberResolver,
16    TableLayout, TableLayoutMode,
17};
18use fop_core::{FoArena, FoNodeData, NodeId, PropertyId};
19use fop_types::{FontRegistry, Length, Point, Rect, Result, Size};
20
21use types::{FloatManager, MarkerMap};
22
23pub use types::{ClearSide, FloatSide, MultiColumnLayout};
24
25/// Layout engine - transforms FO tree to area tree
26pub struct LayoutEngine {
27    /// Default page width (A4)
28    pub(super) page_width: Length,
29
30    /// Default page height (A4)
31    pub(super) page_height: Length,
32
33    /// Font registry for text measurement
34    #[allow(dead_code)]
35    pub(super) font_registry: FontRegistry,
36
37    /// Enable streaming mode for large documents
38    /// When true, prefer using layout_streaming() for memory-efficient processing
39    pub(super) streaming_mode: bool,
40}
41
42impl LayoutEngine {
43    /// Create a new layout engine
44    pub fn new() -> Self {
45        Self {
46            page_width: Length::from_mm(210.0),  // A4 width
47            page_height: Length::from_mm(297.0), // A4 height
48            font_registry: FontRegistry::new(),
49            streaming_mode: false,
50        }
51    }
52
53    /// Enable streaming mode for large documents
54    ///
55    /// When streaming mode is enabled, the engine is optimized for processing
56    /// large documents (1000+ pages) with bounded memory usage.
57    pub fn with_streaming_mode(mut self, enabled: bool) -> Self {
58        self.streaming_mode = enabled;
59        self
60    }
61
62    /// Check if streaming mode is enabled
63    pub fn is_streaming_mode(&self) -> bool {
64        self.streaming_mode
65    }
66
67    /// Perform layout on an FO tree, producing an area tree
68    pub fn layout(&self, fo_tree: &FoArena) -> Result<AreaTree> {
69        let mut area_tree = AreaTree::new();
70        let mut resolver = PageNumberResolver::new();
71        let mut marker_map = MarkerMap::new();
72
73        // First pass: Layout everything and track IDs
74        if let Some((root_id, _)) = fo_tree.root() {
75            self.layout_node(
76                fo_tree,
77                root_id,
78                &mut area_tree,
79                None,
80                &mut resolver,
81                &mut marker_map,
82            )?;
83        }
84
85        // Second pass: Resolve page number citations
86        self.resolve_citations(&mut area_tree, &resolver)?;
87
88        Ok(area_tree)
89    }
90
91    /// Resolve page number citations in the area tree
92    fn resolve_citations(
93        &self,
94        area_tree: &mut AreaTree,
95        resolver: &PageNumberResolver,
96    ) -> Result<()> {
97        for (area_id, ref_id) in resolver.get_citations() {
98            if let Some(page_number) = resolver.get_page_number(ref_id) {
99                // Update the area content with the resolved page number
100                if let Some(area_node) = area_tree.get_mut(*area_id) {
101                    area_node.area.content = Some(AreaContent::Text(page_number.to_string()));
102                }
103            } else {
104                // If the reference cannot be resolved, use "??" as a placeholder
105                if let Some(area_node) = area_tree.get_mut(*area_id) {
106                    area_node.area.content = Some(AreaContent::Text("??".to_string()));
107                }
108            }
109        }
110        Ok(())
111    }
112
113    /// Layout a single FO node and its children
114    pub(in crate::layout::engine) fn layout_node(
115        &self,
116        fo_tree: &FoArena,
117        node_id: NodeId,
118        area_tree: &mut AreaTree,
119        parent_area: Option<crate::area::AreaId>,
120        resolver: &mut PageNumberResolver,
121        marker_map: &mut MarkerMap,
122    ) -> Result<Option<crate::area::AreaId>> {
123        let node = fo_tree
124            .get(node_id)
125            .ok_or_else(|| fop_types::FopError::Generic(format!("Node {} not found", node_id)))?;
126
127        let area_id = match &node.data {
128            FoNodeData::Root => {
129                // Root doesn't produce an area, just process children
130                let children = fo_tree.children(node_id);
131                for child_id in children {
132                    self.layout_node(
133                        fo_tree,
134                        child_id,
135                        area_tree,
136                        parent_area,
137                        resolver,
138                        marker_map,
139                    )?;
140                }
141                None
142            }
143
144            FoNodeData::LayoutMasterSet => {
145                // Layout master set doesn't produce areas
146                None
147            }
148
149            FoNodeData::PageSequence {
150                properties,
151                master_reference,
152                ..
153            } => {
154                // Determine page geometry from the simple-page-master
155                let master_ref = master_reference.clone();
156                let geom = self.extract_page_region_geometry(fo_tree, &master_ref);
157
158                // Create a page area (each page-sequence creates one page)
159                let page_rect = Rect::from_point_size(
160                    Point::ZERO,
161                    Size::new(geom.page_width, geom.page_height),
162                );
163
164                let mut traits = TraitSet::default();
165                if let Ok(color) = properties.get(PropertyId::BackgroundColor) {
166                    traits.background_color = color.as_color();
167                }
168
169                let area = Area::new(AreaType::Page, page_rect).with_traits(traits);
170                let area_id = area_tree.add_area(area);
171
172                // Register ID if present
173                if let Some(id) = &node.id {
174                    resolver.register_element(id.clone(), area_id);
175                }
176
177                // Process children (flow, static-content)
178                // First pass: collect static-content for all regions
179                let children = fo_tree.children(node_id);
180                let mut static_before_id = None;
181                let mut static_after_id = None;
182                let mut static_start_id = None;
183                let mut static_end_id = None;
184                let mut flow_id = None;
185
186                for child_id in children.clone() {
187                    if let Some(child) = fo_tree.get(child_id) {
188                        match &child.data {
189                            FoNodeData::StaticContent { flow_name, .. } => {
190                                match flow_name.as_str() {
191                                    "xsl-region-before" => static_before_id = Some(child_id),
192                                    "xsl-region-after" => static_after_id = Some(child_id),
193                                    "xsl-region-start" => static_start_id = Some(child_id),
194                                    "xsl-region-end" => static_end_id = Some(child_id),
195                                    _ => {}
196                                }
197                            }
198                            FoNodeData::Flow { .. } => {
199                                flow_id = Some(child_id);
200                            }
201                            _ => {}
202                        }
203                    }
204                }
205
206                // Clear markers for the new page
207                marker_map.clear();
208
209                // First pass: Collect markers from the main flow
210                if let Some(flow_node_id) = flow_id {
211                    self.collect_markers(fo_tree, flow_node_id, marker_map);
212                }
213
214                // Layout header (static-content for region-before)
215                if let Some(header_id) = static_before_id {
216                    self.layout_static_content_in_rect(
217                        fo_tree,
218                        header_id,
219                        area_tree,
220                        area_id,
221                        geom.before_rect,
222                        AreaType::Header,
223                        resolver,
224                        marker_map,
225                    )?;
226                }
227
228                // Layout footer (static-content for region-after)
229                if let Some(footer_id) = static_after_id {
230                    self.layout_static_content_in_rect(
231                        fo_tree,
232                        footer_id,
233                        area_tree,
234                        area_id,
235                        geom.after_rect,
236                        AreaType::Footer,
237                        resolver,
238                        marker_map,
239                    )?;
240                }
241
242                // Layout start sidebar (static-content for region-start)
243                if let Some(start_id) = static_start_id {
244                    self.layout_static_content_in_rect(
245                        fo_tree,
246                        start_id,
247                        area_tree,
248                        area_id,
249                        geom.start_rect,
250                        AreaType::SidebarStart,
251                        resolver,
252                        marker_map,
253                    )?;
254                }
255
256                // Layout end sidebar (static-content for region-end)
257                if let Some(end_id) = static_end_id {
258                    self.layout_static_content_in_rect(
259                        fo_tree,
260                        end_id,
261                        area_tree,
262                        area_id,
263                        geom.end_rect,
264                        AreaType::SidebarEnd,
265                        resolver,
266                        marker_map,
267                    )?;
268                }
269
270                // Layout main content (flow) using the computed body rect
271                if let Some(flow_node_id) = flow_id {
272                    self.layout_flow_in_rect(
273                        fo_tree,
274                        flow_node_id,
275                        area_tree,
276                        area_id,
277                        geom.body_rect,
278                        resolver,
279                        marker_map,
280                    )?;
281                }
282
283                // Place collected footnotes at the bottom of the page body region
284                self.place_footnotes_for_page(area_tree, area_id, geom.body_rect)?;
285
286                // Increment page counter after processing this page
287                resolver.set_current_page(resolver.current_page() + 1);
288
289                Some(area_id)
290            }
291
292            FoNodeData::Flow { properties, .. } => {
293                // Flow creates a region area
294                let flow_rect = Rect::from_point_size(
295                    Point::new(Length::from_pt(72.0), Length::from_pt(72.0)), // 1 inch margins
296                    Size::new(
297                        self.page_width - Length::from_pt(144.0),
298                        self.page_height - Length::from_pt(144.0),
299                    ),
300                );
301
302                let mut traits = TraitSet::default();
303                if let Ok(color) = properties.get(PropertyId::Color) {
304                    traits.color = color.as_color();
305                }
306
307                let area = Area::new(AreaType::Region, flow_rect).with_traits(traits);
308                let area_id = area_tree.add_area(area);
309
310                if let Some(parent) = parent_area {
311                    area_tree
312                        .append_child(parent, area_id)
313                        .map_err(fop_types::FopError::Generic)?;
314                }
315
316                // Check for multi-column layout
317                let column_count = extract_column_count(properties);
318                let column_gap = extract_column_gap(properties);
319
320                let children = fo_tree.children(node_id);
321
322                if column_count > 1 {
323                    // Use multi-column layout
324                    let mut multi_col =
325                        MultiColumnLayout::new(column_count, column_gap, flow_rect.width)
326                            .with_max_height(flow_rect.height);
327
328                    for child_id in children {
329                        self.layout_block_multicolumn(
330                            fo_tree,
331                            child_id,
332                            area_tree,
333                            area_id,
334                            &mut multi_col,
335                            resolver,
336                        )?;
337                    }
338                } else {
339                    // Single column layout with float support
340                    let mut block_ctx = BlockLayoutContext::new(flow_rect.width);
341                    let mut float_manager = FloatManager::new();
342                    let is_odd_page = resolver.current_page() % 2 == 1;
343
344                    for child_id in children {
345                        // Remove floats that have ended before the current Y position
346                        float_manager.remove_floats_above(block_ctx.current_y);
347
348                        let child_is_float = fo_tree
349                            .get(child_id)
350                            .map(|n| matches!(n.data, FoNodeData::Float { .. }))
351                            .unwrap_or(false);
352
353                        if child_is_float {
354                            self.layout_float_in_flow(
355                                fo_tree,
356                                child_id,
357                                area_tree,
358                                area_id,
359                                block_ctx.current_y,
360                                flow_rect.width,
361                                is_odd_page,
362                                &mut float_manager,
363                                resolver,
364                            )?;
365                        } else {
366                            // Apply `clear` property: advance past active floats if requested
367                            if let Some(child_node) = fo_tree.get(child_id) {
368                                if let Some(props) = child_node.data.properties() {
369                                    let clear = extract_clear(props);
370                                    block_ctx.current_y = float_manager
371                                        .get_clear_position(clear, block_ctx.current_y);
372                                }
373                            }
374
375                            let (left_offset, avail_width) =
376                                float_manager.available_width(block_ctx.current_y, flow_rect.width);
377
378                            if let Some(child_area_id) = self.layout_block_float_aware(
379                                fo_tree,
380                                child_id,
381                                area_tree,
382                                area_id,
383                                block_ctx.current_y,
384                                avail_width,
385                                left_offset,
386                                resolver,
387                            )? {
388                                if let Some(child_area) = area_tree.get(child_area_id) {
389                                    // Update context with actual height used
390                                    block_ctx.current_y =
391                                        child_area.area.geometry.y + child_area.area.height();
392                                }
393                            }
394                        }
395                    }
396
397                    float_manager.clear();
398                }
399
400                Some(area_id)
401            }
402
403            FoNodeData::Table { properties } => {
404                // Extract table-layout property
405                let layout_mode = if let Ok(prop) = properties.get(PropertyId::TableLayout) {
406                    if let Some(enum_val) = prop.as_enum() {
407                        // EN_AUTO = 9, EN_FIXED = 51
408                        if enum_val == 9 {
409                            TableLayoutMode::Auto
410                        } else {
411                            TableLayoutMode::Fixed
412                        }
413                    } else if prop.is_auto() {
414                        TableLayoutMode::Auto
415                    } else {
416                        TableLayoutMode::Fixed
417                    }
418                } else {
419                    TableLayoutMode::Fixed // Default
420                };
421
422                // Extract border-collapse property
423                let border_collapse = if let Ok(prop) = properties.get(PropertyId::BorderCollapse) {
424                    if let Some(enum_val) = prop.as_enum() {
425                        // EN_SEPARATE = 102, EN_COLLAPSE = 28
426                        if enum_val == 28 {
427                            BorderCollapse::Collapse
428                        } else {
429                            BorderCollapse::Separate
430                        }
431                    } else if let Some(string_val) = prop.as_string() {
432                        if string_val == "collapse" {
433                            BorderCollapse::Collapse
434                        } else {
435                            BorderCollapse::Separate
436                        }
437                    } else {
438                        BorderCollapse::Separate
439                    }
440                } else {
441                    BorderCollapse::Separate // Default
442                };
443
444                // Extract border-spacing property (only used for separate borders)
445                let border_spacing = if let Ok(prop) = properties.get(PropertyId::BorderSpacing) {
446                    prop.as_length().unwrap_or(Length::from_pt(0.0))
447                } else {
448                    Length::from_pt(0.0) // Default per XSL-FO spec
449                };
450
451                // Create table layout
452                let available_width = self.page_width - Length::from_pt(144.0); // 1 inch margins
453                let table_layout = TableLayout::new(available_width)
454                    .with_border_spacing(border_spacing)
455                    .with_layout_mode(layout_mode)
456                    .with_border_collapse(border_collapse);
457
458                // Extract column definitions and separate table sections
459                let children = fo_tree.children(node_id);
460                let mut column_widths = Vec::new();
461                let mut header_id = None;
462                let mut footer_id = None;
463                let mut body_ids = Vec::new();
464
465                for child_id in children.clone() {
466                    if let Some(child) = fo_tree.get(child_id) {
467                        match &child.data {
468                            FoNodeData::TableColumn { .. } => {
469                                // Parse column width from properties
470                                if let Some(props) = child.data.properties() {
471                                    if let Ok(width) = props.get(PropertyId::ColumnWidth) {
472                                        if let Some(len) = width.as_length() {
473                                            column_widths.push(ColumnWidth::Fixed(len));
474                                        } else if width.is_auto() {
475                                            column_widths.push(ColumnWidth::Auto);
476                                        }
477                                    } else {
478                                        column_widths.push(ColumnWidth::Auto);
479                                    }
480                                }
481                            }
482                            FoNodeData::TableHeader { .. } => {
483                                header_id = Some(child_id);
484                            }
485                            FoNodeData::TableFooter { .. } => {
486                                footer_id = Some(child_id);
487                            }
488                            FoNodeData::TableBody { .. } => {
489                                body_ids.push(child_id);
490                            }
491                            _ => {}
492                        }
493                    }
494                }
495
496                // If no explicit columns, use proportional layout
497                if column_widths.is_empty() {
498                    column_widths.push(ColumnWidth::Proportional(1.0));
499                }
500
501                // Compute column widths based on layout mode
502                let computed_widths = match layout_mode {
503                    TableLayoutMode::Fixed => table_layout.compute_fixed_widths(&column_widths),
504                    TableLayoutMode::Auto => {
505                        // For auto layout, we need to create ColumnInfo with measurements
506                        let mut column_info: Vec<ColumnInfo> = column_widths
507                            .iter()
508                            .map(|width_spec| ColumnInfo::new(width_spec.clone()))
509                            .collect();
510
511                        // Build grid to measure content (simplified - in full implementation
512                        // would need actual cell content measurements)
513                        let grid = table_layout.create_grid(1, column_info.len());
514                        table_layout.update_column_info_from_grid(&mut column_info, &grid);
515
516                        table_layout.compute_auto_widths(&column_info)
517                    }
518                };
519
520                // Create table area
521                let table_height = Length::from_pt(100.0); // Placeholder
522                let table_rect =
523                    Rect::new(Length::ZERO, Length::ZERO, available_width, table_height);
524
525                let mut traits = TraitSet::default();
526                if let Ok(color) = properties.get(PropertyId::BackgroundColor) {
527                    traits.background_color = color.as_color();
528                }
529
530                let area = Area::new(AreaType::Block, table_rect).with_traits(traits);
531                let table_id = area_tree.add_area(area);
532
533                if let Some(parent) = parent_area {
534                    area_tree
535                        .append_child(parent, table_id)
536                        .map_err(fop_types::FopError::Generic)?;
537                }
538
539                // Layout table with header, body, and footer
540                self.layout_table(
541                    fo_tree,
542                    area_tree,
543                    table_id,
544                    header_id,
545                    footer_id,
546                    &body_ids,
547                    &computed_widths,
548                    resolver,
549                )?;
550
551                Some(table_id)
552            }
553
554            FoNodeData::ExternalGraphic {
555                src,
556                content_width,
557                content_height,
558                scaling,
559                properties,
560            } => {
561                // Layout external-graphic as Viewport area with image data
562                if let Some(parent) = parent_area {
563                    self.layout_external_graphic(
564                        fo_tree,
565                        node_id,
566                        src,
567                        content_width.as_deref(),
568                        content_height.as_deref(),
569                        scaling.as_deref(),
570                        properties,
571                        area_tree,
572                        parent,
573                    )
574                } else {
575                    None
576                }
577            }
578
579            FoNodeData::ListBlock { properties } => {
580                // Read provisional-distance-between-starts (default 24pt per XSL-FO spec)
581                let provisional_distance = properties
582                    .get(PropertyId::ProvisionalDistanceBetweenStarts)
583                    .ok()
584                    .and_then(|v| v.as_length())
585                    .unwrap_or_else(|| Length::from_pt(24.0));
586
587                // Read provisional-label-separation (default 6pt per XSL-FO spec)
588                let provisional_label_sep = properties
589                    .get(PropertyId::ProvisionalLabelSeparation)
590                    .ok()
591                    .and_then(|v| v.as_length())
592                    .unwrap_or_else(|| Length::from_pt(6.0));
593
594                // space-after on the list block
595                let list_space_after = extract_space_after(properties);
596
597                // Determine available width from parent area or fallback to page width minus margins
598                let available_width = if let Some(parent) = parent_area {
599                    area_tree
600                        .get(parent)
601                        .map(|n| n.area.width())
602                        .filter(|w| *w > Length::ZERO)
603                        .unwrap_or_else(|| self.page_width - Length::from_pt(144.0))
604                } else {
605                    self.page_width - Length::from_pt(144.0)
606                };
607
608                // provisional-distance-between-starts sets the label width;
609                // label-end() = start_indent + provisional-distance-between-starts - provisional-label-separation
610                // body-start() = start_indent + provisional-distance-between-starts
611                // For start_indent=0: label width = provisional_distance - provisional_label_sep
612                let label_end = provisional_distance - provisional_label_sep;
613                let body_start = provisional_distance;
614
615                let list_layout = ListLayout::new(available_width)
616                    .with_label_width(label_end)
617                    .with_label_separation(provisional_label_sep)
618                    .with_body_start(body_start);
619
620                let mut traits = TraitSet::default();
621                if let Ok(color) = properties.get(PropertyId::Color) {
622                    traits.color = color.as_color();
623                }
624                if let Ok(bg) = properties.get(PropertyId::BackgroundColor) {
625                    traits.background_color = bg.as_color();
626                }
627
628                // Initial placeholder rect (height will be updated after layout)
629                let list_rect = Rect::new(
630                    Length::ZERO,
631                    Length::ZERO,
632                    available_width,
633                    Length::from_pt(10.0),
634                );
635
636                let area = Area::new(AreaType::Block, list_rect).with_traits(traits);
637                let list_id = area_tree.add_area(area);
638
639                if let Some(parent) = parent_area {
640                    area_tree
641                        .append_child(parent, list_id)
642                        .map_err(fop_types::FopError::Generic)?;
643                }
644
645                // Layout list items
646                let children = fo_tree.children(node_id);
647                let mut item_y = Length::ZERO;
648                let mut item_index = 0usize;
649
650                for child_id in children.iter() {
651                    if let Some(child) = fo_tree.get(*child_id) {
652                        if matches!(child.data, FoNodeData::ListItem { .. }) {
653                            item_index += 1;
654                            item_y = self.layout_list_item(
655                                fo_tree,
656                                *child_id,
657                                area_tree,
658                                list_id,
659                                item_y,
660                                item_index,
661                                &list_layout,
662                                resolver,
663                            )?;
664                        }
665                    }
666                }
667
668                // Update list block height to match actual content
669                let total_height = item_y + list_space_after;
670                if let Some(list_node) = area_tree.get_mut(list_id) {
671                    list_node.area.geometry.height = total_height.max(Length::from_pt(10.0));
672                }
673
674                Some(list_id)
675            }
676
677            FoNodeData::Float { properties } => {
678                // Extract float property to determine side
679                let float_side = if let Ok(prop) = properties.get(PropertyId::Float) {
680                    if let Some(enum_val) = prop.as_enum() {
681                        // Float enum values from XSL-FO:
682                        // EN_BEFORE = 11, EN_START = 104, EN_END = 45, EN_LEFT = 66, EN_RIGHT = 96, EN_NONE = 77
683                        match enum_val {
684                            66 => FloatSide::Left,
685                            96 => FloatSide::Right,
686                            104 => FloatSide::Start,
687                            45 => FloatSide::End,
688                            _ => FloatSide::None,
689                        }
690                    } else if let Some(string_val) = prop.as_string() {
691                        match string_val {
692                            "left" => FloatSide::Left,
693                            "right" => FloatSide::Right,
694                            "start" => FloatSide::Start,
695                            "end" => FloatSide::End,
696                            "inside" => FloatSide::Inside,
697                            "outside" => FloatSide::Outside,
698                            _ => FloatSide::None,
699                        }
700                    } else {
701                        FloatSide::None
702                    }
703                } else {
704                    FloatSide::None
705                };
706
707                // If float is None, treat as regular block
708                if float_side == FloatSide::None {
709                    return Ok(None);
710                }
711
712                // Create a float area with measured width
713                let traits = extract_traits(properties);
714
715                // Measure float width from children (fallback: 1/3 of page width)
716                let container_w = self.page_width - Length::from_pt(144.0);
717                let float_width = self.measure_float_width(fo_tree, node_id, container_w);
718
719                // X-position defaults to left edge; proper positioning is handled
720                // in layout_float_in_flow when called from a flow context.
721                let float_rect = Rect::from_point_size(
722                    Point::ZERO,
723                    Size::new(float_width, Length::from_pt(1.0)),
724                );
725
726                let area = Area::new(AreaType::FloatArea, float_rect).with_traits(traits);
727                let float_area_id = area_tree.add_area(area);
728
729                if let Some(parent) = parent_area {
730                    area_tree
731                        .append_child(parent, float_area_id)
732                        .map_err(fop_types::FopError::Generic)?;
733                }
734
735                // Layout children within the float
736                let children = fo_tree.children(node_id);
737                let mut float_ctx = BlockLayoutContext::new(float_width);
738
739                for child_id in children {
740                    if let Some(child_area_id) = self.layout_block(
741                        fo_tree,
742                        child_id,
743                        area_tree,
744                        float_area_id,
745                        float_ctx.current_y,
746                        float_width,
747                        resolver,
748                    )? {
749                        if let Some(child_area) = area_tree.get(child_area_id) {
750                            float_ctx.current_y =
751                                child_area.area.geometry.y + child_area.area.height();
752                        }
753                    }
754                }
755
756                // Update float area height based on content
757                let float_height = if float_ctx.current_y > Length::ZERO {
758                    float_ctx.current_y
759                } else {
760                    Length::from_pt(50.0)
761                };
762                if let Some(float_area_node) = area_tree.get_mut(float_area_id) {
763                    float_area_node.area.geometry.height = float_height;
764                }
765
766                Some(float_area_id)
767            }
768
769            FoNodeData::BlockContainer { .. } => {
770                // fo:block-container: process children in current context
771                // (absolute positioning would be handled separately)
772                let children = fo_tree.children(node_id);
773                for child_id in children {
774                    self.layout_node(
775                        fo_tree,
776                        child_id,
777                        area_tree,
778                        parent_area,
779                        resolver,
780                        marker_map,
781                    )?;
782                }
783                None
784            }
785
786            FoNodeData::Wrapper { .. }
787            | FoNodeData::Declarations
788            | FoNodeData::ColorProfile { .. }
789            | FoNodeData::PageSequenceMaster { .. }
790            | FoNodeData::UnsupportedElement { .. } => {
791                // These elements don't directly produce areas
792                None
793            }
794
795            _ => {
796                // Other nodes don't produce areas yet
797                None
798            }
799        };
800
801        Ok(area_id)
802    }
803}
804
805impl Default for LayoutEngine {
806    fn default() -> Self {
807        Self::new()
808    }
809}
810
811#[cfg(test)]
812mod tests {
813    use super::types::{FloatInfo, FloatSide};
814    use super::*;
815    use fop_core::{FoNode, FoNodeData, PropertyList, PropertyValue};
816
817    #[test]
818    fn test_engine_creation() {
819        let engine = LayoutEngine::new();
820        assert_eq!(engine.page_width, Length::from_mm(210.0));
821        assert_eq!(engine.page_height, Length::from_mm(297.0));
822    }
823
824    #[test]
825    fn test_simple_layout() {
826        let mut fo_tree = FoArena::new();
827
828        // Create simple FO tree: root -> page-sequence -> flow -> block
829        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
830
831        let page_seq = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
832            master_reference: "A4".to_string(),
833            format: "1".to_string(),
834            grouping_separator: None,
835            grouping_size: None,
836            properties: PropertyList::new(),
837        }));
838        fo_tree
839            .append_child(root, page_seq)
840            .expect("test: should succeed");
841
842        let flow = fo_tree.add_node(FoNode::new(FoNodeData::Flow {
843            flow_name: "xsl-region-body".to_string(),
844            properties: PropertyList::new(),
845        }));
846        fo_tree
847            .append_child(page_seq, flow)
848            .expect("test: should succeed");
849
850        let block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
851            properties: PropertyList::new(),
852        }));
853        fo_tree
854            .append_child(flow, block)
855            .expect("test: should succeed");
856
857        // Layout
858        let engine = LayoutEngine::new();
859        let area_tree = engine.layout(&fo_tree).expect("test: should succeed");
860
861        // Should have created areas
862        assert!(!area_tree.is_empty());
863    }
864
865    #[test]
866    fn test_static_content_header() {
867        let mut fo_tree = FoArena::new();
868
869        // Create FO tree with header static-content
870        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
871
872        let page_seq = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
873            master_reference: "A4".to_string(),
874            format: "1".to_string(),
875            grouping_separator: None,
876            grouping_size: None,
877            properties: PropertyList::new(),
878        }));
879        fo_tree
880            .append_child(root, page_seq)
881            .expect("test: should succeed");
882
883        // Add static-content for header
884        let header = fo_tree.add_node(FoNode::new(FoNodeData::StaticContent {
885            flow_name: "xsl-region-before".to_string(),
886            properties: PropertyList::new(),
887        }));
888        fo_tree
889            .append_child(page_seq, header)
890            .expect("test: should succeed");
891
892        // Add block to header
893        let header_block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
894            properties: PropertyList::new(),
895        }));
896        fo_tree
897            .append_child(header, header_block)
898            .expect("test: should succeed");
899
900        // Add text to header block
901        let header_text =
902            fo_tree.add_node(FoNode::new(FoNodeData::Text("Header Text".to_string())));
903        fo_tree
904            .append_child(header_block, header_text)
905            .expect("test: should succeed");
906
907        // Add flow
908        let flow = fo_tree.add_node(FoNode::new(FoNodeData::Flow {
909            flow_name: "xsl-region-body".to_string(),
910            properties: PropertyList::new(),
911        }));
912        fo_tree
913            .append_child(page_seq, flow)
914            .expect("test: should succeed");
915
916        let block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
917            properties: PropertyList::new(),
918        }));
919        fo_tree
920            .append_child(flow, block)
921            .expect("test: should succeed");
922
923        // Layout
924        let engine = LayoutEngine::new();
925        let area_tree = engine.layout(&fo_tree).expect("test: should succeed");
926
927        // Check for header area
928        let mut has_header = false;
929        for (_, node) in area_tree.iter() {
930            if matches!(node.area.area_type, AreaType::Header) {
931                has_header = true;
932                break;
933            }
934        }
935        assert!(has_header, "Should have created a header area");
936    }
937
938    #[test]
939    fn test_static_content_footer() {
940        let mut fo_tree = FoArena::new();
941
942        // Create FO tree with footer static-content
943        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
944
945        let page_seq = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
946            master_reference: "A4".to_string(),
947            format: "1".to_string(),
948            grouping_separator: None,
949            grouping_size: None,
950            properties: PropertyList::new(),
951        }));
952        fo_tree
953            .append_child(root, page_seq)
954            .expect("test: should succeed");
955
956        // Add static-content for footer
957        let footer = fo_tree.add_node(FoNode::new(FoNodeData::StaticContent {
958            flow_name: "xsl-region-after".to_string(),
959            properties: PropertyList::new(),
960        }));
961        fo_tree
962            .append_child(page_seq, footer)
963            .expect("test: should succeed");
964
965        // Add block to footer
966        let footer_block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
967            properties: PropertyList::new(),
968        }));
969        fo_tree
970            .append_child(footer, footer_block)
971            .expect("test: should succeed");
972
973        // Add text to footer block
974        let footer_text =
975            fo_tree.add_node(FoNode::new(FoNodeData::Text("Footer Text".to_string())));
976        fo_tree
977            .append_child(footer_block, footer_text)
978            .expect("test: should succeed");
979
980        // Add flow
981        let flow = fo_tree.add_node(FoNode::new(FoNodeData::Flow {
982            flow_name: "xsl-region-body".to_string(),
983            properties: PropertyList::new(),
984        }));
985        fo_tree
986            .append_child(page_seq, flow)
987            .expect("test: should succeed");
988
989        let block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
990            properties: PropertyList::new(),
991        }));
992        fo_tree
993            .append_child(flow, block)
994            .expect("test: should succeed");
995
996        // Layout
997        let engine = LayoutEngine::new();
998        let area_tree = engine.layout(&fo_tree).expect("test: should succeed");
999
1000        // Check for footer area
1001        let mut has_footer = false;
1002        for (_, node) in area_tree.iter() {
1003            if matches!(node.area.area_type, AreaType::Footer) {
1004                has_footer = true;
1005                break;
1006            }
1007        }
1008        assert!(has_footer, "Should have created a footer area");
1009    }
1010
1011    #[test]
1012    fn test_static_content_both_header_and_footer() {
1013        let mut fo_tree = FoArena::new();
1014
1015        // Create FO tree with both header and footer
1016        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
1017
1018        let page_seq = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
1019            master_reference: "A4".to_string(),
1020            format: "1".to_string(),
1021            grouping_separator: None,
1022            grouping_size: None,
1023            properties: PropertyList::new(),
1024        }));
1025        fo_tree
1026            .append_child(root, page_seq)
1027            .expect("test: should succeed");
1028
1029        // Add header
1030        let header = fo_tree.add_node(FoNode::new(FoNodeData::StaticContent {
1031            flow_name: "xsl-region-before".to_string(),
1032            properties: PropertyList::new(),
1033        }));
1034        fo_tree
1035            .append_child(page_seq, header)
1036            .expect("test: should succeed");
1037
1038        let header_block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
1039            properties: PropertyList::new(),
1040        }));
1041        fo_tree
1042            .append_child(header, header_block)
1043            .expect("test: should succeed");
1044
1045        // Add footer
1046        let footer = fo_tree.add_node(FoNode::new(FoNodeData::StaticContent {
1047            flow_name: "xsl-region-after".to_string(),
1048            properties: PropertyList::new(),
1049        }));
1050        fo_tree
1051            .append_child(page_seq, footer)
1052            .expect("test: should succeed");
1053
1054        let footer_block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
1055            properties: PropertyList::new(),
1056        }));
1057        fo_tree
1058            .append_child(footer, footer_block)
1059            .expect("test: should succeed");
1060
1061        // Add flow
1062        let flow = fo_tree.add_node(FoNode::new(FoNodeData::Flow {
1063            flow_name: "xsl-region-body".to_string(),
1064            properties: PropertyList::new(),
1065        }));
1066        fo_tree
1067            .append_child(page_seq, flow)
1068            .expect("test: should succeed");
1069
1070        let block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
1071            properties: PropertyList::new(),
1072        }));
1073        fo_tree
1074            .append_child(flow, block)
1075            .expect("test: should succeed");
1076
1077        // Layout
1078        let engine = LayoutEngine::new();
1079        let area_tree = engine.layout(&fo_tree).expect("test: should succeed");
1080
1081        // Check for both header and footer areas
1082        let mut has_header = false;
1083        let mut has_footer = false;
1084        for (_, node) in area_tree.iter() {
1085            match node.area.area_type {
1086                AreaType::Header => has_header = true,
1087                AreaType::Footer => has_footer = true,
1088                _ => {}
1089            }
1090        }
1091        assert!(has_header, "Should have created a header area");
1092        assert!(has_footer, "Should have created a footer area");
1093    }
1094
1095    #[test]
1096    fn test_float_manager_basic() {
1097        let mut fm = FloatManager::new();
1098        let container_width = Length::from_pt(400.0);
1099
1100        // At y=0 with no floats, full width is available
1101        let (left_off, avail) = fm.available_width(Length::ZERO, container_width);
1102        assert_eq!(left_off, Length::ZERO);
1103        assert_eq!(avail, container_width);
1104
1105        // Add a left float: 100pt wide, from y=0 to y=50
1106        let float_area_id = crate::area::AreaId::from_index(0);
1107        fm.add_float(
1108            FloatInfo {
1109                area_id: float_area_id,
1110                side: FloatSide::Left,
1111                top: Length::ZERO,
1112                bottom: Length::from_pt(50.0),
1113                width: Length::from_pt(100.0),
1114            },
1115            true, // is_odd_page
1116        );
1117
1118        // At y=10, available width should be reduced by 100pt and left_offset=100pt
1119        let (left_off, avail) = fm.available_width(Length::from_pt(10.0), container_width);
1120        assert_eq!(left_off, Length::from_pt(100.0));
1121        assert_eq!(avail, Length::from_pt(300.0));
1122
1123        // At y=60 (past float bottom), full width again
1124        let (left_off, avail) = fm.available_width(Length::from_pt(60.0), container_width);
1125        assert_eq!(left_off, Length::ZERO);
1126        assert_eq!(avail, container_width);
1127    }
1128
1129    #[test]
1130    fn test_float_manager_right_float() {
1131        let mut fm = FloatManager::new();
1132        let container_width = Length::from_pt(400.0);
1133
1134        // Add a right float: 120pt wide, from y=0 to y=80
1135        let float_area_id = crate::area::AreaId::from_index(0);
1136        fm.add_float(
1137            FloatInfo {
1138                area_id: float_area_id,
1139                side: FloatSide::Right,
1140                top: Length::ZERO,
1141                bottom: Length::from_pt(80.0),
1142                width: Length::from_pt(120.0),
1143            },
1144            true,
1145        );
1146
1147        // Left offset should be 0, available width reduced by 120pt
1148        let (left_off, avail) = fm.available_width(Length::from_pt(20.0), container_width);
1149        assert_eq!(left_off, Length::ZERO);
1150        assert_eq!(avail, Length::from_pt(280.0));
1151    }
1152
1153    #[test]
1154    fn test_float_manager_remove_expired() {
1155        let mut fm = FloatManager::new();
1156        let container_width = Length::from_pt(400.0);
1157
1158        let float_area_id = crate::area::AreaId::from_index(0);
1159        fm.add_float(
1160            FloatInfo {
1161                area_id: float_area_id,
1162                side: FloatSide::Left,
1163                top: Length::ZERO,
1164                bottom: Length::from_pt(50.0),
1165                width: Length::from_pt(100.0),
1166            },
1167            true,
1168        );
1169
1170        // Remove floats above y=60 (float ends at 50, so it is removed)
1171        fm.remove_floats_above(Length::from_pt(60.0));
1172
1173        // Now full width should be available
1174        let (left_off, avail) = fm.available_width(Length::from_pt(60.0), container_width);
1175        assert_eq!(left_off, Length::ZERO);
1176        assert_eq!(avail, container_width);
1177    }
1178
1179    #[test]
1180    fn test_layout_with_float_node() {
1181        let mut fo_tree = FoArena::new();
1182
1183        // Build: root -> page-sequence -> flow -> [float(left), block]
1184        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
1185
1186        let page_seq = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
1187            master_reference: "A4".to_string(),
1188            format: "1".to_string(),
1189            grouping_separator: None,
1190            grouping_size: None,
1191            properties: PropertyList::new(),
1192        }));
1193        fo_tree
1194            .append_child(root, page_seq)
1195            .expect("test: should succeed");
1196
1197        let flow = fo_tree.add_node(FoNode::new(FoNodeData::Flow {
1198            flow_name: "xsl-region-body".to_string(),
1199            properties: PropertyList::new(),
1200        }));
1201        fo_tree
1202            .append_child(page_seq, flow)
1203            .expect("test: should succeed");
1204
1205        // Add a float element with "left" side (EN_LEFT = 66)
1206        let mut float_props = PropertyList::new();
1207        float_props.set(PropertyId::Float, PropertyValue::Enum(66));
1208        let float_node = fo_tree.add_node(FoNode::new(FoNodeData::Float {
1209            properties: float_props,
1210        }));
1211        fo_tree
1212            .append_child(flow, float_node)
1213            .expect("test: should succeed");
1214
1215        // Add a block inside the float
1216        let float_block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
1217            properties: PropertyList::new(),
1218        }));
1219        fo_tree
1220            .append_child(float_node, float_block)
1221            .expect("test: should succeed");
1222        let float_text =
1223            fo_tree.add_node(FoNode::new(FoNodeData::Text("Float content".to_string())));
1224        fo_tree
1225            .append_child(float_block, float_text)
1226            .expect("test: should succeed");
1227
1228        // Add a regular block after the float
1229        let block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
1230            properties: PropertyList::new(),
1231        }));
1232        fo_tree
1233            .append_child(flow, block)
1234            .expect("test: should succeed");
1235        let text = fo_tree.add_node(FoNode::new(FoNodeData::Text("Normal text".to_string())));
1236        fo_tree
1237            .append_child(block, text)
1238            .expect("test: should succeed");
1239
1240        // Layout
1241        let engine = LayoutEngine::new();
1242        let area_tree = engine.layout(&fo_tree).expect("test: should succeed");
1243
1244        // Should have produced areas
1245        assert!(!area_tree.is_empty());
1246
1247        // Should have a FloatArea
1248        let mut has_float_area = false;
1249        for (_, node) in area_tree.iter() {
1250            if matches!(node.area.area_type, AreaType::FloatArea) {
1251                has_float_area = true;
1252                break;
1253            }
1254        }
1255        assert!(has_float_area, "Should have created a FloatArea");
1256    }
1257
1258    #[test]
1259    fn test_float_manager_clear_position() {
1260        let mut fm = FloatManager::new();
1261
1262        // Add a left float ending at y=80
1263        let float_area_id = crate::area::AreaId::from_index(0);
1264        fm.add_float(
1265            FloatInfo {
1266                area_id: float_area_id,
1267                side: FloatSide::Left,
1268                top: Length::ZERO,
1269                bottom: Length::from_pt(80.0),
1270                width: Length::from_pt(100.0),
1271            },
1272            true,
1273        );
1274
1275        // Add a right float ending at y=60
1276        let float_area_id2 = crate::area::AreaId::from_index(1);
1277        fm.add_float(
1278            FloatInfo {
1279                area_id: float_area_id2,
1280                side: FloatSide::Right,
1281                top: Length::ZERO,
1282                bottom: Length::from_pt(60.0),
1283                width: Length::from_pt(80.0),
1284            },
1285            true,
1286        );
1287
1288        let current_y = Length::from_pt(10.0);
1289
1290        // clear: left → advance past left float bottom (80pt)
1291        assert_eq!(
1292            fm.get_clear_position(ClearSide::Left, current_y),
1293            Length::from_pt(80.0)
1294        );
1295
1296        // clear: right → advance past right float bottom (60pt)
1297        assert_eq!(
1298            fm.get_clear_position(ClearSide::Right, current_y),
1299            Length::from_pt(60.0)
1300        );
1301
1302        // clear: both → advance past max(80, 60) = 80pt
1303        assert_eq!(
1304            fm.get_clear_position(ClearSide::Both, current_y),
1305            Length::from_pt(80.0)
1306        );
1307
1308        // clear: none → stay at current_y
1309        assert_eq!(fm.get_clear_position(ClearSide::None, current_y), current_y);
1310    }
1311
1312    #[test]
1313    fn test_layout_with_float_and_clear() {
1314        let mut fo_tree = FoArena::new();
1315
1316        // Build: root -> page-sequence -> flow -> [float(right), block(clear:right)]
1317        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
1318
1319        let page_seq = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
1320            master_reference: "A4".to_string(),
1321            format: "1".to_string(),
1322            grouping_separator: None,
1323            grouping_size: None,
1324            properties: PropertyList::new(),
1325        }));
1326        fo_tree
1327            .append_child(root, page_seq)
1328            .expect("test: should succeed");
1329
1330        let flow = fo_tree.add_node(FoNode::new(FoNodeData::Flow {
1331            flow_name: "xsl-region-body".to_string(),
1332            properties: PropertyList::new(),
1333        }));
1334        fo_tree
1335            .append_child(page_seq, flow)
1336            .expect("test: should succeed");
1337
1338        // Float on the right
1339        let mut float_props = PropertyList::new();
1340        float_props.set(PropertyId::Float, PropertyValue::Enum(96)); // EN_RIGHT = 96
1341        let float_node = fo_tree.add_node(FoNode::new(FoNodeData::Float {
1342            properties: float_props,
1343        }));
1344        fo_tree
1345            .append_child(flow, float_node)
1346            .expect("test: should succeed");
1347
1348        let float_block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
1349            properties: PropertyList::new(),
1350        }));
1351        fo_tree
1352            .append_child(float_node, float_block)
1353            .expect("test: should succeed");
1354
1355        // Block with clear:right (EN_RIGHT = 96)
1356        let mut clear_props = PropertyList::new();
1357        clear_props.set(PropertyId::Clear, PropertyValue::Enum(96)); // EN_RIGHT = 96
1358        let clear_block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
1359            properties: clear_props,
1360        }));
1361        fo_tree
1362            .append_child(flow, clear_block)
1363            .expect("test: should succeed");
1364        let text = fo_tree.add_node(FoNode::new(FoNodeData::Text("After float".to_string())));
1365        fo_tree
1366            .append_child(clear_block, text)
1367            .expect("test: should succeed");
1368
1369        // Layout – should not panic
1370        let engine = LayoutEngine::new();
1371        let area_tree = engine.layout(&fo_tree).expect("test: should succeed");
1372
1373        assert!(!area_tree.is_empty());
1374
1375        // Should have a FloatArea
1376        let mut has_float = false;
1377        for (_, node) in area_tree.iter() {
1378            if matches!(node.area.area_type, AreaType::FloatArea) {
1379                has_float = true;
1380                break;
1381            }
1382        }
1383        assert!(has_float, "Expected a FloatArea in the area tree");
1384    }
1385
1386    #[test]
1387    fn test_layout_with_footnote() {
1388        let mut fo_tree = FoArena::new();
1389
1390        // Build: root -> page-sequence -> flow -> block -> footnote
1391        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
1392
1393        let page_seq = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
1394            master_reference: "A4".to_string(),
1395            format: "1".to_string(),
1396            grouping_separator: None,
1397            grouping_size: None,
1398            properties: PropertyList::new(),
1399        }));
1400        fo_tree
1401            .append_child(root, page_seq)
1402            .expect("test: should succeed");
1403
1404        let flow = fo_tree.add_node(FoNode::new(FoNodeData::Flow {
1405            flow_name: "xsl-region-body".to_string(),
1406            properties: PropertyList::new(),
1407        }));
1408        fo_tree
1409            .append_child(page_seq, flow)
1410            .expect("test: should succeed");
1411
1412        let block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
1413            properties: PropertyList::new(),
1414        }));
1415        fo_tree
1416            .append_child(flow, block)
1417            .expect("test: should succeed");
1418
1419        // Add text before footnote
1420        let text_before = fo_tree.add_node(FoNode::new(FoNodeData::Text("Text with ".to_string())));
1421        fo_tree
1422            .append_child(block, text_before)
1423            .expect("test: should succeed");
1424
1425        // Add fo:footnote with inline reference mark and body
1426        let footnote = fo_tree.add_node(FoNode::new(FoNodeData::Footnote {
1427            properties: PropertyList::new(),
1428        }));
1429        fo_tree
1430            .append_child(block, footnote)
1431            .expect("test: should succeed");
1432
1433        // fo:inline child (reference mark "1")
1434        let ref_mark = fo_tree.add_node(FoNode::new(FoNodeData::Inline {
1435            properties: PropertyList::new(),
1436        }));
1437        fo_tree
1438            .append_child(footnote, ref_mark)
1439            .expect("test: should succeed");
1440
1441        let ref_text = fo_tree.add_node(FoNode::new(FoNodeData::Text("1".to_string())));
1442        fo_tree
1443            .append_child(ref_mark, ref_text)
1444            .expect("test: should succeed");
1445
1446        // fo:footnote-body child
1447        let footnote_body = fo_tree.add_node(FoNode::new(FoNodeData::FootnoteBody {
1448            properties: PropertyList::new(),
1449        }));
1450        fo_tree
1451            .append_child(footnote, footnote_body)
1452            .expect("test: should succeed");
1453
1454        // Block inside footnote-body
1455        let body_block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
1456            properties: PropertyList::new(),
1457        }));
1458        fo_tree
1459            .append_child(footnote_body, body_block)
1460            .expect("test: should succeed");
1461
1462        let body_text = fo_tree.add_node(FoNode::new(FoNodeData::Text(
1463            "1. The footnote text.".to_string(),
1464        )));
1465        fo_tree
1466            .append_child(body_block, body_text)
1467            .expect("test: should succeed");
1468
1469        // Layout
1470        let engine = LayoutEngine::new();
1471        let area_tree = engine.layout(&fo_tree).expect("test: should succeed");
1472
1473        // Should have produced areas
1474        assert!(!area_tree.is_empty());
1475
1476        // Should have a Footnote area
1477        let mut has_footnote_area = false;
1478        for (_, node) in area_tree.iter() {
1479            if matches!(node.area.area_type, AreaType::Footnote) {
1480                has_footnote_area = true;
1481                break;
1482            }
1483        }
1484        assert!(has_footnote_area, "Should have created a Footnote area");
1485    }
1486
1487    #[test]
1488    fn test_static_content_sidebar_start() {
1489        let mut fo_tree = FoArena::new();
1490
1491        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
1492
1493        let page_seq = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
1494            master_reference: "A4".to_string(),
1495            format: "1".to_string(),
1496            grouping_separator: None,
1497            grouping_size: None,
1498            properties: PropertyList::new(),
1499        }));
1500        fo_tree
1501            .append_child(root, page_seq)
1502            .expect("test: should succeed");
1503
1504        // Add static-content for region-start (left sidebar)
1505        let sidebar_start = fo_tree.add_node(FoNode::new(FoNodeData::StaticContent {
1506            flow_name: "xsl-region-start".to_string(),
1507            properties: PropertyList::new(),
1508        }));
1509        fo_tree
1510            .append_child(page_seq, sidebar_start)
1511            .expect("test: should succeed");
1512
1513        let sidebar_block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
1514            properties: PropertyList::new(),
1515        }));
1516        fo_tree
1517            .append_child(sidebar_start, sidebar_block)
1518            .expect("test: should succeed");
1519
1520        // Add flow
1521        let flow = fo_tree.add_node(FoNode::new(FoNodeData::Flow {
1522            flow_name: "xsl-region-body".to_string(),
1523            properties: PropertyList::new(),
1524        }));
1525        fo_tree
1526            .append_child(page_seq, flow)
1527            .expect("test: should succeed");
1528
1529        let block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
1530            properties: PropertyList::new(),
1531        }));
1532        fo_tree
1533            .append_child(flow, block)
1534            .expect("test: should succeed");
1535
1536        let engine = LayoutEngine::new();
1537        let area_tree = engine.layout(&fo_tree).expect("test: should succeed");
1538
1539        let mut has_sidebar_start = false;
1540        for (_, node) in area_tree.iter() {
1541            if matches!(node.area.area_type, AreaType::SidebarStart) {
1542                has_sidebar_start = true;
1543                break;
1544            }
1545        }
1546        assert!(has_sidebar_start, "Should have created a SidebarStart area");
1547    }
1548
1549    #[test]
1550    fn test_static_content_sidebar_end() {
1551        let mut fo_tree = FoArena::new();
1552
1553        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
1554
1555        let page_seq = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
1556            master_reference: "A4".to_string(),
1557            format: "1".to_string(),
1558            grouping_separator: None,
1559            grouping_size: None,
1560            properties: PropertyList::new(),
1561        }));
1562        fo_tree
1563            .append_child(root, page_seq)
1564            .expect("test: should succeed");
1565
1566        // Add static-content for region-end (right sidebar)
1567        let sidebar_end = fo_tree.add_node(FoNode::new(FoNodeData::StaticContent {
1568            flow_name: "xsl-region-end".to_string(),
1569            properties: PropertyList::new(),
1570        }));
1571        fo_tree
1572            .append_child(page_seq, sidebar_end)
1573            .expect("test: should succeed");
1574
1575        let sidebar_block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
1576            properties: PropertyList::new(),
1577        }));
1578        fo_tree
1579            .append_child(sidebar_end, sidebar_block)
1580            .expect("test: should succeed");
1581
1582        // Add flow
1583        let flow = fo_tree.add_node(FoNode::new(FoNodeData::Flow {
1584            flow_name: "xsl-region-body".to_string(),
1585            properties: PropertyList::new(),
1586        }));
1587        fo_tree
1588            .append_child(page_seq, flow)
1589            .expect("test: should succeed");
1590
1591        let block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
1592            properties: PropertyList::new(),
1593        }));
1594        fo_tree
1595            .append_child(flow, block)
1596            .expect("test: should succeed");
1597
1598        let engine = LayoutEngine::new();
1599        let area_tree = engine.layout(&fo_tree).expect("test: should succeed");
1600
1601        let mut has_sidebar_end = false;
1602        for (_, node) in area_tree.iter() {
1603            if matches!(node.area.area_type, AreaType::SidebarEnd) {
1604                has_sidebar_end = true;
1605                break;
1606            }
1607        }
1608        assert!(has_sidebar_end, "Should have created a SidebarEnd area");
1609    }
1610
1611    #[test]
1612    fn test_page_region_geometry_from_simple_page_master() {
1613        use fop_core::PropertyValue;
1614        use fop_types::Length;
1615
1616        let mut fo_tree = FoArena::new();
1617
1618        // Build: root -> layout-master-set -> simple-page-master
1619        //           with: region-before(extent=30pt), region-after(extent=20pt),
1620        //                 region-start(extent=50pt), region-end(extent=40pt)
1621        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
1622
1623        let lms = fo_tree.add_node(FoNode::new(FoNodeData::LayoutMasterSet));
1624        fo_tree
1625            .append_child(root, lms)
1626            .expect("test: should succeed");
1627
1628        let mut spm_props = PropertyList::new();
1629        spm_props.set(
1630            PropertyId::PageWidth,
1631            PropertyValue::Length(Length::from_pt(595.0)),
1632        );
1633        spm_props.set(
1634            PropertyId::PageHeight,
1635            PropertyValue::Length(Length::from_pt(842.0)),
1636        );
1637        spm_props.set(
1638            PropertyId::MarginTop,
1639            PropertyValue::Length(Length::from_pt(50.0)),
1640        );
1641        spm_props.set(
1642            PropertyId::MarginBottom,
1643            PropertyValue::Length(Length::from_pt(50.0)),
1644        );
1645        spm_props.set(
1646            PropertyId::MarginLeft,
1647            PropertyValue::Length(Length::from_pt(60.0)),
1648        );
1649        spm_props.set(
1650            PropertyId::MarginRight,
1651            PropertyValue::Length(Length::from_pt(60.0)),
1652        );
1653
1654        let spm = fo_tree.add_node(FoNode::new(FoNodeData::SimplePageMaster {
1655            master_name: "test-master".to_string(),
1656            properties: spm_props,
1657        }));
1658        fo_tree
1659            .append_child(lms, spm)
1660            .expect("test: should succeed");
1661
1662        // region-before extent=30pt
1663        let mut rb_props = PropertyList::new();
1664        rb_props.set(
1665            PropertyId::Extent,
1666            PropertyValue::Length(Length::from_pt(30.0)),
1667        );
1668        let rb = fo_tree.add_node(FoNode::new(FoNodeData::RegionBefore {
1669            properties: rb_props,
1670        }));
1671        fo_tree.append_child(spm, rb).expect("test: should succeed");
1672
1673        // region-after extent=20pt
1674        let mut ra_props = PropertyList::new();
1675        ra_props.set(
1676            PropertyId::Extent,
1677            PropertyValue::Length(Length::from_pt(20.0)),
1678        );
1679        let ra = fo_tree.add_node(FoNode::new(FoNodeData::RegionAfter {
1680            properties: ra_props,
1681        }));
1682        fo_tree.append_child(spm, ra).expect("test: should succeed");
1683
1684        // region-start extent=50pt
1685        let mut rs_props = PropertyList::new();
1686        rs_props.set(
1687            PropertyId::Extent,
1688            PropertyValue::Length(Length::from_pt(50.0)),
1689        );
1690        let rs = fo_tree.add_node(FoNode::new(FoNodeData::RegionStart {
1691            properties: rs_props,
1692        }));
1693        fo_tree.append_child(spm, rs).expect("test: should succeed");
1694
1695        // region-end extent=40pt
1696        let mut re_props = PropertyList::new();
1697        re_props.set(
1698            PropertyId::Extent,
1699            PropertyValue::Length(Length::from_pt(40.0)),
1700        );
1701        let re = fo_tree.add_node(FoNode::new(FoNodeData::RegionEnd {
1702            properties: re_props,
1703        }));
1704        fo_tree.append_child(spm, re).expect("test: should succeed");
1705
1706        // region-body (no inner margins)
1707        let body_region = fo_tree.add_node(FoNode::new(FoNodeData::RegionBody {
1708            properties: PropertyList::new(),
1709        }));
1710        fo_tree
1711            .append_child(spm, body_region)
1712            .expect("test: should succeed");
1713
1714        let engine = LayoutEngine::new();
1715        let geom = engine.extract_page_region_geometry(&fo_tree, "test-master");
1716
1717        // Page dimensions
1718        assert_eq!(geom.page_width, Length::from_pt(595.0));
1719        assert_eq!(geom.page_height, Length::from_pt(842.0));
1720
1721        // Content area: x=60, y=50, w=595-120=475, h=842-100=742
1722        // region-before: x=60, y=50, w=475, h=30
1723        assert_eq!(geom.before_rect.x, Length::from_pt(60.0));
1724        assert_eq!(geom.before_rect.y, Length::from_pt(50.0));
1725        assert_eq!(geom.before_rect.width, Length::from_pt(475.0));
1726        assert_eq!(geom.before_rect.height, Length::from_pt(30.0));
1727
1728        // region-after: x=60, y=50+742-20=772, w=475, h=20
1729        assert_eq!(geom.after_rect.x, Length::from_pt(60.0));
1730        assert_eq!(geom.after_rect.y, Length::from_pt(772.0));
1731        assert_eq!(geom.after_rect.width, Length::from_pt(475.0));
1732        assert_eq!(geom.after_rect.height, Length::from_pt(20.0));
1733
1734        // sidebar top = 50+30=80, sidebar h = 742-30-20=692
1735        // region-start: x=60, y=80, w=50, h=692
1736        assert_eq!(geom.start_rect.x, Length::from_pt(60.0));
1737        assert_eq!(geom.start_rect.y, Length::from_pt(80.0));
1738        assert_eq!(geom.start_rect.width, Length::from_pt(50.0));
1739        assert_eq!(geom.start_rect.height, Length::from_pt(692.0));
1740
1741        // region-end: x=60+475-40=495, y=80, w=40, h=692
1742        assert_eq!(geom.end_rect.x, Length::from_pt(495.0));
1743        assert_eq!(geom.end_rect.y, Length::from_pt(80.0));
1744        assert_eq!(geom.end_rect.width, Length::from_pt(40.0));
1745        assert_eq!(geom.end_rect.height, Length::from_pt(692.0));
1746
1747        // region-body: x=60+50=110, y=80, w=475-50-40=385, h=692
1748        assert_eq!(geom.body_rect.x, Length::from_pt(110.0));
1749        assert_eq!(geom.body_rect.y, Length::from_pt(80.0));
1750        assert_eq!(geom.body_rect.width, Length::from_pt(385.0));
1751        assert_eq!(geom.body_rect.height, Length::from_pt(692.0));
1752    }
1753
1754    #[test]
1755    fn test_all_five_regions_layout() {
1756        let mut fo_tree = FoArena::new();
1757
1758        let root = fo_tree.add_node(FoNode::new(FoNodeData::Root));
1759
1760        let page_seq = fo_tree.add_node(FoNode::new(FoNodeData::PageSequence {
1761            master_reference: "A4".to_string(),
1762            format: "1".to_string(),
1763            grouping_separator: None,
1764            grouping_size: None,
1765            properties: PropertyList::new(),
1766        }));
1767        fo_tree
1768            .append_child(root, page_seq)
1769            .expect("test: should succeed");
1770
1771        // Add all four static-content regions
1772        for flow_name in &[
1773            "xsl-region-before",
1774            "xsl-region-after",
1775            "xsl-region-start",
1776            "xsl-region-end",
1777        ] {
1778            let sc = fo_tree.add_node(FoNode::new(FoNodeData::StaticContent {
1779                flow_name: flow_name.to_string(),
1780                properties: PropertyList::new(),
1781            }));
1782            fo_tree
1783                .append_child(page_seq, sc)
1784                .expect("test: should succeed");
1785            let sc_block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
1786                properties: PropertyList::new(),
1787            }));
1788            fo_tree
1789                .append_child(sc, sc_block)
1790                .expect("test: should succeed");
1791        }
1792
1793        // Add flow (region-body)
1794        let flow = fo_tree.add_node(FoNode::new(FoNodeData::Flow {
1795            flow_name: "xsl-region-body".to_string(),
1796            properties: PropertyList::new(),
1797        }));
1798        fo_tree
1799            .append_child(page_seq, flow)
1800            .expect("test: should succeed");
1801
1802        let block = fo_tree.add_node(FoNode::new(FoNodeData::Block {
1803            properties: PropertyList::new(),
1804        }));
1805        fo_tree
1806            .append_child(flow, block)
1807            .expect("test: should succeed");
1808
1809        let engine = LayoutEngine::new();
1810        let area_tree = engine.layout(&fo_tree).expect("test: should succeed");
1811
1812        let mut has_header = false;
1813        let mut has_footer = false;
1814        let mut has_sidebar_start = false;
1815        let mut has_sidebar_end = false;
1816        let mut has_region = false;
1817
1818        for (_, node) in area_tree.iter() {
1819            match node.area.area_type {
1820                AreaType::Header => has_header = true,
1821                AreaType::Footer => has_footer = true,
1822                AreaType::SidebarStart => has_sidebar_start = true,
1823                AreaType::SidebarEnd => has_sidebar_end = true,
1824                AreaType::Region => has_region = true,
1825                _ => {}
1826            }
1827        }
1828
1829        assert!(has_header, "Should have a header area");
1830        assert!(has_footer, "Should have a footer area");
1831        assert!(has_sidebar_start, "Should have a sidebar-start area");
1832        assert!(has_sidebar_end, "Should have a sidebar-end area");
1833        assert!(has_region, "Should have a body region area");
1834    }
1835
1836    /// Parse a length string like "10mm", "72pt", "1.5in" to a Length value
1837    #[allow(dead_code)]
1838    fn parse_fo_length_str(s: &str) -> Option<Length> {
1839        if let Some(v) = s.strip_suffix("pt") {
1840            v.parse::<f64>().ok().map(Length::from_pt)
1841        } else if let Some(v) = s.strip_suffix("mm") {
1842            v.parse::<f64>().ok().map(Length::from_mm)
1843        } else if let Some(v) = s.strip_suffix("cm") {
1844            v.parse::<f64>().ok().map(Length::from_cm)
1845        } else if let Some(v) = s.strip_suffix("in") {
1846            v.parse::<f64>().ok().map(Length::from_inch)
1847        } else if let Some(v) = s.strip_suffix("px") {
1848            v.parse::<f64>().ok().map(|px| Length::from_pt(px * 0.75))
1849        } else {
1850            None
1851        }
1852    }
1853}