//! 构建时工具:从 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 = css.chars().collect(); let mut i = 0; let mut last_significant: Option = 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::>() .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::>() .join(",\n"); out.push_str(&format!("{} {{\n{}\n}}\n\n", rewritten, body)); } } out }