summaryrefslogtreecommitdiff
path: root/crates/typst-html/src/convert.rs
blob: 171b4cb7e3d733adecc6f9fcc5860ae8d46dbdd2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
use typst_library::diag::{warning, SourceResult};
use typst_library::engine::Engine;
use typst_library::foundations::{Content, StyleChain, Target, TargetElem};
use typst_library::introspection::{SplitLocator, TagElem};
use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size};
use typst_library::model::ParElem;
use typst_library::routines::Pair;
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};

use crate::fragment::html_fragment;
use crate::{attr, tag, FrameElem, HtmlElem, HtmlElement, HtmlFrame, HtmlNode};

/// Converts realized content into HTML nodes.
pub fn convert_to_nodes<'a>(
    engine: &mut Engine,
    locator: &mut SplitLocator,
    children: impl IntoIterator<Item = Pair<'a>>,
) -> SourceResult<Vec<HtmlNode>> {
    let mut output = Vec::new();
    for (child, styles) in children {
        handle(engine, child, locator, styles, &mut output)?;
    }
    Ok(output)
}

/// Convert one element into HTML node(s).
fn handle(
    engine: &mut Engine,
    child: &Content,
    locator: &mut SplitLocator,
    styles: StyleChain,
    output: &mut Vec<HtmlNode>,
) -> SourceResult<()> {
    if let Some(elem) = child.to_packed::<TagElem>() {
        output.push(HtmlNode::Tag(elem.tag.clone()));
    } else if let Some(elem) = child.to_packed::<HtmlElem>() {
        let mut children = vec![];
        if let Some(body) = elem.body.get_ref(styles) {
            children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
        }
        let element = HtmlElement {
            tag: elem.tag,
            attrs: elem.attrs.get_cloned(styles),
            children,
            span: elem.span(),
        };
        output.push(element.into());
    } else if let Some(elem) = child.to_packed::<ParElem>() {
        let children =
            html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?;
        output.push(
            HtmlElement::new(tag::p)
                .with_children(children)
                .spanned(elem.span())
                .into(),
        );
    } else if let Some(elem) = child.to_packed::<BoxElem>() {
        // TODO: This is rather incomplete.
        if let Some(body) = elem.body.get_ref(styles) {
            let children =
                html_fragment(engine, body, locator.next(&elem.span()), styles)?;
            output.push(
                HtmlElement::new(tag::span)
                    .with_attr(attr::style, "display: inline-block;")
                    .with_children(children)
                    .spanned(elem.span())
                    .into(),
            )
        }
    } else if let Some((elem, body)) =
        child
            .to_packed::<BlockElem>()
            .and_then(|elem| match elem.body.get_ref(styles) {
                Some(BlockBody::Content(body)) => Some((elem, body)),
                _ => None,
            })
    {
        // TODO: This is rather incomplete.
        let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
        output.push(
            HtmlElement::new(tag::div)
                .with_children(children)
                .spanned(elem.span())
                .into(),
        );
    } else if child.is::<SpaceElem>() {
        output.push(HtmlNode::text(' ', child.span()));
    } else if let Some(elem) = child.to_packed::<TextElem>() {
        let text = if let Some(case) = styles.get(TextElem::case) {
            case.apply(&elem.text).into()
        } else {
            elem.text.clone()
        };
        output.push(HtmlNode::text(text, elem.span()));
    } else if let Some(elem) = child.to_packed::<LinebreakElem>() {
        output.push(HtmlElement::new(tag::br).spanned(elem.span()).into());
    } else if let Some(elem) = child.to_packed::<SmartQuoteElem>() {
        output.push(HtmlNode::text(
            if elem.double.get(styles) { '"' } else { '\'' },
            child.span(),
        ));
    } else if let Some(elem) = child.to_packed::<FrameElem>() {
        let locator = locator.next(&elem.span());
        let style = TargetElem::target.set(Target::Paged).wrap();
        let frame = (engine.routines.layout_frame)(
            engine,
            &elem.body,
            locator,
            styles.chain(&style),
            Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
        )?;
        output.push(HtmlNode::Frame(HtmlFrame {
            inner: frame,
            text_size: styles.resolve(TextElem::size),
        }));
    } else {
        engine.sink.warn(warning!(
            child.span(),
            "{} was ignored during HTML export",
            child.elem().name()
        ));
    }
    Ok(())
}

/// Checks whether the given element is an inline-level HTML element.
pub fn is_inline(elem: &Content) -> bool {
    elem.to_packed::<HtmlElem>()
        .is_some_and(|elem| tag::is_inline_by_default(elem.tag))
}