ironpress 1.4.4

Pure Rust HTML/CSS/Markdown to PDF converter with layout engine, LaTeX math, tables, images, custom fonts, and streaming output. No browser, no system dependencies.
Documentation
use super::support::{layout_pages, visible_runs};
use crate::layout::elements::test_support::LayoutElementTestExt;
use crate::layout::elements::{Container, Image, LayoutVisitor, TextBlock};
use crate::layout::engine::visit_layout_tree;
use crate::style::computed::Position;

#[test]
fn generated_content_survives_grid_item_block_content_layout() {
    let runs = visible_runs(
        r#"<style>
            .grid { display: grid; grid-template-columns: 100pt; }
            .item::before { content: "before"; }
            .item::after { content: "after"; }
        </style>
        <div class="grid">
            <div class="item"><div>body</div></div>
        </div>"#,
    );
    let text = runs.iter().map(|run| run.text.as_str()).collect::<String>();

    assert!(text.contains("before"), "missing ::before in {text:?}");
    assert!(text.contains("body"), "missing principal child in {text:?}");
    assert!(text.contains("after"), "missing ::after in {text:?}");
}

#[test]
fn generated_content_is_source_ordered_as_flex_items() {
    let runs = visible_runs(
        r#"<style>
            .flex { display: flex; width: 240pt; }
            .flex::before { content: "BEFORE"; color: red; }
            .flex::after { content: "AFTER"; color: green; }
        </style>
        <div class="flex"><strong>BODY</strong></div>"#,
    );
    let text = runs.iter().map(|run| run.text.as_str()).collect::<String>();

    assert_eq!(text, "BEFOREBODYAFTER");
}

#[test]
fn absolute_generated_flex_child_does_not_replace_in_flow_items() {
    let runs = visible_runs(
        r#"<style>
            .flex { display: flex; position: relative; width: 240pt; height: 80pt; }
            .flex::before {
                content: "ABSOLUTE";
                position: absolute;
                left: 0;
                top: 0;
            }
        </style>
        <div class="flex"><strong>FLOW</strong></div>"#,
    );
    let text = runs.iter().map(|run| run.text.as_str()).collect::<String>();

    assert!(
        text.contains("ABSOLUTE"),
        "missing generated child in {text:?}"
    );
    assert!(text.contains("FLOW"), "missing principal child in {text:?}");
}

#[test]
fn flex_counter_traversal_follows_generated_and_out_of_flow_source_order() {
    let runs = visible_runs(
        r#"<style>
            .flex { display: flex; position: relative; counter-reset: item; }
            .flex::before {
                counter-increment: item;
                content: "B" counter(item);
            }
            .absolute {
                position: absolute;
                counter-increment: item;
            }
            .absolute::before { content: "X" counter(item); }
            .flow { counter-increment: item; }
            .flow::before { content: "F" counter(item); }
            .flex::after {
                counter-increment: item;
                content: "A" counter(item);
            }
        </style>
        <div class="flex"><span class="absolute"></span><span class="flow"></span></div>"#,
    );
    let text = runs.iter().map(|run| run.text.as_str()).collect::<String>();

    for expected in ["B1", "X2", "F3", "A4"] {
        assert!(text.contains(expected), "missing {expected:?} in {text:?}");
    }
}

#[test]
fn empty_string_generated_box_retains_absolute_background_and_filter() {
    #[derive(Default)]
    struct GeneratedBox {
        found: bool,
        text_boxes: Vec<(Position, bool, bool)>,
        images: Vec<(Position, bool)>,
    }

    impl LayoutVisitor for GeneratedBox {
        fn visit_text_block(&mut self, block: &TextBlock) {
            let has_background = block.paint.background.color.is_some();
            let has_filter = block
                .paint
                .group
                .filter
                .as_ref()
                .is_some_and(|filter| filter.requires_source_surface());
            self.text_boxes
                .push((block.positioning.scheme, has_background, has_filter));
            self.found |=
                block.positioning.scheme == Position::Absolute && has_background && has_filter;
        }

        fn visit_image(&mut self, image: &Image) {
            let has_filter_overflow = !image.paint.raster_overflow.is_zero();
            self.images
                .push((image.positioning.scheme, has_filter_overflow));
            self.found |= image.positioning.scheme == Position::Absolute && has_filter_overflow;
        }
    }

    let pages = layout_pages(
        r#"<style>
            .wrap { position: relative; width: 72pt; height: 72pt; }
            .wrap::before {
                content: "";
                position: absolute;
                inset: 0;
                width: 72pt;
                height: 72pt;
                background: #336699;
                filter: blur(4pt);
            }
        </style><div class="wrap"></div>"#,
    );
    let mut generated = GeneratedBox::default();
    for (_, element) in &pages[0].elements {
        visit_layout_tree(element.as_ref(), &mut generated);
    }

    assert!(
        generated.found,
        "content: \"\" must generate its styled pseudo-element box; text={:?}, images={:?}",
        generated.text_boxes, generated.images,
    );
}

#[test]
fn generated_block_remains_inside_its_absolute_originating_box() {
    #[derive(Default)]
    struct AbsoluteGeneratedBox(Vec<(f32, f32, f32, usize)>);

    impl LayoutVisitor for AbsoluteGeneratedBox {
        fn visit_container(&mut self, container: &Container) {
            if container.positioning.scheme == Position::Absolute
                && container.children.iter().any(|child| {
                    child
                        .inspect_text(|block| block.paint.background.color.is_some())
                        .unwrap_or(false)
                })
            {
                self.0.push((
                    container.positioning.insets.left,
                    container.positioning.insets.top,
                    container.box_model.size.width.resolve(0.0),
                    container.children.len(),
                ));
            }
        }
    }

    let pages = layout_pages(
        r#"<style>
            * { margin:0; padding:0; box-sizing:border-box }
            .panel { position:relative; width:120pt; height:108pt }
            .top { position:absolute; left:12pt; top:9pt; width:72pt; height:72pt }
            .top::before {
                content:"";
                display:block;
                width:72pt;
                height:72pt;
                background:#ef233c;
            }
        </style><div class="panel"><div class="top"></div></div>"#,
    );
    let mut boxes = AbsoluteGeneratedBox::default();
    for (_, element) in &pages[0].elements {
        visit_layout_tree(element.as_ref(), &mut boxes);
    }

    assert_eq!(boxes.0, [(12.0, 9.0, 72.0, 1)]);
}