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:
@@ -98,15 +98,21 @@ body {
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
/* Bloc de code (entre triple backticks) */
|
||||
.content pre {
|
||||
/* Bloc de code sans coloration syntaxique */
|
||||
.content pre:not(.syntax-highlighting) {
|
||||
background: #0f172a;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Texte à l'intérieur d'un bloc de code */
|
||||
.content pre code {
|
||||
/* Bloc de code avec coloration syntaxique */
|
||||
.content .syntax-highlighting {
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Texte dans un bloc sans coloration */
|
||||
.content pre:not(.syntax-highlighting) code {
|
||||
background: none;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,9 @@ body { background: #f1f5f9; }
|
||||
.content p { color: #1e293b; }
|
||||
.content ul li, .content ol li { color: #1e293b; }
|
||||
.content code { background: #f1f5f9; color: #4f46e5; border: 1px solid #e2e8f0; }
|
||||
.content pre { background: #0f172a; border: 1px solid #1e293b; border-radius: 10px; }
|
||||
.content pre code { background: none; color: #e2e8f0; }
|
||||
.content pre:not(.syntax-highlighting) { background: #0f172a; border: 1px solid #1e293b; border-radius: 10px; }
|
||||
.content .syntax-highlighting { border: 1px solid #1e293b; border-radius: 10px; }
|
||||
.content pre:not(.syntax-highlighting) code { background: none; color: #e2e8f0; }
|
||||
.content blockquote { border-color: #818cf8; background: #f5f3ff; color: #4338ca; }
|
||||
.content table { color: #374151; }
|
||||
.content table th { border-bottom-color: #4f46e5; color: #334155; }
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
use crate::application::{file_service, render_service};
|
||||
use crate::infrastructure::comrak_renderer::{ComrakPreviewRenderer, ComrakRenderer};
|
||||
use crate::infrastructure::comrak_renderer::{
|
||||
syntax_css_for_theme, ComrakPreviewRenderer, ComrakRenderer,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn render_markdown(content: String) -> String {
|
||||
let renderer = ComrakPreviewRenderer {
|
||||
theme: "base16-ocean.dark".to_string(),
|
||||
};
|
||||
let renderer = ComrakPreviewRenderer;
|
||||
render_service::render_string(&renderer, &content)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn convert_file(path: String) -> Result<String, String> {
|
||||
let renderer = ComrakRenderer {
|
||||
theme: "InspiredGitHub".to_string(),
|
||||
};
|
||||
let renderer = ComrakRenderer;
|
||||
render_service::render_file(&renderer, &path)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_syntax_highlight_css() -> String {
|
||||
syntax_css_for_theme("base16-ocean.dark")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_md_files(dir: String) -> Result<Vec<String>, String> {
|
||||
file_service::list_markdown_files(&dir)
|
||||
@@ -41,7 +44,7 @@ mod tests {
|
||||
#[test]
|
||||
fn render_markdown_code_block() {
|
||||
let html = render_markdown("```rust\nfn main() {}\n```\n".to_string());
|
||||
assert!(html.contains("<code") || html.contains("<pre>"));
|
||||
assert!(html.contains("syntax-highlighting"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -80,6 +83,18 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_syntax_highlight_css_returns_non_empty() {
|
||||
let css = get_syntax_highlight_css();
|
||||
assert!(!css.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_syntax_highlight_css_contains_wrapper_class() {
|
||||
let css = get_syntax_highlight_css();
|
||||
assert!(css.contains(".syntax-highlighting {"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_md_files_sorted() {
|
||||
let dir = tmpdir("pena_list_sorted");
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ pub fn run() {
|
||||
commands::render::render_markdown,
|
||||
commands::render::convert_file,
|
||||
commands::render::list_md_files,
|
||||
commands::render::get_syntax_highlight_css,
|
||||
commands::watch::start_watch,
|
||||
commands::watch::stop_watch,
|
||||
commands::theme::list_themes,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<title>Pena — Markdown Viewer</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=Roboto+Mono&display=swap">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
+11
@@ -1,8 +1,18 @@
|
||||
import { showHome, openPath } from './router.js';
|
||||
import { initCssModal } from './ui/css-modal.js';
|
||||
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
const win = window.__TAURI__.window.getCurrentWindow();
|
||||
|
||||
async function injectSyntaxCss() {
|
||||
const css = await invoke('get_syntax_highlight_css');
|
||||
if (!css) return;
|
||||
const style = document.createElement('style');
|
||||
style.id = 'pena-syntax-style';
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
document.getElementById('titlebar').addEventListener('mousedown', (e) => {
|
||||
if (e.target.closest('.titlebar-controls')) return;
|
||||
if (e.buttons === 1) win.startDragging();
|
||||
@@ -32,5 +42,6 @@ document.getElementById('btn-open-dir').addEventListener('click', async () => {
|
||||
|
||||
document.getElementById('btn-back').addEventListener('click', showHome);
|
||||
|
||||
await injectSyntaxCss();
|
||||
await initCssModal();
|
||||
showHome();
|
||||
|
||||
+2
-1
@@ -451,7 +451,8 @@ body {
|
||||
border-radius: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.content pre { background: #f4f4f7; padding: 20px 24px; border-radius: 6px; overflow-x: auto; margin: 16px 0; }
|
||||
.content pre { padding: 20px 24px; border-radius: 6px; overflow-x: auto; margin: 16px 0; }
|
||||
.content pre:not(.syntax-highlighting) { background: #f4f4f7; }
|
||||
.content pre code { background: none; padding: 0; font-weight: 400; font-size: 13px; }
|
||||
|
||||
.content blockquote {
|
||||
|
||||
Reference in New Issue
Block a user