diff options
Diffstat (limited to 'crates/typst-html/src/lib.rs')
| -rw-r--r-- | crates/typst-html/src/lib.rs | 421 |
1 files changed, 78 insertions, 343 deletions
diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index 7063931b..d7b29dbb 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -1,33 +1,28 @@ //! Typst's HTML exporter. +mod attr; +mod charsets; +mod convert; mod css; +mod document; +mod dom; mod encode; +mod fragment; mod rules; +mod tag; mod typed; +pub use self::document::html_document; +pub use self::dom::*; pub use self::encode::html; pub use self::rules::register; -use comemo::{Track, Tracked, TrackedMut}; -use typst_library::diag::{bail, warning, At, SourceResult}; -use typst_library::engine::{Engine, Route, Sink, Traced}; -use typst_library::foundations::{ - Content, Module, Scope, StyleChain, Target, TargetElem, -}; -use typst_library::html::{ - attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlFrame, HtmlNode, -}; -use typst_library::introspection::{ - Introspector, Locator, LocatorLink, SplitLocator, TagElem, -}; -use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size}; -use typst_library::model::{DocumentInfo, ParElem}; -use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines}; -use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; -use typst_library::{Category, World}; -use typst_syntax::Span; +use ecow::EcoString; +use typst_library::foundations::{Content, Module, Scope}; +use typst_library::Category; +use typst_macros::elem; -/// Create a module with all HTML definitions. +/// Creates the module with all HTML definitions. pub fn module() -> Module { let mut html = Scope::deduplicating(); html.start_category(Category::Html); @@ -37,337 +32,77 @@ pub fn module() -> Module { Module::new("html", html) } -/// Produce an HTML document from content. +/// An HTML element that can contain Typst content. /// -/// This first performs root-level realization and then turns the resulting -/// elements into HTML. -#[typst_macros::time(name = "html document")] -pub fn html_document( - engine: &mut Engine, - content: &Content, - styles: StyleChain, -) -> SourceResult<HtmlDocument> { - html_document_impl( - engine.routines, - engine.world, - engine.introspector, - engine.traced, - TrackedMut::reborrow_mut(&mut engine.sink), - engine.route.track(), - content, - styles, - ) -} - -/// The internal implementation of `html_document`. -#[comemo::memoize] -#[allow(clippy::too_many_arguments)] -fn html_document_impl( - routines: &Routines, - world: Tracked<dyn World + '_>, - introspector: Tracked<Introspector>, - traced: Tracked<Traced>, - sink: TrackedMut<Sink>, - route: Tracked<Route>, - content: &Content, - styles: StyleChain, -) -> SourceResult<HtmlDocument> { - let mut locator = Locator::root().split(); - let mut engine = Engine { - routines, - world, - introspector, - traced, - sink, - route: Route::extend(route).unnested(), - }; - - // Mark the external styles as "outside" so that they are valid at the page - // level. - let styles = styles.to_map().outside(); - let styles = StyleChain::new(&styles); - - let arenas = Arenas::default(); - let mut info = DocumentInfo::default(); - let children = (engine.routines.realize)( - RealizationKind::HtmlDocument(&mut info), - &mut engine, - &mut locator, - &arenas, - content, - styles, - )?; - - let output = handle_list(&mut engine, &mut locator, children.iter().copied())?; - let introspector = Introspector::html(&output); - let root = root_element(output, &info)?; - - Ok(HtmlDocument { info, root, introspector }) -} - -/// Produce HTML nodes from content. -#[typst_macros::time(name = "html fragment")] -pub fn html_fragment( - engine: &mut Engine, - content: &Content, - locator: Locator, - styles: StyleChain, -) -> SourceResult<Vec<HtmlNode>> { - html_fragment_impl( - engine.routines, - engine.world, - engine.introspector, - engine.traced, - TrackedMut::reborrow_mut(&mut engine.sink), - engine.route.track(), - content, - locator.track(), - styles, - ) -} - -/// The cached, internal implementation of [`html_fragment`]. -#[comemo::memoize] -#[allow(clippy::too_many_arguments)] -fn html_fragment_impl( - routines: &Routines, - world: Tracked<dyn World + '_>, - introspector: Tracked<Introspector>, - traced: Tracked<Traced>, - sink: TrackedMut<Sink>, - route: Tracked<Route>, - content: &Content, - locator: Tracked<Locator>, - styles: StyleChain, -) -> SourceResult<Vec<HtmlNode>> { - let link = LocatorLink::new(locator); - let mut locator = Locator::link(&link).split(); - let mut engine = Engine { - routines, - world, - introspector, - traced, - sink, - route: Route::extend(route), - }; - - engine.route.check_html_depth().at(content.span())?; - - let arenas = Arenas::default(); - let children = (engine.routines.realize)( - // No need to know about the `FragmentKind` because we handle both - // uniformly. - RealizationKind::HtmlFragment(&mut FragmentKind::Block), - &mut engine, - &mut locator, - &arenas, - content, - styles, - )?; - - handle_list(&mut engine, &mut locator, children.iter().copied()) -} - -/// Convert children into HTML nodes. -fn handle_list<'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 a child 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>() { - output.push(HtmlNode::text(elem.text.clone(), 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(()) -} - -/// Wrap the nodes in `<html>` and `<body>` if they are not yet rooted, -/// supplying a suitable `<head>`. -fn root_element(output: Vec<HtmlNode>, info: &DocumentInfo) -> SourceResult<HtmlElement> { - let head = head_element(info); - let body = match classify_output(output)? { - OutputKind::Html(element) => return Ok(element), - OutputKind::Body(body) => body, - OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs), - }; - Ok(HtmlElement::new(tag::html).with_children(vec![head.into(), body.into()])) +/// Typst's HTML export automatically generates the appropriate tags for most +/// elements. However, sometimes, it is desirable to retain more control. For +/// example, when using Typst to generate your blog, you could use this function +/// to wrap each article in an `<article>` tag. +/// +/// Typst is aware of what is valid HTML. A tag and its attributes must form +/// syntactically valid HTML. Some tags, like `meta` do not accept content. +/// Hence, you must not provide a body for them. We may add more checks in the +/// future, so be sure that you are generating valid HTML when using this +/// function. +/// +/// Normally, Typst will generate `html`, `head`, and `body` tags for you. If +/// you instead create them with this function, Typst will omit its own tags. +/// +/// ```typ +/// #html.elem("div", attrs: (style: "background: aqua"))[ +/// A div with _Typst content_ inside! +/// ] +/// ``` +#[elem(name = "elem")] +pub struct HtmlElem { + /// The element's tag. + #[required] + pub tag: HtmlTag, + + /// The element's HTML attributes. + pub attrs: HtmlAttrs, + + /// The contents of the HTML element. + /// + /// The body can be arbitrary Typst content. + #[positional] + pub body: Option<Content>, } -/// Generate a `<head>` element. -fn head_element(info: &DocumentInfo) -> HtmlElement { - let mut children = vec![]; - - children.push(HtmlElement::new(tag::meta).with_attr(attr::charset, "utf-8").into()); - - children.push( - HtmlElement::new(tag::meta) - .with_attr(attr::name, "viewport") - .with_attr(attr::content, "width=device-width, initial-scale=1") - .into(), - ); - - if let Some(title) = &info.title { - children.push( - HtmlElement::new(tag::title) - .with_children(vec![HtmlNode::Text(title.clone(), Span::detached())]) - .into(), - ); +impl HtmlElem { + /// Add an attribute to the element. + pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into<EcoString>) -> Self { + self.attrs + .as_option_mut() + .get_or_insert_with(Default::default) + .push(attr, value); + self } - if let Some(description) = &info.description { - children.push( - HtmlElement::new(tag::meta) - .with_attr(attr::name, "description") - .with_attr(attr::content, description.clone()) - .into(), - ); - } - - if !info.author.is_empty() { - children.push( - HtmlElement::new(tag::meta) - .with_attr(attr::name, "authors") - .with_attr(attr::content, info.author.join(", ")) - .into(), - ) - } - - if !info.keywords.is_empty() { - children.push( - HtmlElement::new(tag::meta) - .with_attr(attr::name, "keywords") - .with_attr(attr::content, info.keywords.join(", ")) - .into(), - ) - } - - HtmlElement::new(tag::head).with_children(children) -} - -/// Determine which kind of output the user generated. -fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> { - let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count(); - for node in &mut output { - let HtmlNode::Element(elem) = node else { continue }; - let tag = elem.tag; - let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html)); - match (tag, count) { - (tag::html, 1) => return Ok(OutputKind::Html(take())), - (tag::body, 1) => return Ok(OutputKind::Body(take())), - (tag::html | tag::body, _) => bail!( - elem.span, - "`{}` element must be the only element in the document", - elem.tag, - ), - _ => {} + /// Adds CSS styles to an element. + fn with_styles(self, properties: css::Properties) -> Self { + if let Some(value) = properties.into_inline_styles() { + self.with_attr(attr::style, value) + } else { + self } } - Ok(OutputKind::Leafs(output)) } -/// What kinds of output the user generated. -enum OutputKind { - /// The user generated their own `<html>` element. We do not need to supply - /// one. - Html(HtmlElement), - /// The user generate their own `<body>` element. We do not need to supply - /// one, but need supply the `<html>` element. - Body(HtmlElement), - /// The user generated leafs which we wrap in a `<body>` and `<html>`. - Leafs(Vec<HtmlNode>), +/// An element that lays out its content as an inline SVG. +/// +/// Sometimes, converting Typst content to HTML is not desirable. This can be +/// the case for plots and other content that relies on positioning and styling +/// to convey its message. +/// +/// This function allows you to use the Typst layout engine that would also be +/// used for PDF, SVG, and PNG export to render a part of your document exactly +/// how it would appear when exported in one of these formats. It embeds the +/// content as an inline SVG. +#[elem] +pub struct FrameElem { + /// The content that shall be laid out. + #[positional] + #[required] + pub body: Content, } |
