diff options
Diffstat (limited to 'crates/typst-html/src/document.rs')
| -rw-r--r-- | crates/typst-html/src/document.rs | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/crates/typst-html/src/document.rs b/crates/typst-html/src/document.rs new file mode 100644 index 00000000..9f0124e5 --- /dev/null +++ b/crates/typst-html/src/document.rs @@ -0,0 +1,219 @@ +use std::num::NonZeroUsize; + +use comemo::{Tracked, TrackedMut}; +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::{Engine, Route, Sink, Traced}; +use typst_library::foundations::{Content, StyleChain}; +use typst_library::introspection::{Introspector, IntrospectorBuilder, Locator}; +use typst_library::layout::{Point, Position, Transform}; +use typst_library::model::DocumentInfo; +use typst_library::routines::{Arenas, RealizationKind, Routines}; +use typst_library::World; +use typst_syntax::Span; +use typst_utils::NonZeroExt; + +use crate::{attr, tag, HtmlDocument, HtmlElement, HtmlNode}; + +/// Produce an HTML document from 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 { + info: &mut info, + is_inline: crate::convert::is_inline, + }, + &mut engine, + &mut locator, + &arenas, + content, + styles, + )?; + + let output = crate::convert::convert_to_nodes( + &mut engine, + &mut locator, + children.iter().copied(), + )?; + let introspector = introspect_html(&output); + let root = root_element(output, &info)?; + + Ok(HtmlDocument { info, root, introspector }) +} + +/// Introspects HTML nodes. +#[typst_macros::time(name = "introspect html")] +fn introspect_html(output: &[HtmlNode]) -> Introspector { + fn discover( + builder: &mut IntrospectorBuilder, + sink: &mut Vec<(Content, Position)>, + nodes: &[HtmlNode], + ) { + for node in nodes { + match node { + HtmlNode::Tag(tag) => builder.discover_in_tag( + sink, + tag, + Position { page: NonZeroUsize::ONE, point: Point::zero() }, + ), + HtmlNode::Text(_, _) => {} + HtmlNode::Element(elem) => discover(builder, sink, &elem.children), + HtmlNode::Frame(frame) => builder.discover_in_frame( + sink, + &frame.inner, + NonZeroUsize::ONE, + Transform::identity(), + ), + } + } + } + + let mut elems = Vec::new(); + let mut builder = IntrospectorBuilder::new(); + discover(&mut builder, &mut elems, output); + builder.finalize(elems) +} + +/// 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()])) +} + +/// 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(), + ); + } + + 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, + ), + _ => {} + } + } + 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>), +} |
