use crate::fragment::html_fragment; use crate::{attr, tag, FrameElem, HtmlElem, HtmlElement, HtmlFrame, HtmlNode}; use typst_library::diag::{warning, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{Content, StyleChain, Target, TargetElem}; use typst_library::introspection::{SplitLocator, TagElem}; use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size}; use typst_library::model::ParElem; use typst_library::routines::Pair; use typst_library::text::{ is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, SpaceElem, TextElem, }; /// Converts realized content into HTML nodes. pub fn convert_to_nodes<'a>( engine: &mut Engine, locator: &mut SplitLocator, children: impl IntoIterator>, ) -> SourceResult> { let mut quoter = SmartQuoter::new(); let mut output = Vec::new(); for (child, styles) in children { handle(engine, child, locator, styles, &mut quoter, &mut output)?; } Ok(output) } /// Convert one element into HTML node(s). fn handle( engine: &mut Engine, child: &Content, locator: &mut SplitLocator, styles: StyleChain, quoter: &mut SmartQuoter, output: &mut Vec, ) -> SourceResult<()> { if let Some(elem) = child.to_packed::() { output.push(HtmlNode::Tag(elem.tag.clone())); } else if let Some(elem) = child.to_packed::() { 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::() { 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::() { // 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::() .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::() { output.push(HtmlNode::text(' ', child.span())); } else if let Some(elem) = child.to_packed::() { let text = if let Some(case) = styles.get(TextElem::case) { case.apply(&elem.text).into() } else { elem.text.clone() }; output.push(HtmlNode::text(text, elem.span())); } else if let Some(elem) = child.to_packed::() { output.push(HtmlElement::new(tag::br).spanned(elem.span()).into()); } else if let Some(elem) = child.to_packed::() { let double = elem.double.get(styles); if elem.enabled.get(styles) { let before = last_char(output); let quotes = SmartQuotes::get( elem.quotes.get_ref(styles), styles.get(TextElem::lang), styles.get(TextElem::region), elem.alternative.get(styles), ); let quote = quoter.quote(before, "es, double); output.push(HtmlNode::text(quote, child.span())); } else { output.push(HtmlNode::text(if double { '"' } else { '\'' }, child.span())); } } else if let Some(elem) = child.to_packed::() { 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::new(frame, styles))); } else { engine.sink.warn(warning!( child.span(), "{} was ignored during HTML export", child.elem().name() )); } Ok(()) } /// Returns the last non-default ignorable character from the passed nodes. fn last_char(nodes: &[HtmlNode]) -> Option { for node in nodes.iter().rev() { if let Some(c) = match node { HtmlNode::Text(s, _) => s.chars().rev().find(|&c| !is_default_ignorable(c)), HtmlNode::Element(e) => last_char(&e.children), _ => None, } { return Some(c); } } None } /// Checks whether the given element is an inline-level HTML element. pub fn is_inline(elem: &Content) -> bool { elem.to_packed::() .is_some_and(|elem| tag::is_inline_by_default(elem.tag)) }