yggdrasil/src/bin/generate_highlight_css.rs
xfy e68b77aac0 style(highlight): unify code block background via --color-paper-code-block
strip syntect-injected background-color from generated CSS so the code
block background comes from a single source (--color-paper-code-block
in input.css), avoiding double-layer color mismatch at the .code span
edge. update the variable values for both light and dark themes.
2026-06-23 15:28:12 +08:00

220 lines
8.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 构建时工具:从 Catppuccin .tmTheme 主题文件生成 `public/highlight.css`。
//!
//! 该二进制文件在编译/构建阶段运行(通过 `make highlight-css` 或
//! `cargo run --bin generate_highlight_css`),使用 syntect 解析 Sublime Text
//! 的 `.tmTheme` XML 主题定义,并输出供博客正文代码块使用的 CSS 类规则。
//!
//! 主要流程:
//! 1. 从 `themes/` 目录加载 Catppuccin Latte浅色与 Mocha深色两套 tmTheme
//! 2. 调用 syntect 的 `css_for_theme_with_class_style` 生成基于类名的 CSS
//! 3. 去掉 syntect 自带的注释以减小体积;
//! 4. 将选择器重写为以 `.md-content pre code` 为作用域,深色模式再加 `.dark ` 前缀;
//! 5. 写入 `public/highlight.css`,供运行时 Markdown 渲染引用。
//!
//! CSS 类前缀规则:
//! - 浅色主题:`{base} { ... }` 与 `{base} .kw { ... }`
//! - 深色主题:`.dark {base} { ... }` 与 `.dark {base} .kw { ... }`
//! - 其中 `{base}` 为 `.md-content pre code`,保证只影响正文代码块;
//! - `.code` 是 syntect 的根作用域特殊类,只映射到 base 容器本身。
//!
//! 背景色策略syntect 输出的 `background-color` 会被剥离,代码块背景统一由
//! `input.css` 中的 `--color-paper-code-block` 变量提供(亮色浅、暗色深),
//! 避免 `.code` span 与外层 `pre` 双层背景叠加导致的边缘色差。
use syntect::highlighting::ThemeSet;
use syntect::html::{css_for_theme_with_class_style, ClassStyle};
fn main() {
// 加载 Catppuccin Latte浅色与 Mocha深色Sublime Text tmTheme 文件
let latte = ThemeSet::get_theme("themes/Catppuccin Latte.tmTheme")
.expect("Failed to load Catppuccin Latte theme");
let mocha = ThemeSet::get_theme("themes/Catppuccin Mocha.tmTheme")
.expect("Failed to load Catppuccin Mocha theme");
// 使用 Spaced 类风格生成 CSS例如 `.kw`、`.kw .kw2`
// 这样每个 token 作用域会生成独立的 class便于精确控制高亮颜色
let latte_css = css_for_theme_with_class_style(&latte, ClassStyle::Spaced)
.expect("Failed to generate Latte CSS");
let mocha_css = css_for_theme_with_class_style(&mocha, ClassStyle::Spaced)
.expect("Failed to generate Mocha CSS");
// 重写选择器:浅色主题无前缀,深色主题加 `.dark ` 前缀
let latte_rewritten = rewrite_rules(&latte_css, ".md-content pre code", "");
let mocha_rewritten = rewrite_rules(&mocha_css, ".md-content pre code", ".dark ");
let mut output = String::new();
output.push_str("/* Auto-generated by generate_highlight_css — DO NOT EDIT */\n");
output.push_str("/* Catppuccin Latte (light) */\n");
output.push_str(&latte_rewritten);
output.push_str("/* Catppuccin Mocha (dark) */\n");
output.push_str(&mocha_rewritten);
// 压缩最终 CSS注释、空白、换行统一在这一步处理minify_css 会跳过 /* */
let output = minify_css(&output);
// 确保 public/ 目录存在并写入生成的 CSS
std::fs::create_dir_all("public").expect("Failed to create public/");
std::fs::write("public/highlight.css", output).expect("Failed to write public/highlight.css");
println!("Generated public/highlight.css");
}
/// 压缩 CSS移除注释、合并空白、删除选择器/属性周围的无用空格。
fn minify_css(css: &str) -> String {
let mut out = String::with_capacity(css.len());
let chars: Vec<char> = css.chars().collect();
let mut i = 0;
let mut last_significant: Option<char> = None;
let mut pending_space = false;
while i < chars.len() {
// 跳过 /* ... */ 注释
if i + 1 < chars.len() && chars[i] == '/' && chars[i + 1] == '*' {
i += 2;
while i + 1 < chars.len() && !(chars[i] == '*' && chars[i + 1] == '/') {
i += 1;
}
i += 2;
continue;
}
let ch = chars[i];
if ch.is_whitespace() {
if last_significant.is_some() {
pending_space = true;
}
i += 1;
continue;
}
// 在特定分隔符前后不需要空格
let no_space_before = "{ } : ; ,".contains(ch);
let no_space_after = last_significant.is_some_and(|c| "{ } : ; ,".contains(c));
if pending_space && !no_space_after && !no_space_before {
out.push(' ');
}
pending_space = false;
out.push(ch);
last_significant = Some(ch);
i += 1;
}
out.trim().to_string()
}
/// 从 CSS 规则体中移除 `background-color` 声明。
///
/// syntect 会为根作用域 `.code` 注入主题底色(如 Latte `#eff1f5`),这与
/// 外层 `pre` 的 `--color-paper-code-block` 背景叠加会形成色差。这里只保留
/// 前景色相关声明,让背景由单一来源控制。
fn strip_background_color(body: &str) -> String {
body.split(';')
.filter(|decl| {
let trimmed = decl.trim();
!trimmed.is_empty()
&& !trimmed
.to_ascii_lowercase()
.starts_with("background-color")
})
.collect::<Vec<_>>()
.join(";")
}
/// 剥离 CSS 字符串开头的 `/* ... */` 注释块(含其后的空白)。
///
/// syntect 输出的第一条规则选择器前会带主题头注释,这里在 `.code` 特判前
/// 先清掉它,使根作用域类能被正确识别并映射到 base 容器本身。
fn strip_leading_comment(s: &str) -> &str {
let trimmed = s.trim_start();
if trimmed.starts_with("/*") {
if let Some(end) = trimmed.find("*/") {
return trimmed[end + 2..].trim_start();
}
}
s
}
/// 将 syntect 生成的 CSS 选择器包装到指定的 base 选择器下,并可附加前缀。
///
/// syntect 原始输出形如:
/// .code { ... }
/// .kw { color: #xxx; }
/// .kw, .kt { color: #yyy; }
///
/// 转换后变为:
/// .md-content pre code { ... }
/// .dark .md-content pre code .kw { ... }
///
/// 参数:
/// - `css`: syntect 生成的原始 CSS
/// - `base`: 作用域基础选择器,这里固定为 `.md-content pre code`
/// - `prefix`: 主题前缀,浅色主题传空字符串,深色主题传 `.dark `。
fn rewrite_rules(css: &str, base: &str, prefix: &str) -> String {
let mut out = String::new();
let mut pos = 0;
let bytes = css.as_bytes();
while pos < bytes.len() {
let rest = &css[pos..];
let open = match rest.find('{') {
Some(i) => i,
None => break,
};
let selector = rest[..open].trim();
if selector.is_empty() {
pos += open + 1;
continue;
}
// syntect 会把主题头注释(/* theme ... */)粘在第一条规则的选择器前。
// 注释虽在后续 minify 阶段会去除,但此处的 .code 特判发生在 minify 之前,
// 所以这里先剥掉前导注释块,避免根作用域类匹配失败。
let selector = strip_leading_comment(selector);
// 查找配对的右大括号,处理可能的嵌套 {}
let after_open = pos + open + 1;
let mut depth = 1usize;
let mut close = after_open;
while close < bytes.len() && depth > 0 {
if bytes[close] == b'{' {
depth += 1;
} else if bytes[close] == b'}' {
depth -= 1;
}
close += 1;
}
let body = css[after_open..close - 1].trim();
pos = close;
if body.is_empty() {
continue;
}
// 剥离 syntect 注入的 background-color代码块背景改由
// `--color-paper-code-block` 变量统一提供(见 input.css
let body = strip_background_color(body);
if body.is_empty() {
continue;
}
// .code 是 syntect 用于根作用域的特殊类,直接映射到 base 容器
if selector == ".code" {
out.push_str(&format!("{}{} {{\n{}\n}}\n\n", prefix, base, body));
} else {
// 对每个逗号分隔的选择器前加上 prefix + base保持多选择器规则完整
let rewritten = selector
.split(',')
.map(|s| format!("{}{} {}", prefix, base, s.trim()))
.collect::<Vec<_>>()
.join(",\n");
out.push_str(&format!("{} {{\n{}\n}}\n\n", rewritten, body));
}
}
out
}