Support for raw syntax highlighting in HTML export (#6691)

This commit is contained in:
Laurenz 2025-08-04 15:35:00 +02:00 committed by GitHub
parent 04b8b3195f
commit 6177c1b22d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 53 additions and 10 deletions

View File

@ -26,7 +26,6 @@ impl Properties {
}
/// Adds a new property in builder-style.
#[expect(unused)]
pub fn with(mut self, property: &str, value: impl Display) -> Self {
self.push(property, value);
self

View File

@ -16,7 +16,7 @@ mod typed;
pub use self::document::html_document;
pub use self::dom::*;
pub use self::encode::html;
pub use self::rules::register;
pub use self::rules::{html_span_filled, register};
use ecow::EcoString;
use typst_library::Category;

View File

@ -17,7 +17,7 @@ use typst_library::text::{
HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SmallcapsElem,
SpaceElem, StrikeElem, SubElem, SuperElem, UnderlineElem,
};
use typst_library::visualize::ImageElem;
use typst_library::visualize::{Color, ImageElem};
use crate::{FrameElem, HtmlAttrs, HtmlElem, HtmlTag, attr, css, tag};
@ -427,6 +427,20 @@ const RAW_RULE: ShowFn<RawElem> = |elem, _, styles| {
})
};
/// This is used by `RawElem::synthesize` through a routine.
///
/// It's a temporary workaround until `TextElem::fill` is supported in HTML
/// export.
#[doc(hidden)]
pub fn html_span_filled(content: Content, color: Color) -> Content {
let span = content.span();
HtmlElem::new(tag::span)
.with_styles(css::Properties::new().with("color", css::color(color)))
.with_body(Some(content))
.pack()
.spanned(span)
}
const RAW_LINE_RULE: ShowFn<RawLine> = |elem, _, _| Ok(elem.body.clone());
const IMAGE_RULE: ShowFn<ImageElem> = |elem, engine, styles| {

View File

@ -15,6 +15,7 @@ use crate::foundations::{
use crate::introspection::{Introspector, Locator, SplitLocator};
use crate::layout::{Frame, Region};
use crate::model::DocumentInfo;
use crate::visualize::Color;
/// Defines the `Routines` struct.
macro_rules! routines {
@ -95,6 +96,12 @@ routines! {
/// Constructs the `html` module.
fn html_module() -> Module
/// Wraps content in a span with a color.
///
/// This is a temporary workaround until `TextElem::fill` is supported in
/// HTML export.
fn html_span_filled(content: Content, color: Color) -> Content
}
/// Defines what kind of realization we are performing.

View File

@ -18,11 +18,12 @@ use crate::diag::{
use crate::engine::Engine;
use crate::foundations::{
Bytes, Content, Derived, OneOrMultiple, Packed, PlainText, ShowSet, Smart,
StyleChain, Styles, Synthesize, cast, elem, scope,
StyleChain, Styles, Synthesize, Target, TargetElem, cast, elem, scope,
};
use crate::layout::{Em, HAlignment};
use crate::loading::{DataSource, Load};
use crate::model::{Figurable, ParElem};
use crate::routines::Routines;
use crate::text::{FontFamily, FontList, LocalName, TextElem, TextSize};
use crate::visualize::Color;
@ -300,8 +301,12 @@ impl RawElem {
}
impl Synthesize for Packed<RawElem> {
fn synthesize(&mut self, _: &mut Engine, styles: StyleChain) -> SourceResult<()> {
let seq = self.highlight(styles);
fn synthesize(
&mut self,
engine: &mut Engine,
styles: StyleChain,
) -> SourceResult<()> {
let seq = self.highlight(engine.routines, styles);
self.lines = Some(seq);
Ok(())
}
@ -309,7 +314,7 @@ impl Synthesize for Packed<RawElem> {
impl Packed<RawElem> {
#[comemo::memoize]
fn highlight(&self, styles: StyleChain) -> Vec<Packed<RawLine>> {
fn highlight(&self, routines: &Routines, styles: StyleChain) -> Vec<Packed<RawLine>> {
let elem = self.as_ref();
let lines = preprocess(&elem.text, styles, self.span());
@ -341,6 +346,7 @@ impl Packed<RawElem> {
};
let foreground = theme.settings.foreground.unwrap_or(synt::Color::BLACK);
let target = styles.get(TargetElem::target);
let mut seq = vec![];
if matches!(lang.as_deref(), Some("typ" | "typst" | "typc" | "typm")) {
@ -363,7 +369,15 @@ impl Packed<RawElem> {
let span_offset = text[..range.start]
.rfind('\n')
.map_or(0, |i| range.start - (i + 1));
styled(&text[range], foreground, style, span, span_offset)
styled(
routines,
target,
&text[range],
foreground,
style,
span,
span_offset,
)
},
&mut |i, range, line| {
let span = lines.get(i).map_or_else(Span::detached, |l| l.1);
@ -400,6 +414,8 @@ impl Packed<RawElem> {
.flatten()
{
line_content.push(styled(
routines,
target,
piece,
foreground,
style,
@ -769,6 +785,8 @@ fn preprocess(
/// Style a piece of text with a syntect style.
fn styled(
routines: &Routines,
target: Target,
piece: &str,
foreground: synt::Color,
style: synt::Style,
@ -782,7 +800,11 @@ fn styled(
}
if style.foreground != foreground {
body = body.set(TextElem::fill, to_typst(style.foreground).into());
let color = to_typst(style.foreground);
body = match target {
Target::Html => (routines.html_span_filled)(body, color),
Target::Paged => body.set(TextElem::fill, color.into()),
};
}
if style.font_style.contains(synt::FontStyle::BOLD) {

View File

@ -358,4 +358,5 @@ pub static ROUTINES: LazyLock<Routines> = LazyLock::new(|| Routines {
realize: typst_realize::realize,
layout_frame: typst_layout::layout_frame,
html_module: typst_html::module,
html_span_filled: typst_html::html_span_filled,
});

View File

@ -6,6 +6,6 @@
</head>
<body>
<p>This is <code><strong>*</strong><strong>inline</strong><strong>*</strong></code>.</p>
<pre><code>#set text(blue)<br><strong>*</strong><strong>Hello</strong><strong>*</strong> <em>_</em><em>world</em><em>_</em>!</code></pre>
<pre><code><span style="color: #d73a49">#</span><span style="color: #d73a49">set</span> <span style="color: #4b69c6">text</span>(blue)<br><strong>*</strong><strong>Hello</strong><strong>*</strong> <em>_</em><em>world</em><em>_</em>!</code></pre>
</body>
</html>