diff --git a/crates/typst-html/src/convert.rs b/crates/typst-html/src/convert.rs index 171b4cb7e..a767d1e88 100644 --- a/crates/typst-html/src/convert.rs +++ b/crates/typst-html/src/convert.rs @@ -1,3 +1,5 @@ +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}; @@ -5,10 +7,10 @@ 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::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; - -use crate::fragment::html_fragment; -use crate::{attr, tag, FrameElem, HtmlElem, HtmlElement, HtmlFrame, HtmlNode}; +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>( @@ -16,9 +18,10 @@ pub fn convert_to_nodes<'a>( 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 output)?; + handle(engine, child, locator, styles, &mut quoter, &mut output)?; } Ok(output) } @@ -29,6 +32,7 @@ fn handle( child: &Content, locator: &mut SplitLocator, styles: StyleChain, + quoter: &mut SmartQuoter, output: &mut Vec, ) -> SourceResult<()> { if let Some(elem) = child.to_packed::() { @@ -95,10 +99,20 @@ fn handle( } 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::() { - output.push(HtmlNode::text( - if elem.double.get(styles) { '"' } else { '\'' }, - child.span(), - )); + 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(); @@ -123,6 +137,20 @@ fn handle( 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::()