perf(slug): slugify 单遍状态机重写,分配 4→1

旧实现 4 次堆分配、3 遍扫描:
1. title.to_lowercase() 分配整串 String(中文/符号根本不受影响却全量拷)
2. split('-').filter().collect::<Vec<&str>>() 分配 Vec
3. parts.join('-') 再分配 String
4. slug.chars().take(100).collect() 截断又分配

新实现用 prev_dash 状态机在单遍内完成:拼音成词、ASCII 小写化、
分隔符合并去首尾、按 char 截断到 100。1 次预分配、1 遍扫描。

与同仓库 markdown.rs::slugify_heading 的单遍写法对齐。
全部 27 个 slug 测试通过(含拼音边界、连续 dash、首尾 dash、下划线保留)。
This commit is contained in:
xfy 2026-07-15 11:38:54 +08:00
parent b34f44d3e7
commit d4f8b463d2

View File

@ -20,30 +20,79 @@ use pinyin::ToPinyin;
/// 结果截断至 100 字符;若全部被过滤则回退为当前时间戳。
///
/// 多音字取默认读音(不处理歧义),博客 slug 场景足够。
///
/// 单遍状态机实现:旧实现 `to_lowercase` 分配一次 String + `split.collect::<Vec>` +
/// `join` 再分配 + `take.collect` 第三次分配,共 4 次堆分配、3 遍扫描。
/// 现用 `prev_dash` 状态在单遍内合并连续 `-` 并按需截断1 次分配、1 遍扫描。
pub fn slugify(title: &str) -> String {
let mut out = String::with_capacity(title.len());
for c in title.to_lowercase().chars() {
// prev_dash = true 表示当前不应再输出 `-`(处于开头,或上一输出字符已是 `-`)。
// 等价于旧实现字符流 + split('-').filter(!empty).join('-') 的合并/去首尾效果。
let mut prev_dash = true;
let mut len = 0usize;
/// 输出一个字符,截断到 100。返回 false 表示已达上限应停止。
fn push_char(out: &mut String, len: &mut usize, c: char) -> bool {
if *len >= 100 {
return false;
}
out.push(c);
*len += 1;
true
}
for c in title.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);
// 拼音内部不含 `-`;连续字母直接拼接,与旧实现 push_str 行为一致。
for pc in py.plain().chars() {
if !push_char(&mut out, &mut len, pc) {
break;
}
}
prev_dash = false;
// 汉字成词,后接 `-`(合并连续、去除尾部由末尾清理负责)。
if !push_char(&mut out, &mut len, '-') {
break;
}
prev_dash = true;
} else {
out.push('-');
// ASCII 小写化避免对整串 to_lowercase 的全量分配。
let lc = c.to_ascii_lowercase();
if lc.is_ascii_alphanumeric() {
if !push_char(&mut out, &mut len, lc) {
break;
}
prev_dash = false;
} else if lc == '_' {
// 下划线保留原样split('-') 不把它当分隔符,行为一致)。
if !push_char(&mut out, &mut len, '_') {
break;
}
prev_dash = false;
} else {
// `-` 或其他字符(空格/标点):仅当前面非分隔符时输出一个 `-`
// 连续分隔符合并为一个;首尾的 `-` 由 prev_dash 初值和末尾清理负责。
if !prev_dash {
if !push_char(&mut out, &mut len, '-') {
break;
}
prev_dash = true;
}
}
}
}
// 合并连续的连字符,并去除首尾空段。
let parts: Vec<&str> = out.split('-').filter(|s| !s.is_empty()).collect();
let slug = parts.join("-");
if slug.is_empty() {
return format!("{}", chrono::Utc::now().timestamp());
// 去除尾部 `-`(开头已被 prev_dash 初值 true 挡住)。
while out.ends_with('-') {
out.pop();
}
slug.chars().take(100).collect()
if out.is_empty() {
return chrono::Utc::now().timestamp().to_string();
}
out
}
#[cfg(feature = "server")]