diff --git a/crates/typst-library/src/model/emph.rs b/crates/typst-library/src/model/emph.rs index e36e5ef7f..45097b340 100644 --- a/crates/typst-library/src/model/emph.rs +++ b/crates/typst-library/src/model/emph.rs @@ -1,6 +1,9 @@ use crate::diag::SourceResult; use crate::engine::Engine; -use crate::foundations::{elem, Content, Packed, Show, StyleChain}; +use crate::foundations::{ + elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem, +}; +use crate::html::{tag, HtmlElem}; use crate::text::{ItalicToggle, TextElem}; /// Emphasizes content by toggling italics. @@ -35,7 +38,15 @@ pub struct EmphElem { impl Show for Packed { #[typst_macros::time(name = "emph", span = self.span())] - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_emph(ItalicToggle(true)))) + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + let body = self.body.clone(); + Ok(if TargetElem::target_in(styles).is_html() { + HtmlElem::new(tag::em) + .with_body(Some(body)) + .pack() + .spanned(self.span()) + } else { + body.styled(TextElem::set_emph(ItalicToggle(true))) + }) } } diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index bac792d3f..e0121ba24 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -1,13 +1,15 @@ use std::str::FromStr; +use ecow::eco_format; use smallvec::SmallVec; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, - Styles, + Styles, TargetElem, }; +use crate::html::{attr, tag, HtmlElem}; use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem}; use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem}; @@ -214,6 +216,19 @@ impl EnumElem { impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + if TargetElem::target_in(styles).is_html() { + return Ok(HtmlElem::new(tag::ol) + .with_body(Some(Content::sequence(self.children.iter().map(|item| { + let mut li = HtmlElem::new(tag::li); + if let Some(nr) = item.number(styles) { + li = li.with_attr(attr::value, eco_format!("{nr}")); + } + li.with_body(Some(item.body.clone())).pack().spanned(item.span()) + })))) + .pack() + .spanned(self.span())); + } + let mut realized = BlockElem::multi_layouter(self.clone(), engine.routines.layout_enum) .pack() diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index 3e2777c1a..e871fbeb8 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -9,8 +9,9 @@ use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, select_where, Content, Element, NativeElement, Packed, Selector, - Show, ShowSet, Smart, StyleChain, Styles, Synthesize, + Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, }; +use crate::html::{tag, HtmlElem}; use crate::introspection::{ Count, Counter, CounterKey, CounterUpdate, Locatable, Location, }; @@ -326,15 +327,30 @@ impl Synthesize for Packed { impl Show for Packed { #[typst_macros::time(name = "figure", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let mut realized = self.body().clone(); + let target = TargetElem::target_in(styles); + let mut realized = self.body.clone(); // Build the caption, if any. if let Some(caption) = self.caption(styles) { - let v = VElem::new(self.gap(styles).into()).with_weak(true).pack(); - realized = match caption.position(styles) { - OuterVAlignment::Top => caption.pack() + v + realized, - OuterVAlignment::Bottom => realized + v + caption.pack(), + let (first, second) = match caption.position(styles) { + OuterVAlignment::Top => (caption.pack(), realized), + OuterVAlignment::Bottom => (realized, caption.pack()), }; + let mut seq = Vec::with_capacity(3); + seq.push(first); + if !target.is_html() { + let v = VElem::new(self.gap(styles).into()).with_weak(true); + seq.push(v.pack().spanned(self.span())) + } + seq.push(second); + realized = Content::sequence(seq) + } + + if target.is_html() { + return Ok(HtmlElem::new(tag::figure) + .with_body(Some(realized)) + .pack() + .spanned(self.span())); } // Wrap the contents in a block. @@ -607,6 +623,13 @@ impl Show for Packed { realized = supplement + numbers + self.get_separator(styles) + realized; } + if TargetElem::target_in(styles).is_html() { + return Ok(HtmlElem::new(tag::figcaption) + .with_body(Some(realized)) + .pack() + .spanned(self.span())); + } + Ok(realized) } } diff --git a/crates/typst-library/src/model/heading.rs b/crates/typst-library/src/model/heading.rs index 269e95ebb..eb3c5413a 100644 --- a/crates/typst-library/src/model/heading.rs +++ b/crates/typst-library/src/model/heading.rs @@ -6,8 +6,9 @@ use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, - Styles, Synthesize, + Styles, Synthesize, TargetElem, }; +use crate::html::{tag, HtmlElem}; use crate::introspection::{ Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink, }; @@ -216,6 +217,22 @@ impl Synthesize for Packed { impl Show for Packed { #[typst_macros::time(name = "heading", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + if TargetElem::target_in(styles).is_html() { + // HTML's h1 is closer to a title element. There should only be one. + // Meanwhile, a level 1 Typst heading is a section heading. For this + // reason, levels are offset by one: A Typst level 1 heading becomes + // a `

