From cac6d0ae3132c5c877db755431aa5f97eeb68ffe Mon Sep 17 00:00:00 2001 From: Jan-Bulthuis Date: Sun, 19 Jul 2026 15:38:52 +0200 Subject: [PATCH] feat: perform highlighting --- src/syntax.rs | 86 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/src/syntax.rs b/src/syntax.rs index d1b5e7d..2b72150 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -5,15 +5,22 @@ use std::{ fs, path::Path, sync::OnceLock, + time::Duration, }; +use ropey::Rope; use tracing::{info, warn}; use tree_house::{ - InjectionLanguageMarker, Language, LanguageConfig, LanguageLoader, highlighter::Highlight, + InjectionLanguageMarker, Language, LanguageConfig, LanguageLoader, Syntax, + highlighter::{Highlight, HighlightEvent, Highlighter}, read_query, }; use tree_house_bindings::Grammar; -use typst::foundations::{Func, NativeFunc, func}; +use typst::{ + foundations::{Content, Func, NativeElement, NativeFunc, func}, + text::TextElem, +}; +use typst_html::{HtmlAttr, HtmlElem, attr, tag}; use crate::AppConfig; @@ -183,7 +190,76 @@ fn highlight( #[named] #[default(None)] lang: Option, -) -> String { - let rev = text.chars().rev(); - rev.collect() +) -> Content { + let highlighted = highlight_code(&text, lang.as_deref()).unwrap_or(TextElem::packed(text)); + + if block { + HtmlElem::new(tag::pre) + .with_body(Some(highlighted)) + .with_attr(HtmlAttr::constant("class"), "code-block") + .pack() + } else { + HtmlElem::new(tag::code) + .with_body(Some(highlighted)) + .with_attr(HtmlAttr::constant("class"), "code-inline") + .pack() + } +} + +/// Highlights `source` as `lang`. +fn highlight_code(source: &str, lang: Option<&str>) -> Option { + let registry = REGISTRY.get()?; + let language = *registry.by_name.get(lang?)?; + + let rope = Rope::from_str(source); + let slice = rope.slice(..); + let syntax = Syntax::new(slice, language, Duration::from_millis(500), registry).ok()?; + let mut hl = Highlighter::new(&syntax, slice, registry, 0..source.len() as u32); + + let src = source.as_bytes(); + let src_len = source.len() as u32; + let mut pos: u32 = 0; + let mut classes = String::new(); + let mut nodes = Vec::::new(); + + loop { + let next = hl.next_event_offset().min(src_len); + + if next > pos { + let text = std::str::from_utf8(&src[pos as usize..next as usize]).ok()?; + let node = TextElem::packed(text); + nodes.push(if classes.is_empty() { + node + } else { + HtmlElem::new(tag::span) + .with_attr(attr::class, classes.clone()) + .with_body(Some(node)) + .pack() + }); + pos = next; + } + + if pos >= src_len { + break; + } + + let (event, highlights) = hl.advance(); + let base = match event { + // Replace the active highlights + HighlightEvent::Refresh => String::new(), + // Extend the active highlights + HighlightEvent::Push => classes.clone(), + }; + classes = highlights.fold(base, |mut acc, h| { + let name = HIGHLIGHT_NAMES[h.idx()].replace('.', "-"); + if !acc.is_empty() { + acc.push(' '); + } + acc.push_str("hl-"); + acc.push_str(&name); + acc + }); + } + + Some(Content::sequence(nodes)) }