From d4f8b463d2077cf826f4cc17a66bbe7ff1d0ddff Mon Sep 17 00:00:00 2001 From: xfy Date: Wed, 15 Jul 2026 11:38:54 +0800 Subject: [PATCH] =?UTF-8?q?perf(slug):=20slugify=20=E5=8D=95=E9=81=8D?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=9C=BA=E9=87=8D=E5=86=99=EF=BC=8C=E5=88=86?= =?UTF-8?q?=E9=85=8D=204=E2=86=921?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 旧实现 4 次堆分配、3 遍扫描: 1. title.to_lowercase() 分配整串 String(中文/符号根本不受影响却全量拷) 2. split('-').filter().collect::>() 分配 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、下划线保留)。 --- src/api/slug.rs | 77 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/src/api/slug.rs b/src/api/slug.rs index 221620c..94f6e06 100644 --- a/src/api/slug.rs +++ b/src/api/slug.rs @@ -20,30 +20,79 @@ use pinyin::ToPinyin; /// 结果截断至 100 字符;若全部被过滤则回退为当前时间戳。 /// /// 多音字取默认读音(不处理歧义),博客 slug 场景足够。 +/// +/// 单遍状态机实现:旧实现 `to_lowercase` 分配一次 String + `split.collect::` + +/// `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")]