1use std::num::NonZeroUsize;
2
3use comemo::Track;
4use ecow::{EcoVec, eco_format};
5use typst_library::diag::{At, SourceResult, bail, error, warning};
6use typst_library::engine::Engine;
7use typst_library::foundations::{
8 Content, Context, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target,
9};
10use typst_library::introspection::Counter;
11use typst_library::layout::resolve::{Cell, CellGrid, Entry, Header};
12use typst_library::layout::{
13 BlockBody, BlockElem, BoxElem, HElem, OuterVAlignment, Sizing,
14};
15use typst_library::model::{
16 Attribution, BibliographyElem, CiteElem, CiteGroup, CslIndentElem, CslLightElem,
17 Destination, DirectLinkElem, EmphElem, EnumElem, FigureCaption, FigureElem,
18 FootnoteElem, FootnoteEntry, FootnoteMarker, HeadingElem, LinkElem, LinkTarget,
19 ListElem, OutlineElem, OutlineEntry, OutlineNode, ParElem, ParbreakElem, QuoteElem,
20 RefElem, StrongElem, TableCell, TableElem, TermsElem, TitleElem, Works,
21};
22use typst_library::text::{
23 HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SmallcapsElem,
24 SpaceElem, StrikeElem, SubElem, SuperElem, UnderlineElem,
25};
26use typst_library::visualize::{Color, ImageElem};
27use typst_macros::elem;
28use typst_utils::singleton;
29
30use crate::{FrameElem, HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag, attr, css, tag};
31
32pub fn register(rules: &mut NativeRuleMap) {
34 use Target::{Html, Paged};
35
36 rules.register(Html, PAR_RULE);
38 rules.register(Html, STRONG_RULE);
39 rules.register(Html, EMPH_RULE);
40 rules.register(Html, LIST_RULE);
41 rules.register(Html, ENUM_RULE);
42 rules.register(Html, TERMS_RULE);
43 rules.register(Html, LINK_RULE);
44 rules.register(Html, DIRECT_LINK_RULE);
45 rules.register(Html, TITLE_RULE);
46 rules.register(Html, HEADING_RULE);
47 rules.register(Html, FIGURE_RULE);
48 rules.register(Html, FIGURE_CAPTION_RULE);
49 rules.register(Html, QUOTE_RULE);
50 rules.register(Html, FOOTNOTE_RULE);
51 rules.register(Html, FOOTNOTE_MARKER_RULE);
52 rules.register(Html, FOOTNOTE_CONTAINER_RULE);
53 rules.register(Html, FOOTNOTE_ENTRY_RULE);
54 rules.register(Html, OUTLINE_RULE);
55 rules.register(Html, OUTLINE_ENTRY_RULE);
56 rules.register(Html, REF_RULE);
57 rules.register(Html, CITE_GROUP_RULE);
58 rules.register(Html, BIBLIOGRAPHY_RULE);
59 rules.register(Html, CSL_LIGHT_RULE);
60 rules.register(Html, CSL_INDENT_RULE);
61 rules.register(Html, TABLE_RULE);
62
63 rules.register(Html, SUB_RULE);
65 rules.register(Html, SUPER_RULE);
66 rules.register(Html, UNDERLINE_RULE);
67 rules.register(Html, OVERLINE_RULE);
68 rules.register(Html, STRIKE_RULE);
69 rules.register(Html, HIGHLIGHT_RULE);
70 rules.register(Html, SMALLCAPS_RULE);
71 rules.register(Html, RAW_RULE);
72 rules.register(Html, RAW_LINE_RULE);
73
74 rules.register(Html, BLOCK_RULE);
76 rules.register(Html, BOX_RULE);
77
78 rules.register(Html, IMAGE_RULE);
80
81 rules.register::<FrameElem>(Paged, |elem, _, _| Ok(elem.body.clone()));
85}
86
87const PAR_RULE: ShowFn<ParElem> =
88 |elem, _, _| Ok(HtmlElem::new(tag::p).with_body(Some(elem.body.clone())).pack());
89
90const STRONG_RULE: ShowFn<StrongElem> =
91 |elem, _, _| Ok(HtmlElem::new(tag::strong).with_body(Some(elem.body.clone())).pack());
92
93const EMPH_RULE: ShowFn<EmphElem> =
94 |elem, _, _| Ok(HtmlElem::new(tag::em).with_body(Some(elem.body.clone())).pack());
95
96const LIST_RULE: ShowFn<ListElem> = |elem, _, styles| {
97 Ok(HtmlElem::new(tag::ul)
98 .with_body(Some(Content::sequence(elem.children.iter().map(|item| {
99 let mut body = item.body.clone();
101 if !elem.tight.get(styles) {
102 body += ParbreakElem::shared();
103 }
104 HtmlElem::new(tag::li)
105 .with_body(Some(body))
106 .pack()
107 .spanned(item.span())
108 }))))
109 .pack())
110};
111
112const ENUM_RULE: ShowFn<EnumElem> = |elem, _, styles| {
113 let mut ol = HtmlElem::new(tag::ol);
114
115 if elem.reversed.get(styles) {
116 ol = ol.with_attr(attr::reversed, "reversed");
117 }
118
119 if let Some(n) = elem.start.get(styles).custom() {
120 ol = ol.with_attr(attr::start, eco_format!("{n}"));
121 }
122
123 let body = Content::sequence(elem.children.iter().map(|item| {
124 let mut li = HtmlElem::new(tag::li);
125 if let Smart::Custom(nr) = item.number.get(styles) {
126 li = li.with_attr(attr::value, eco_format!("{nr}"));
127 }
128 let mut body = item.body.clone();
130 if !elem.tight.get(styles) {
131 body += ParbreakElem::shared();
132 }
133 li.with_body(Some(body)).pack().spanned(item.span())
134 }));
135
136 Ok(ol.with_body(Some(body)).pack())
137};
138
139const TERMS_RULE: ShowFn<TermsElem> = |elem, _, styles| {
140 Ok(HtmlElem::new(tag::dl)
141 .with_body(Some(Content::sequence(elem.children.iter().flat_map(|item| {
142 let mut description = item.description.clone();
144 if !elem.tight.get(styles) {
145 description += ParbreakElem::shared();
146 }
147
148 [
149 HtmlElem::new(tag::dt)
150 .with_body(Some(item.term.clone()))
151 .pack()
152 .spanned(item.term.span()),
153 HtmlElem::new(tag::dd)
154 .with_body(Some(description))
155 .pack()
156 .spanned(item.description.span()),
157 ]
158 }))))
159 .pack())
160};
161
162const LINK_RULE: ShowFn<LinkElem> = |elem, engine, _| {
163 let dest = elem.dest.resolve(engine.introspector).at(elem.span())?;
164
165 let href = match dest {
166 Destination::Url(url) => Some(url.clone().into_inner()),
167 Destination::Location(location) => {
168 let id = engine
169 .introspector
170 .html_id(location)
171 .cloned()
172 .ok_or("failed to determine link anchor")
173 .at(elem.span())?;
174 Some(eco_format!("#{id}"))
175 }
176 Destination::Position(_) => {
177 engine.sink.warn(warning!(
178 elem.span(),
179 "positional link was ignored during HTML export"
180 ));
181 None
182 }
183 };
184
185 Ok(HtmlElem::new(tag::a)
186 .with_optional_attr(attr::href, href)
187 .with_body(Some(elem.body.clone()))
188 .pack())
189};
190
191const DIRECT_LINK_RULE: ShowFn<DirectLinkElem> = |elem, _, _| {
192 Ok(LinkElem::new(
193 LinkTarget::Dest(Destination::Location(elem.loc)),
194 elem.body.clone(),
195 )
196 .pack())
197};
198
199const TITLE_RULE: ShowFn<TitleElem> = |elem, _, styles| {
200 Ok(HtmlElem::new(tag::h1)
201 .with_body(Some(elem.resolve_body(styles).at(elem.span())?))
202 .pack())
203};
204
205const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
206 let span = elem.span();
207
208 let mut realized = elem.body.clone();
209 if let Some(numbering) = elem.numbering.get_ref(styles).as_ref() {
210 let location = elem.location().unwrap();
211 let numbering = Counter::of(HeadingElem::ELEM)
212 .display_at_loc(engine, location, styles, numbering)?
213 .spanned(span);
214 realized = numbering + SpaceElem::shared().clone() + realized;
215 }
216
217 let level = elem.resolve_level(styles).get();
222 Ok(if level >= 6 {
223 engine.sink.warn(warning!(
224 span,
225 "heading of level {} was transformed to \
226 <div role=\"heading\" aria-level=\"{}\">, which is not \
227 supported by all assistive technology",
228 level, level + 1;
229 hint: "HTML only supports <h1> to <h6>, not <h{}>", level + 1;
230 hint: "you may want to restructure your document so that \
231 it doesn't contain deep headings"
232 ));
233 HtmlElem::new(tag::div)
234 .with_body(Some(realized))
235 .with_attr(attr::role, "heading")
236 .with_attr(attr::aria_level, eco_format!("{}", level + 1))
237 .pack()
238 } else {
239 let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1];
240 HtmlElem::new(t).with_body(Some(realized)).pack()
241 })
242};
243
244const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
245 let span = elem.span();
246 let mut realized = elem.body.clone();
247
248 if let Some(caption) = elem.caption.get_cloned(styles) {
250 realized = match caption.position.get(styles) {
251 OuterVAlignment::Top => caption.pack() + realized,
252 OuterVAlignment::Bottom => realized + caption.pack(),
253 };
254 }
255
256 realized += ParbreakElem::shared().clone().spanned(span);
258
259 Ok(HtmlElem::new(tag::figure).with_body(Some(realized)).pack())
260};
261
262const FIGURE_CAPTION_RULE: ShowFn<FigureCaption> = |elem, engine, styles| {
263 Ok(HtmlElem::new(tag::figcaption)
264 .with_body(Some(elem.realize(engine, styles)?))
265 .pack())
266};
267
268const QUOTE_RULE: ShowFn<QuoteElem> = |elem, _, styles| {
269 let span = elem.span();
270 let block = elem.block.get(styles);
271
272 let mut realized = elem.body.clone();
273
274 if elem.quotes.get(styles).unwrap_or(!block) {
275 realized = QuoteElem::quoted(realized, styles);
276 }
277
278 let attribution = elem.attribution.get_ref(styles);
279
280 if block {
281 let mut blockquote = HtmlElem::new(tag::blockquote).with_body(Some(realized));
282 if let Some(Attribution::Content(attribution)) = attribution
283 && let Some(link) = attribution.to_packed::<LinkElem>()
284 && let LinkTarget::Dest(Destination::Url(url)) = &link.dest
285 {
286 blockquote = blockquote.with_attr(attr::cite, url.clone().into_inner());
287 }
288
289 realized = blockquote.pack().spanned(span);
290
291 if let Some(attribution) = attribution.as_ref() {
292 realized += attribution.realize(span);
293 }
294 } else if let Some(Attribution::Label(label)) = attribution {
295 realized += SpaceElem::shared().clone();
296 realized += CiteElem::new(*label).pack().spanned(span);
297 }
298
299 Ok(realized)
300};
301
302const FOOTNOTE_RULE: ShowFn<FootnoteElem> = |elem, engine, styles| {
303 let span = elem.span();
304 let (dest, num) = elem.realize(engine, styles)?;
305 let sup = SuperElem::new(num).pack().spanned(span);
306
307 let link = LinkElem::new(dest.into(), sup)
309 .pack()
310 .styled(HtmlElem::role.set(Some("doc-noteref".into())));
311
312 let marker = FootnoteMarker::new().pack().spanned(span);
315
316 Ok(HElem::hole().clone() + link + marker)
317};
318
319#[elem]
323pub struct FootnoteContainer {}
324
325impl FootnoteContainer {
326 pub fn shared() -> &'static Content {
328 singleton!(Content, FootnoteContainer::new().pack())
329 }
330
331 pub fn unsupported_with_custom_dom(engine: &Engine) -> SourceResult<()> {
333 let markers = engine.introspector.query(&FootnoteMarker::ELEM.select());
334 if markers.is_empty() {
335 return Ok(());
336 }
337
338 Err(markers
339 .iter()
340 .map(|marker| {
341 error!(
342 marker.span(),
343 "footnotes are not currently supported in combination \
344 with a custom `<html>` or `<body>` element";
345 hint: "you can still use footnotes with a custom footnote show rule"
346 )
347 })
348 .collect())
349 }
350}
351
352const FOOTNOTE_MARKER_RULE: ShowFn<FootnoteMarker> = |_, _, _| Ok(Content::empty());
353
354const FOOTNOTE_CONTAINER_RULE: ShowFn<FootnoteContainer> = |_, engine, _| {
355 let notes = engine.introspector.query(&FootnoteElem::ELEM.select());
356 if notes.is_empty() {
357 return Ok(Content::empty());
358 }
359
360 let items = notes.into_iter().filter_map(|note| {
362 let note = note.into_packed::<FootnoteElem>().unwrap();
363 if note.is_ref() {
364 return None;
365 }
366
367 let loc = note.location().unwrap();
368 let span = note.span();
369 Some(
370 HtmlElem::new(tag::li)
371 .with_body(Some(FootnoteEntry::new(note).pack().spanned(span)))
372 .with_parent(loc)
373 .pack()
374 .located(loc.variant(1))
375 .spanned(span),
376 )
377 });
378
379 let list = HtmlElem::new(tag::ol)
383 .with_styles(css::Properties::new().with("list-style-type", "none"))
384 .with_body(Some(Content::sequence(items)))
385 .pack();
386
387 Ok(HtmlElem::new(tag::section)
391 .with_attr(attr::role, "doc-endnotes")
392 .with_body(Some(list))
393 .pack())
394};
395
396const FOOTNOTE_ENTRY_RULE: ShowFn<FootnoteEntry> = |elem, engine, styles| {
397 let (prefix, body) = elem.realize(engine, styles)?;
398
399 let backlink = prefix.styled(HtmlElem::role.set(Some("doc-backlink".into())));
402
403 Ok(backlink + body)
409};
410
411const OUTLINE_RULE: ShowFn<OutlineElem> = |elem, engine, styles| {
412 fn convert_list(list: Vec<OutlineNode>) -> Content {
413 HtmlElem::new(tag::ol)
418 .with_styles(css::Properties::new().with("list-style-type", "none"))
419 .with_body(Some(Content::sequence(list.into_iter().map(convert_node))))
420 .pack()
421 }
422
423 fn convert_node(node: OutlineNode) -> Content {
424 let body = if !node.children.is_empty() {
425 HtmlElem::new(tag::div).with_body(Some(node.entry.pack())).pack()
430 + convert_list(node.children)
431 } else {
432 node.entry.pack()
433 };
434 HtmlElem::new(tag::li).with_body(Some(body)).pack()
435 }
436
437 let title = elem.realize_title(styles);
438 let tree = elem.realize_tree(engine, styles)?;
439 let list = convert_list(tree);
440
441 Ok(HtmlElem::new(tag::nav)
442 .with_attr(attr::role, "doc-toc")
443 .with_body(Some(title.unwrap_or_default() + list))
444 .pack())
445};
446
447const OUTLINE_ENTRY_RULE: ShowFn<OutlineEntry> = |elem, engine, styles| {
448 let span = elem.span();
449 let context = Context::new(None, Some(styles));
450
451 let mut realized = elem.body().at(span)?;
452
453 if let Some(prefix) = elem.prefix(engine, context.track(), span)? {
454 let wrapped = HtmlElem::new(tag::span)
455 .with_attr(attr::class, "prefix")
456 .with_body(Some(prefix))
457 .pack()
458 .spanned(span);
459
460 let separator = match elem.element.to_packed::<FigureElem>() {
461 Some(elem) => elem.resolve_separator(styles),
462 None => SpaceElem::shared().clone(),
463 };
464
465 realized = Content::sequence([wrapped, separator, realized]);
466 }
467
468 let loc = elem.element_location().at(span)?;
469 let dest = Destination::Location(loc);
470
471 Ok(LinkElem::new(dest.into(), realized).pack())
472};
473
474const REF_RULE: ShowFn<RefElem> = |elem, engine, styles| elem.realize(engine, styles);
475
476const CITE_GROUP_RULE: ShowFn<CiteGroup> = |elem, engine, _| {
477 Ok(elem
478 .realize(engine)?
479 .styled(HtmlElem::role.set(Some("doc-biblioref".into()))))
480};
481
482const BIBLIOGRAPHY_RULE: ShowFn<BibliographyElem> = |elem, engine, styles| {
487 let span = elem.span();
488 let works = Works::generate(engine).at(span)?;
489 let references = works.references(elem, styles)?;
490
491 let items = references.iter().map(|(prefix, reference, loc)| {
492 let mut realized = reference.clone();
493
494 if let Some(mut prefix) = prefix.clone() {
495 if prefix.is::<DirectLinkElem>() {
498 prefix = prefix.set(HtmlElem::role, Some("doc-backlink".into()));
499 }
500
501 let wrapped = HtmlElem::new(tag::span)
502 .with_attr(attr::class, "prefix")
503 .with_body(Some(prefix))
504 .pack()
505 .spanned(span);
506
507 let separator = SpaceElem::shared().clone();
508 realized = Content::sequence([wrapped, separator, realized]);
509 }
510
511 HtmlElem::new(tag::li)
512 .with_body(Some(realized))
513 .pack()
514 .located(*loc)
515 .spanned(span)
516 });
517
518 let title = elem.realize_title(styles);
519 let list = HtmlElem::new(tag::ul)
520 .with_styles(css::Properties::new().with("list-style-type", "none"))
521 .with_body(Some(Content::sequence(items)))
522 .pack()
523 .spanned(span);
524
525 Ok(HtmlElem::new(tag::section)
526 .with_attr(attr::role, "doc-bibliography")
527 .with_optional_attr(attr::class, works.hanging_indent.then_some("hanging-indent"))
528 .with_body(Some(title.unwrap_or_default() + list))
529 .pack())
530};
531
532const CSL_LIGHT_RULE: ShowFn<CslLightElem> = |elem, _, _| {
533 Ok(HtmlElem::new(tag::span)
534 .with_attr(attr::class, "light")
535 .with_body(Some(elem.body.clone()))
536 .pack())
537};
538
539const CSL_INDENT_RULE: ShowFn<CslIndentElem> = |elem, _, _| {
540 Ok(HtmlElem::new(tag::div)
541 .with_attr(attr::class, "indent")
542 .with_body(Some(elem.body.clone()))
543 .pack())
544};
545
546const TABLE_RULE: ShowFn<TableElem> = |elem, _, styles| {
547 let grid = elem.grid.as_ref().unwrap();
548 Ok(show_cellgrid(grid, styles))
549};
550
551fn show_cellgrid(grid: &CellGrid, styles: StyleChain) -> Content {
552 let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack();
553 let mut rows: Vec<_> = grid.entries.chunks(grid.non_gutter_column_count()).collect();
554
555 let tr = |tag, row: &[Entry]| {
556 let row = row
557 .iter()
558 .flat_map(|entry| entry.as_cell())
559 .map(|cell| show_cell(tag, cell, styles));
560 elem(tag::tr, Content::sequence(row))
561 };
562
563 let footer = grid.footer.as_ref().map(|ft| {
566 let footer_start = if grid.has_gutter { ft.start.div_ceil(2) } else { ft.start };
570 let rows = rows.drain(footer_start..);
571 elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row))))
572 });
573
574 let header_range = |hd: &Header| {
576 if grid.has_gutter {
577 hd.range.start / 2..hd.range.end.div_ceil(2)
580 } else {
581 hd.range.clone()
582 }
583 };
584
585 let mut consecutive_header_end = 0;
588 let first_mid_table_header = grid
589 .headers
590 .iter()
591 .take_while(|hd| {
592 let range = header_range(hd);
593 let is_consecutive = range.start == consecutive_header_end;
594 consecutive_header_end = range.end;
595 is_consecutive
596 })
597 .count();
598
599 let (y_offset, header) = if first_mid_table_header > 0 {
600 let removed_header_rows =
601 header_range(grid.headers.get(first_mid_table_header - 1).unwrap()).end;
602 let rows = rows.drain(..removed_header_rows);
603
604 (
605 removed_header_rows,
606 Some(elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))),
607 )
608 } else {
609 (0, None)
610 };
611
612 let mut next_header = first_mid_table_header;
617 let mut body =
618 Content::sequence(rows.into_iter().enumerate().map(|(relative_y, row)| {
619 let y = relative_y + y_offset;
620 if let Some(current_header_range) =
621 grid.headers.get(next_header).map(|h| header_range(h))
622 && current_header_range.contains(&y)
623 {
624 if y + 1 == current_header_range.end {
625 next_header += 1;
626 }
627
628 tr(tag::th, row)
629 } else {
630 tr(tag::td, row)
631 }
632 }));
633
634 if header.is_some() || footer.is_some() {
635 body = elem(tag::tbody, body);
636 }
637
638 let content = header.into_iter().chain(core::iter::once(body)).chain(footer);
639 elem(tag::table, Content::sequence(content))
640}
641
642fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
643 let cell = cell.body.clone();
644 let Some(cell) = cell.to_packed::<TableCell>() else { return cell };
645 let mut attrs = HtmlAttrs::new();
646 let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string());
647 if let Some(colspan) = span(cell.colspan.get(styles)) {
648 attrs.push(attr::colspan, colspan);
649 }
650 if let Some(rowspan) = span(cell.rowspan.get(styles)) {
651 attrs.push(attr::rowspan, rowspan);
652 }
653 HtmlElem::new(tag)
654 .with_body(Some(cell.body.clone()))
655 .with_attrs(attrs)
656 .pack()
657 .spanned(cell.span())
658}
659
660const SUB_RULE: ShowFn<SubElem> =
661 |elem, _, _| Ok(HtmlElem::new(tag::sub).with_body(Some(elem.body.clone())).pack());
662
663const SUPER_RULE: ShowFn<SuperElem> =
664 |elem, _, _| Ok(HtmlElem::new(tag::sup).with_body(Some(elem.body.clone())).pack());
665
666const UNDERLINE_RULE: ShowFn<UnderlineElem> = |elem, _, _| {
667 Ok(HtmlElem::new(tag::span)
671 .with_attr(attr::style, "text-decoration: underline")
672 .with_body(Some(elem.body.clone()))
673 .pack())
674};
675
676const OVERLINE_RULE: ShowFn<OverlineElem> = |elem, _, _| {
677 Ok(HtmlElem::new(tag::span)
678 .with_attr(attr::style, "text-decoration: overline")
679 .with_body(Some(elem.body.clone()))
680 .pack())
681};
682
683const STRIKE_RULE: ShowFn<StrikeElem> =
684 |elem, _, _| Ok(HtmlElem::new(tag::s).with_body(Some(elem.body.clone())).pack());
685
686const HIGHLIGHT_RULE: ShowFn<HighlightElem> =
687 |elem, _, _| Ok(HtmlElem::new(tag::mark).with_body(Some(elem.body.clone())).pack());
688
689const SMALLCAPS_RULE: ShowFn<SmallcapsElem> = |elem, _, styles| {
690 Ok(HtmlElem::new(tag::span)
691 .with_attr(
692 attr::style,
693 if elem.all.get(styles) {
694 "font-variant-caps: all-small-caps"
695 } else {
696 "font-variant-caps: small-caps"
697 },
698 )
699 .with_body(Some(elem.body.clone()))
700 .pack())
701};
702
703const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
704 let lines = elem.lines.as_deref().unwrap_or_default();
705
706 let mut seq = EcoVec::with_capacity((2 * lines.len()).saturating_sub(1));
707 for (i, line) in lines.iter().enumerate() {
708 if i != 0 {
709 seq.push(LinebreakElem::shared().clone());
710 }
711
712 seq.push(line.clone().pack());
713 }
714
715 let lang = elem.lang.get_ref(styles);
716 let code = HtmlElem::new(tag::code)
717 .with_optional_attr(const { HtmlAttr::constant("data-lang") }, lang.clone())
718 .with_body(Some(Content::sequence(seq)))
719 .pack()
720 .spanned(elem.span());
721
722 Ok(if elem.block.get(styles) {
723 HtmlElem::new(tag::pre).with_body(Some(code)).pack()
724 } else {
725 code
726 })
727};
728
729#[doc(hidden)]
734pub fn html_span_filled(content: Content, color: Color) -> Content {
735 let span = content.span();
736 HtmlElem::new(tag::span)
737 .with_styles(css::Properties::new().with("color", css::color(color)))
738 .with_body(Some(content))
739 .pack()
740 .spanned(span)
741}
742
743const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());
744
745const BLOCK_RULE: ShowFn<BlockElem> = |elem, _, styles| {
747 let body = match elem.body.get_cloned(styles) {
748 None => None,
749 Some(BlockBody::Content(body)) => Some(body),
750 Some(BlockBody::SingleLayouter(_) | BlockBody::MultiLayouter(_)) => {
752 bail!(
753 elem.span(),
754 "blocks with layout routines should not occur in \
755 HTML export – this is a bug"
756 )
757 }
758 };
759
760 Ok(HtmlElem::new(tag::div).with_body(body).pack())
761};
762
763const BOX_RULE: ShowFn<BoxElem> = |elem, _, styles| {
765 Ok(HtmlElem::new(tag::span)
766 .with_styles(css::Properties::new().with("display", "inline-block"))
767 .with_body(elem.body.get_cloned(styles))
768 .pack())
769};
770
771const IMAGE_RULE: ShowFn<ImageElem> = |elem, engine, styles| {
772 let image = elem.decode(engine, styles)?;
773
774 let mut attrs = HtmlAttrs::new();
775 attrs.push(attr::src, typst_svg::convert_image_to_base64_url(&image));
776
777 if let Some(alt) = elem.alt.get_cloned(styles) {
778 attrs.push(attr::alt, alt);
779 }
780
781 let mut inline = css::Properties::new();
782
783 if let Some(value) = typst_svg::convert_image_scaling(image.scaling()) {
785 inline.push("image-rendering", value);
786 }
787
788 match elem.width.get(styles) {
790 Smart::Auto => {}
791 Smart::Custom(rel) => inline.push("width", css::rel(rel)),
792 }
793
794 match elem.height.get(styles) {
796 Sizing::Auto => {}
797 Sizing::Rel(rel) => inline.push("height", css::rel(rel)),
798 Sizing::Fr(_) => {}
799 }
800
801 Ok(HtmlElem::new(tag::img).with_attrs(attrs).with_styles(inline).pack())
802};