A few basic HTML show rules

These are very incomplete and ignore various properties. They are just to get things started.
This commit is contained in:
Laurenz 2024-12-02 14:19:24 +01:00
parent 0ef97c104a
commit 885c7d96ee
9 changed files with 171 additions and 34 deletions

View File

@ -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<EmphElem> {
#[typst_macros::time(name = "emph", span = self.span())]
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(self.body().clone().styled(TextElem::set_emph(ItalicToggle(true))))
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
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)))
})
}
}

View File

@ -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<EnumElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
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()

View File

@ -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<FigureElem> {
impl Show for Packed<FigureElem> {
#[typst_macros::time(name = "figure", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
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<FigureCaption> {
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)
}
}

View File

@ -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<HeadingElem> {
impl Show for Packed<HeadingElem> {
#[typst_macros::time(name = "heading", span = self.span())]
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
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 `<h2>`.
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();

View File

@ -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<LinkElem> {
#[typst_macros::time(name = "link", span = self.span())]
fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult<Content> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
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))))
})
}
}

View File

@ -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<ListElem> {
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
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()

View File

@ -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<StrongElem> {
#[typst_macros::time(name = "strong", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
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))))
})
}
}

View File

@ -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<TermsElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
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<TermsElem> {
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<TermsElem> {
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<TermsElem> {
.with_weak(true)
.with_attach(true)
.pack()
.spanned(self.span());
.spanned(span);
realized = spacing + realized;
}

View File

@ -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<RawElem> {
}
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());