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::()