feat: coloration syntaxique via CSS classes (themeable)

Remplace les styles inline syntect par des classes CSS. Le CSS est
généré côté Rust (base16-ocean.dark) et injecté au démarrage via
get_syntax_highlight_css. Les thèmes peuvent surcharger .syntax-highlighting
et les classes de tokens. Supprime le lien CDN highlight.js inutilisé.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 15:36:08 +02:00
parent 75c85e7e0a
commit df899bd1a8
8 changed files with 107 additions and 26 deletions
@@ -1,19 +1,16 @@
use comrak::plugins::syntect::SyntectAdapterBuilder;
use comrak::{markdown_to_html_with_plugins, Options, Plugins};
use syntect::highlighting::ThemeSet;
use syntect::html::{css_for_theme_with_class_style, ClassStyle};
use crate::domain::MarkdownRenderer;
pub struct ComrakRenderer {
pub theme: String,
}
pub struct ComrakPreviewRenderer {
pub theme: String,
}
pub struct ComrakRenderer;
pub struct ComrakPreviewRenderer;
impl MarkdownRenderer for ComrakRenderer {
fn render(&self, content: &str) -> String {
let adapter = SyntectAdapterBuilder::new().theme(&self.theme).build();
let adapter = SyntectAdapterBuilder::new().css().build();
let mut options = Options::default();
options.extension.table = true;
options.extension.strikethrough = true;
@@ -27,10 +24,60 @@ impl MarkdownRenderer for ComrakRenderer {
impl MarkdownRenderer for ComrakPreviewRenderer {
fn render(&self, content: &str) -> String {
let adapter = SyntectAdapterBuilder::new().theme(&self.theme).build();
let adapter = SyntectAdapterBuilder::new().css().build();
let options = Options::default();
let mut plugins = Plugins::default();
plugins.render.codefence_syntax_highlighter = Some(&adapter);
markdown_to_html_with_plugins(content, &options, &plugins)
}
}
pub fn syntax_css_for_theme(syntect_theme_name: &str) -> String {
let ts = ThemeSet::load_defaults();
let theme = match ts.themes.get(syntect_theme_name) {
Some(t) => t,
None => return String::new(),
};
match css_for_theme_with_class_style(theme, ClassStyle::Spaced) {
Ok(css) => css.replace(".code {", ".syntax-highlighting {"),
Err(_) => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_outputs_syntax_highlighting_class() {
let r = ComrakRenderer;
let html = r.render("```rust\nfn main() {}\n```\n");
assert!(html.contains("syntax-highlighting"));
}
#[test]
fn render_outputs_classed_spans() {
let r = ComrakRenderer;
let html = r.render("```rust\nfn main() {}\n```\n");
assert!(html.contains("<span class="));
}
#[test]
fn syntax_css_known_theme_contains_wrapper() {
let css = syntax_css_for_theme("base16-ocean.dark");
assert!(!css.is_empty());
assert!(css.contains(".syntax-highlighting {"));
}
#[test]
fn syntax_css_known_theme_contains_token_rules() {
let css = syntax_css_for_theme("base16-ocean.dark");
assert!(css.contains("color:") || css.contains("color :"));
}
#[test]
fn syntax_css_unknown_theme_returns_empty() {
let css = syntax_css_for_theme("nonexistent-theme-xyz");
assert!(css.is_empty());
}
}