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.
220 lines
8.4 KiB
Rust
220 lines
8.4 KiB
Rust
//! 构建时工具:从 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
|
||
}
|