1mod encode;
4
5pub use self::encode::html;
6
7use comemo::{Track, Tracked, TrackedMut};
8use typst_library::diag::{bail, warning, At, SourceResult};
9use typst_library::engine::{Engine, Route, Sink, Traced};
10use typst_library::foundations::{Content, StyleChain, Target, TargetElem};
11use typst_library::html::{
12 attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlNode,
13};
14use typst_library::introspection::{
15 Introspector, Locator, LocatorLink, SplitLocator, TagElem,
16};
17use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size};
18use typst_library::model::{DocumentInfo, ParElem};
19use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
20use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
21use typst_library::World;
22use typst_syntax::Span;
23
24#[typst_macros::time(name = "html document")]
29pub fn html_document(
30 engine: &mut Engine,
31 content: &Content,
32 styles: StyleChain,
33) -> SourceResult<HtmlDocument> {
34 html_document_impl(
35 engine.routines,
36 engine.world,
37 engine.introspector,
38 engine.traced,
39 TrackedMut::reborrow_mut(&mut engine.sink),
40 engine.route.track(),
41 content,
42 styles,
43 )
44}
45
46#[comemo::memoize]
48#[allow(clippy::too_many_arguments)]
49fn html_document_impl(
50 routines: &Routines,
51 world: Tracked<dyn World + '_>,
52 introspector: Tracked<Introspector>,
53 traced: Tracked<Traced>,
54 sink: TrackedMut<Sink>,
55 route: Tracked<Route>,
56 content: &Content,
57 styles: StyleChain,
58) -> SourceResult<HtmlDocument> {
59 let mut locator = Locator::root().split();
60 let mut engine = Engine {
61 routines,
62 world,
63 introspector,
64 traced,
65 sink,
66 route: Route::extend(route).unnested(),
67 };
68
69 let styles = styles.to_map().outside();
72 let styles = StyleChain::new(&styles);
73
74 let arenas = Arenas::default();
75 let mut info = DocumentInfo::default();
76 let children = (engine.routines.realize)(
77 RealizationKind::HtmlDocument(&mut info),
78 &mut engine,
79 &mut locator,
80 &arenas,
81 content,
82 styles,
83 )?;
84
85 let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
86 let introspector = Introspector::html(&output);
87 let root = root_element(output, &info)?;
88
89 Ok(HtmlDocument { info, root, introspector })
90}
91
92#[typst_macros::time(name = "html fragment")]
94pub fn html_fragment(
95 engine: &mut Engine,
96 content: &Content,
97 locator: Locator,
98 styles: StyleChain,
99) -> SourceResult<Vec<HtmlNode>> {
100 html_fragment_impl(
101 engine.routines,
102 engine.world,
103 engine.introspector,
104 engine.traced,
105 TrackedMut::reborrow_mut(&mut engine.sink),
106 engine.route.track(),
107 content,
108 locator.track(),
109 styles,
110 )
111}
112
113#[comemo::memoize]
115#[allow(clippy::too_many_arguments)]
116fn html_fragment_impl(
117 routines: &Routines,
118 world: Tracked<dyn World + '_>,
119 introspector: Tracked<Introspector>,
120 traced: Tracked<Traced>,
121 sink: TrackedMut<Sink>,
122 route: Tracked<Route>,
123 content: &Content,
124 locator: Tracked<Locator>,
125 styles: StyleChain,
126) -> SourceResult<Vec<HtmlNode>> {
127 let link = LocatorLink::new(locator);
128 let mut locator = Locator::link(&link).split();
129 let mut engine = Engine {
130 routines,
131 world,
132 introspector,
133 traced,
134 sink,
135 route: Route::extend(route),
136 };
137
138 engine.route.check_html_depth().at(content.span())?;
139
140 let arenas = Arenas::default();
141 let children = (engine.routines.realize)(
142 RealizationKind::HtmlFragment(&mut FragmentKind::Block),
145 &mut engine,
146 &mut locator,
147 &arenas,
148 content,
149 styles,
150 )?;
151
152 handle_list(&mut engine, &mut locator, children.iter().copied())
153}
154
155fn handle_list<'a>(
157 engine: &mut Engine,
158 locator: &mut SplitLocator,
159 children: impl IntoIterator<Item = Pair<'a>>,
160) -> SourceResult<Vec<HtmlNode>> {
161 let mut output = Vec::new();
162 for (child, styles) in children {
163 handle(engine, child, locator, styles, &mut output)?;
164 }
165 Ok(output)
166}
167
168fn handle(
170 engine: &mut Engine,
171 child: &Content,
172 locator: &mut SplitLocator,
173 styles: StyleChain,
174 output: &mut Vec<HtmlNode>,
175) -> SourceResult<()> {
176 if let Some(elem) = child.to_packed::<TagElem>() {
177 output.push(HtmlNode::Tag(elem.tag.clone()));
178 } else if let Some(elem) = child.to_packed::<HtmlElem>() {
179 let mut children = vec![];
180 if let Some(body) = elem.body(styles) {
181 children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
182 }
183 if tag::is_void(elem.tag) && !children.is_empty() {
184 bail!(elem.span(), "HTML void elements may not have children");
185 }
186 let element = HtmlElement {
187 tag: elem.tag,
188 attrs: elem.attrs(styles).clone(),
189 children,
190 span: elem.span(),
191 };
192 output.push(element.into());
193 } else if let Some(elem) = child.to_packed::<ParElem>() {
194 let children =
195 html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?;
196 output.push(
197 HtmlElement::new(tag::p)
198 .with_children(children)
199 .spanned(elem.span())
200 .into(),
201 );
202 } else if let Some(elem) = child.to_packed::<BoxElem>() {
203 if let Some(body) = elem.body(styles) {
205 let children =
206 html_fragment(engine, body, locator.next(&elem.span()), styles)?;
207 output.push(
208 HtmlElement::new(tag::span)
209 .with_attr(attr::style, "display: inline-block;")
210 .with_children(children)
211 .spanned(elem.span())
212 .into(),
213 )
214 }
215 } else if let Some((elem, body)) =
216 child
217 .to_packed::<BlockElem>()
218 .and_then(|elem| match elem.body(styles) {
219 Some(BlockBody::Content(body)) => Some((elem, body)),
220 _ => None,
221 })
222 {
223 let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
225 output.push(
226 HtmlElement::new(tag::div)
227 .with_children(children)
228 .spanned(elem.span())
229 .into(),
230 );
231 } else if child.is::<SpaceElem>() {
232 output.push(HtmlNode::text(' ', child.span()));
233 } else if let Some(elem) = child.to_packed::<TextElem>() {
234 output.push(HtmlNode::text(elem.text.clone(), elem.span()));
235 } else if let Some(elem) = child.to_packed::<LinebreakElem>() {
236 output.push(HtmlElement::new(tag::br).spanned(elem.span()).into());
237 } else if let Some(elem) = child.to_packed::<SmartQuoteElem>() {
238 output.push(HtmlNode::text(
239 if elem.double(styles) { '"' } else { '\'' },
240 child.span(),
241 ));
242 } else if let Some(elem) = child.to_packed::<FrameElem>() {
243 let locator = locator.next(&elem.span());
244 let style = TargetElem::set_target(Target::Paged).wrap();
245 let frame = (engine.routines.layout_frame)(
246 engine,
247 &elem.body,
248 locator,
249 styles.chain(&style),
250 Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
251 )?;
252 output.push(HtmlNode::Frame(frame));
253 } else {
254 engine.sink.warn(warning!(
255 child.span(),
256 "{} was ignored during HTML export",
257 child.elem().name()
258 ));
259 }
260 Ok(())
261}
262
263fn root_element(output: Vec<HtmlNode>, info: &DocumentInfo) -> SourceResult<HtmlElement> {
266 let body = match classify_output(output)? {
267 OutputKind::Html(element) => return Ok(element),
268 OutputKind::Body(body) => body,
269 OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs),
270 };
271 Ok(HtmlElement::new(tag::html)
272 .with_children(vec![head_element(info).into(), body.into()]))
273}
274
275fn head_element(info: &DocumentInfo) -> HtmlElement {
277 let mut children = vec![];
278
279 children.push(HtmlElement::new(tag::meta).with_attr(attr::charset, "utf-8").into());
280
281 children.push(
282 HtmlElement::new(tag::meta)
283 .with_attr(attr::name, "viewport")
284 .with_attr(attr::content, "width=device-width, initial-scale=1")
285 .into(),
286 );
287
288 if let Some(title) = &info.title {
289 children.push(
290 HtmlElement::new(tag::title)
291 .with_children(vec![HtmlNode::Text(title.clone(), Span::detached())])
292 .into(),
293 );
294 }
295
296 if let Some(description) = &info.description {
297 children.push(
298 HtmlElement::new(tag::meta)
299 .with_attr(attr::name, "description")
300 .with_attr(attr::content, description.clone())
301 .into(),
302 );
303 }
304
305 HtmlElement::new(tag::head).with_children(children)
306}
307
308fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> {
310 let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count();
311 for node in &mut output {
312 let HtmlNode::Element(elem) = node else { continue };
313 let tag = elem.tag;
314 let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html));
315 match (tag, count) {
316 (tag::html, 1) => return Ok(OutputKind::Html(take())),
317 (tag::body, 1) => return Ok(OutputKind::Body(take())),
318 (tag::html | tag::body, _) => bail!(
319 elem.span,
320 "`{}` element must be the only element in the document",
321 elem.tag,
322 ),
323 _ => {}
324 }
325 }
326 Ok(OutputKind::Leafs(output))
327}
328
329enum OutputKind {
331 Html(HtmlElement),
334 Body(HtmlElement),
337 Leafs(Vec<HtmlNode>),
339}