From 9311f6f08eca583b6214834148c94685f0161c21 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 26 Jun 2025 15:44:45 +0200 Subject: [PATCH] Basic support for text decoration functions in HTML (#6510) --- crates/typst-library/src/text/deco.rs | 32 ++++++++++++++++++++++++++- tests/ref/html/html-deco.html | 11 +++++++++ tests/suite/text/deco.typ | 8 +++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/ref/html/html-deco.html diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs index 7aa06e815..d745a48fd 100644 --- a/crates/typst-library/src/text/deco.rs +++ b/crates/typst-library/src/text/deco.rs @@ -2,7 +2,10 @@ use smallvec::smallvec; use crate::diag::SourceResult; use crate::engine::Engine; -use crate::foundations::{elem, Content, Packed, Show, Smart, StyleChain}; +use crate::foundations::{ + elem, Content, NativeElement, Packed, Show, Smart, StyleChain, TargetElem, +}; +use crate::html::{attr, tag, HtmlElem}; use crate::layout::{Abs, Corners, Length, Rel, Sides}; use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric}; use crate::visualize::{Color, FixedStroke, Paint, Stroke}; @@ -81,6 +84,16 @@ pub struct UnderlineElem { impl Show for Packed { #[typst_macros::time(name = "underline", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + if TargetElem::target_in(styles).is_html() { + // Note: In modern HTML, `` is not the underline element, but + // rather an "Unarticulated Annotation" element (see HTML spec + // 4.5.22). Using `text-decoration` instead is recommended by MDN. + return Ok(HtmlElem::new(tag::span) + .with_attr(attr::style, "text-decoration: underline") + .with_body(Some(self.body.clone())) + .pack()); + } + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Underline { stroke: self.stroke(styles).unwrap_or_default(), @@ -173,6 +186,13 @@ pub struct OverlineElem { impl Show for Packed { #[typst_macros::time(name = "overline", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + if TargetElem::target_in(styles).is_html() { + return Ok(HtmlElem::new(tag::span) + .with_attr(attr::style, "text-decoration: overline") + .with_body(Some(self.body.clone())) + .pack()); + } + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Overline { stroke: self.stroke(styles).unwrap_or_default(), @@ -250,6 +270,10 @@ pub struct StrikeElem { impl Show for Packed { #[typst_macros::time(name = "strike", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + if TargetElem::target_in(styles).is_html() { + return Ok(HtmlElem::new(tag::s).with_body(Some(self.body.clone())).pack()); + } + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { // Note that we do not support evade option for strikethrough. line: DecoLine::Strikethrough { @@ -345,6 +369,12 @@ pub struct HighlightElem { impl Show for Packed { #[typst_macros::time(name = "highlight", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + if TargetElem::target_in(styles).is_html() { + return Ok(HtmlElem::new(tag::mark) + .with_body(Some(self.body.clone())) + .pack()); + } + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Highlight { fill: self.fill(styles), diff --git a/tests/ref/html/html-deco.html b/tests/ref/html/html-deco.html new file mode 100644 index 000000000..87f2ab4c8 --- /dev/null +++ b/tests/ref/html/html-deco.html @@ -0,0 +1,11 @@ + + + + + + + +

Struck Highlighted Underlined Overlined

+

Mixed

+ + diff --git a/tests/suite/text/deco.typ b/tests/suite/text/deco.typ index 07fdb6c19..a1d287d9d 100644 --- a/tests/suite/text/deco.typ +++ b/tests/suite/text/deco.typ @@ -83,3 +83,11 @@ We can also specify a customized value #highlight(stroke: 2pt + blue)[abc] #highlight(stroke: (top: blue, left: red, bottom: green, right: orange))[abc] #highlight(stroke: 1pt, radius: 3pt)[#lorem(5)] + +--- html-deco html --- +#strike[Struck] +#highlight[Highlighted] +#underline[Underlined] +#overline[Overlined] + +#(strike, highlight, underline, overline).fold([Mixed], (it, f) => f(it))