`. + let level = self.resolve_level(styles); + let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level.get().min(5) - 1]; + + // TODO: Don't ignore the various non-body properties. + let body = self.body().clone(); + return Ok(HtmlElem::new(t) + .with_body(Some(body)) + .pack() + .spanned(self.span())); + } + const SPACING_TO_NUMBERING: Em = Em::new(0.3); let span = self.span(); diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index 31c65a1df..8ab129fdd 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -3,11 +3,13 @@ use std::ops::Deref; use ecow::{eco_format, EcoString}; use smallvec::SmallVec; -use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::diag::{bail, warning, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Content, Label, Packed, Repr, Show, Smart, StyleChain, + cast, elem, Content, Label, NativeElement, Packed, Repr, Show, Smart, StyleChain, + TargetElem, }; +use crate::html::{attr, tag, HtmlElem}; use crate::introspection::Location; use crate::layout::Position; use crate::text::{Hyphenate, TextElem}; @@ -99,18 +101,36 @@ impl LinkElem { impl Show for Packed { #[typst_macros::time(name = "link", span = self.span())] - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { let body = self.body().clone(); - let linked = match self.dest() { - LinkTarget::Dest(dest) => body.linked(dest.clone()), - LinkTarget::Label(label) => { - let elem = engine.introspector.query_label(*label).at(self.span())?; - let dest = Destination::Location(elem.location().unwrap()); - body.clone().linked(dest) - } - }; + let dest = self.dest(); - Ok(linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))))) + Ok(if TargetElem::target_in(styles).is_html() { + if let LinkTarget::Dest(Destination::Url(url)) = dest { + HtmlElem::new(tag::a) + .with_attr(attr::href, url.clone().into_inner()) + .with_body(Some(body)) + .pack() + .spanned(self.span()) + } else { + engine.sink.warn(warning!( + self.span(), + "non-URL links are not yet supported by HTML export" + )); + body + } + } else { + let linked = match self.dest() { + LinkTarget::Dest(dest) => body.linked(dest.clone()), + LinkTarget::Label(label) => { + let elem = engine.introspector.query_label(*label).at(self.span())?; + let dest = Destination::Location(elem.location().unwrap()); + body.clone().linked(dest) + } + }; + + linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))) + }) } } diff --git a/crates/typst-library/src/model/list.rs b/crates/typst-library/src/model/list.rs index 18bddd100..1e369d541 100644 --- a/crates/typst-library/src/model/list.rs +++ b/crates/typst-library/src/model/list.rs @@ -4,8 +4,9 @@ use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, Show, - Smart, StyleChain, Styles, Value, + Smart, StyleChain, Styles, TargetElem, Value, }; +use crate::html::{tag, HtmlElem}; use crate::layout::{BlockElem, Em, Length, VElem}; use crate::model::ParElem; use crate::text::TextElem; @@ -140,6 +141,18 @@ impl ListElem { impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + if TargetElem::target_in(styles).is_html() { + return Ok(HtmlElem::new(tag::ul) + .with_body(Some(Content::sequence(self.children.iter().map(|item| { + HtmlElem::new(tag::li) + .with_body(Some(item.body.clone())) + .pack() + .spanned(item.span()) + })))) + .pack() + .spanned(self.span())); + } + let mut realized = BlockElem::multi_layouter(self.clone(), engine.routines.layout_list) .pack() diff --git a/crates/typst-library/src/model/strong.rs b/crates/typst-library/src/model/strong.rs index 0e23179e6..16d04ba97 100644 --- a/crates/typst-library/src/model/strong.rs +++ b/crates/typst-library/src/model/strong.rs @@ -1,6 +1,9 @@ use crate::diag::SourceResult; use crate::engine::Engine; -use crate::foundations::{elem, Content, Packed, Show, StyleChain}; +use crate::foundations::{ + elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem, +}; +use crate::html::{tag, HtmlElem}; use crate::text::{TextElem, WeightDelta}; /// Strongly emphasizes content by increasing the font weight. @@ -40,9 +43,14 @@ pub struct StrongElem { impl Show for Packed { #[typst_macros::time(name = "strong", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self - .body() - .clone() - .styled(TextElem::set_delta(WeightDelta(self.delta(styles))))) + let body = self.body.clone(); + Ok(if TargetElem::target_in(styles).is_html() { + HtmlElem::new(tag::strong) + .with_body(Some(body)) + .pack() + .spanned(self.span()) + } else { + body.styled(TextElem::set_delta(WeightDelta(self.delta(styles)))) + }) } } diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs index bbcb63fc2..13aa8c6d5 100644 --- a/crates/typst-library/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -4,8 +4,9 @@ use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, - Styles, + Styles, TargetElem, }; +use crate::html::{tag, HtmlElem}; use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem}; use crate::model::{ListItemLike, ListLike, ParElem}; use crate::text::TextElem; @@ -114,6 +115,26 @@ impl TermsElem { impl Show for Packed { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + let span = self.span(); + if TargetElem::target_in(styles).is_html() { + return Ok(HtmlElem::new(tag::dl) + .with_body(Some(Content::sequence(self.children.iter().flat_map( + |item| { + [ + HtmlElem::new(tag::dt) + .with_body(Some(item.term.clone())) + .pack() + .spanned(item.term.span()), + HtmlElem::new(tag::dd) + .with_body(Some(item.description.clone())) + .pack() + .spanned(item.description.span()), + ] + }, + )))) + .pack()); + } + let separator = self.separator(styles); let indent = self.indent(styles); let hanging_indent = self.hanging_indent(styles); @@ -127,7 +148,7 @@ impl Show for Packed { let pad = hanging_indent + indent; let unpad = (!hanging_indent.is_zero()) - .then(|| HElem::new((-hanging_indent).into()).pack().spanned(self.span())); + .then(|| HElem::new((-hanging_indent).into()).pack().spanned(span)); let mut children = vec![]; for child in self.children().iter() { @@ -149,7 +170,7 @@ impl Show for Packed { let mut realized = StackElem::new(children) .with_spacing(Some(gutter.into())) .pack() - .spanned(self.span()) + .spanned(span) .padded(padding); if self.tight(styles) { @@ -158,7 +179,7 @@ impl Show for Packed { .with_weak(true) .with_attach(true) .pack() - .spanned(self.span()); + .spanned(span); realized = spacing + realized; } diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index f318f06e9..10a7cfee1 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -14,8 +14,9 @@ use crate::diag::{At, FileError, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, Args, Array, Bytes, Content, Fold, NativeElement, Packed, - PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, Value, + PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, Value, }; +use crate::html::{tag, HtmlElem}; use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; use crate::model::{Figurable, ParElem}; use crate::text::{ @@ -451,6 +452,14 @@ impl Show for Packed { } let mut realized = Content::sequence(seq); + + if TargetElem::target_in(styles).is_html() { + return Ok(HtmlElem::new(tag::pre) + .with_body(Some(realized)) + .pack() + .spanned(self.span())); + } + if self.block(styles) { // Align the text before inserting it into the block. realized = realized.aligned(self.align(styles).into());