diff --git a/Cargo.lock b/Cargo.lock index 0a1331e..ea1b881 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3272,6 +3272,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pinyin" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e225f595052d9c46045755be4b8d7950b6d9f3c33e0c0b74ba58f11bbfa8c64b" + [[package]] name = "pkg-config" version = "0.3.33" @@ -5491,6 +5497,7 @@ dependencies = [ "lol_html", "md-5 0.10.6", "moka", + "pinyin", "pulldown-cmark", "rand 0.8.6", "regex", diff --git a/Cargo.toml b/Cargo.toml index dc40b61..8552aff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,8 @@ governor = { version = "0.10", optional = true } sysinfo = { version = "0.39", optional = true } sqlparser = { version = "0.62", optional = true } dashmap = { version = "6.2", optional = true } +# 中文标题 → 拼音 slug(纯 Rust、零依赖,安全交叉编译到 musl/FreeBSD)。 +pinyin = { version = "0.11", optional = true } md-5 = { version = "0.10", optional = true } futures = { version = "0.3", optional = true } bytes = { version = "1", optional = true } @@ -106,6 +108,7 @@ server = [ "dep:sysinfo", "dep:sqlparser", "dep:dashmap", + "dep:pinyin", "dep:md-5", "dep:futures", "dep:bytes", diff --git a/src/api/markdown.rs b/src/api/markdown.rs index 1c19ce1..620ec81 100644 --- a/src/api/markdown.rs +++ b/src/api/markdown.rs @@ -361,12 +361,23 @@ fn generate_toc_html(headings: &[(u8, String, String)]) -> String { #[cfg(feature = "server")] /// 将标题文本转换为可用于锚点的 slug。 +/// +/// 汉字转无声调拼音(每字成词,用 `-` 分隔),与 [`crate::api::slug::slugify`] +/// 保持一致;ASCII 字母数字保留,其余字符作为词分隔。结果为空时回退 `heading`。 fn slugify_heading(text: &str) -> String { + use pinyin::ToPinyin; + let mut slug = String::new(); let mut prev_dash = true; for c in text.to_lowercase().chars() { - if c.is_alphanumeric() { + // 汉字优先转拼音;非汉字(含 ascii)返回 None。 + // 每个汉字成词,拼音后补 `-` 与后续内容分隔(连续汉字也会被分开)。 + if let Some(py) = c.to_pinyin() { + slug.push_str(py.plain()); + slug.push('-'); + prev_dash = true; + } else if c.is_alphanumeric() { slug.push(c); prev_dash = false; } else if !prev_dash { @@ -503,8 +514,14 @@ mod tests { #[test] fn slugify_heading_chinese() { - let slug = slugify_heading("你好世界"); - assert!(!slug.is_empty()); + // 汉字逐字转拼音,用 `-` 分隔。 + assert_eq!(slugify_heading("你好世界"), "ni-hao-shi-jie"); + } + + #[test] + fn slugify_heading_mixed_chinese_ascii() { + // Rust 入门指南 → rust + ru-men-zhi-nan,词之间用 `-` 分隔。 + assert_eq!(slugify_heading("Rust 入门指南"), "rust-ru-men-zhi-nan"); } #[test] diff --git a/src/api/slug.rs b/src/api/slug.rs index ef3eede..221620c 100644 --- a/src/api/slug.rs +++ b/src/api/slug.rs @@ -9,26 +9,34 @@ #[cfg(feature = "server")] use dioxus::prelude::*; +#[cfg(feature = "server")] +use pinyin::ToPinyin; + #[cfg(feature = "server")] /// 将标题转换为 URL 友好的 slug。 /// -/// 非字母数字字符替换为 `-` 并合并连续 `-`,结果截断至 100 字符; -/// 若全部字符被过滤,则返回当前时间戳作为 slug。 +/// 汉字转为无声调拼音(每字独立成词,用 `-` 分隔,如 `你好` → `ni-hao`); +/// ASCII 字母数字与 `-`/`_` 保留;其余字符替换为 `-`,连续 `-` 合并。 +/// 结果截断至 100 字符;若全部被过滤则回退为当前时间戳。 +/// +/// 多音字取默认读音(不处理歧义),博客 slug 场景足够。 pub fn slugify(title: &str) -> String { - let slug: String = title - .to_lowercase() - .chars() - .map(|c| { - if c.is_ascii_alphanumeric() || c == '-' || c == '_' { - c - } else { - '-' - } - }) - .collect(); + let mut out = String::with_capacity(title.len()); + for c in title.to_lowercase().chars() { + // 汉字优先转拼音:to_pinyin() 对非汉字(含 ascii)返回 None。 + if let Some(py) = c.to_pinyin() { + out.push_str(py.plain()); + // 每个汉字成词,用 `-` 与后续内容分隔。 + out.push('-'); + } else if c.is_ascii_alphanumeric() || c == '-' || c == '_' { + out.push(c); + } else { + out.push('-'); + } + } // 合并连续的连字符,并去除首尾空段。 - let parts: Vec<&str> = slug.split('-').filter(|s| !s.is_empty()).collect(); + let parts: Vec<&str> = out.split('-').filter(|s| !s.is_empty()).collect(); let slug = parts.join("-"); if slug.is_empty() { @@ -116,8 +124,21 @@ mod tests { #[test] fn slugify_chinese_characters() { - let slug = slugify("你好世界 hello"); - assert!(slug.contains("hello")); + // 汉字逐字转拼音,用 `-` 分隔;混入的 ascii 自然衔接到末尾。 + assert_eq!(slugify("你好世界"), "ni-hao-shi-jie"); + assert_eq!(slugify("你好世界 hello"), "ni-hao-shi-jie-hello"); + } + + #[test] + fn slugify_mixed_chinese_ascii() { + // Rust 入门指南 → rust + ru-men-zhi-nan + assert_eq!(slugify("Rust 入门指南"), "rust-ru-men-zhi-nan"); + } + + #[test] + fn slugify_chinese_with_punctuation() { + // 标点替换为 `-`,随后与拼音分隔符合并。 + assert_eq!(slugify("你好,世界!"), "ni-hao-shi-jie"); } #[test]