fix(highlight): 编译期内嵌自定义语法,修复线上 Docker 镜像代码高亮缺失

生产镜像是 FROM scratch 的静态 musl 二进制,运行时不存在 syntaxes/ 目录;
CARGO_MANIFEST_DIR 烘焙的是构建机路径(Docker 内为 /build),运行时
add_from_folder 必然失败,Kotlin/Swift/TypeScript/TSX/JSX/Zig/Vue 七种
语言静默回退为纯文本。

改为 include_str! 编译期嵌入 .sublime-syntax,彻底消除运行时文件依赖;
新增两个守护测试:内嵌列表与 syntaxes/ 目录一致性、自定义语法均已加载。
This commit is contained in:
xfy 2026-07-21 14:10:38 +08:00
parent 9e9c6be5ce
commit ccee9131a9

View File

@ -8,18 +8,36 @@ pub mod server {
use std::sync::LazyLock; use std::sync::LazyLock;
use syntect::html::{ClassStyle, ClassedHTMLGenerator}; use syntect::html::{ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet; use syntect::parsing::{SyntaxDefinition, SyntaxSet};
use syntect::util::LinesWithEndings; use syntect::util::LinesWithEndings;
/// 全局语法集合,懒加载时合并内置语法与 `syntaxes/` 目录下的自定义语法。 /// 编译期内嵌的自定义语法定义(文件名 stem → .sublime-syntax 内容)。
static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(|| { ///
/// 生产镜像是 `FROM scratch` 的静态 musl 二进制,容器内不存在 `syntaxes/`
/// 目录;而 `CARGO_MANIFEST_DIR` 烘焙的是构建机路径Docker 里是 /build
/// 运行时 `add_from_folder` 注定失败、这些语言静默回退为纯文本。
/// 因此改为 `include_str!` 编译期嵌入,彻底消除运行时文件依赖。
/// 列表与 `syntaxes/` 目录的一致性由测试 `custom_syntax_list_matches_directory` 保证。
pub(crate) const CUSTOM_SYNTAXES: &[(&str, &str)] = &[
("JSX", include_str!("../syntaxes/JSX.sublime-syntax")),
("Kotlin", include_str!("../syntaxes/Kotlin.sublime-syntax")),
("Swift", include_str!("../syntaxes/Swift.sublime-syntax")),
("TSX", include_str!("../syntaxes/TSX.sublime-syntax")),
("TypeScript", include_str!("../syntaxes/TypeScript.sublime-syntax")),
("Vue", include_str!("../syntaxes/Vue.sublime-syntax")),
("Zig", include_str!("../syntaxes/Zig.sublime-syntax")),
];
/// 全局语法集合,懒加载时合并内置语法与内嵌的自定义语法。
pub(crate) static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(|| {
let mut builder = SyntaxSet::load_defaults_newlines().into_builder(); let mut builder = SyntaxSet::load_defaults_newlines().into_builder();
// 使用 CARGO_MANIFEST_DIR 派生的绝对路径,避免运行时工作目录不确定导致加载失败 for (name, src) in CUSTOM_SYNTAXES {
let syntaxes_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/syntaxes"); match SyntaxDefinition::load_from_str(src, true, Some(name)) {
tracing::info!("Loading custom syntaxes from: {}", syntaxes_dir); Ok(def) => {
match builder.add_from_folder(syntaxes_dir, true) { builder.add(def);
Ok(()) => tracing::info!("Custom syntaxes loaded successfully"), }
Err(e) => tracing::warn!("Failed to load custom syntaxes: {:?}", e), Err(e) => tracing::warn!("Failed to load embedded syntax {}: {:?}", name, e),
}
} }
let built = builder.build(); let built = builder.build();
tracing::info!( tracing::info!(
@ -123,6 +141,42 @@ pub mod server {
mod tests { mod tests {
use super::server::*; use super::server::*;
#[test]
fn custom_syntax_list_matches_directory() {
// CUSTOM_SYNTAXES 必须与 syntaxes/ 目录下的 .sublime-syntax 文件一一对应,
// 防止新增/删除语法文件后忘记同步内嵌列表(仿 migrations 数组的编译期校验)。
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/syntaxes");
let mut on_disk: Vec<String> = std::fs::read_dir(dir)
.expect("syntaxes/ 目录应存在")
.filter_map(|e| {
let p = e.ok()?.path();
if p.extension()? == "sublime-syntax" {
Some(p.file_stem()?.to_string_lossy().into_owned())
} else {
None
}
})
.collect();
on_disk.sort();
let mut embedded: Vec<&str> = CUSTOM_SYNTAXES.iter().map(|(name, _)| *name).collect();
embedded.sort();
assert_eq!(embedded, on_disk, "CUSTOM_SYNTAXES 与 syntaxes/ 目录不一致");
}
#[test]
fn custom_syntaxes_are_loaded() {
// 内嵌语法必须真正进入 SyntaxSet守护生产环境回退纯文本的回归
for (name, _) in CUSTOM_SYNTAXES {
let lower = name.to_lowercase();
assert!(
SYNTAX_SET.find_syntax_by_name(name).is_some()
|| SYNTAX_SET.find_syntax_by_extension(&lower).is_some(),
"自定义语法 {} 未加载",
name
);
}
}
#[test] #[test]
fn highlight_code_rust() { fn highlight_code_rust() {
let result = highlight_code("fn main() {}", Some("rust")); let result = highlight_code("fn main() {}", Some("rust"));