diff --git a/crates/typst-html/src/convert.rs b/crates/typst-html/src/convert.rs index 817b0f025..663658f25 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(); @@ -120,6 +134,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::() diff --git a/tests/ref/html/par-semantic-html.html b/tests/ref/html/par-semantic-html.html index 09c7d2fd0..2ae1f6779 100644 --- a/tests/ref/html/par-semantic-html.html +++ b/tests/ref/html/par-semantic-html.html @@ -6,8 +6,8 @@

Heading is no paragraph

-

I'm a paragraph.

-
I'm not.
+

I’m a paragraph.

+
I’m not.

We are two.

So we are paragraphs.

diff --git a/tests/ref/html/quote-nesting-html.html b/tests/ref/html/quote-nesting-html.html index 6b05a94a0..7c2f3d33a 100644 --- a/tests/ref/html/quote-nesting-html.html +++ b/tests/ref/html/quote-nesting-html.html @@ -5,6 +5,6 @@ -

When you said that “he surely meant that ‘she intended to say “I'm sorry”’”, I was quite confused.

+

When you said that “he surely meant that ‘she intended to say “I’m sorry”’”, I was quite confused.

diff --git a/tests/ref/html/smartquotes-html.html b/tests/ref/html/smartquotes-html.html new file mode 100644 index 000000000..7a9d083c6 --- /dev/null +++ b/tests/ref/html/smartquotes-html.html @@ -0,0 +1,11 @@ + + + + + + + +

When you said that “he surely meant that ‘she intended to say “I’m sorry”’”, I was quite confused.

+

box

+ + diff --git a/tests/suite/text/smartquote.typ b/tests/suite/text/smartquote.typ index 6eab35076..022f6b24e 100644 --- a/tests/suite/text/smartquote.typ +++ b/tests/suite/text/smartquote.typ @@ -172,3 +172,8 @@ Some people's thought on this would be #[#set smartquote(enabled: false); "stran --- issue-5146-smartquotes-after-equations --- $i$'s $i$ 's + +--- smartquotes-html html --- +When you said that "he surely meant that 'she intended to say "I'm sorry"'", I was quite confused. + +'#box[box]'