Rust slugify

每日随笔2023-06-20

将任何 unicode 字符串转为语义化的 ascii 码(方便博客一类应用中操作文件名或 URL 链接)。

生成的 slug 会保留 a-z、0-9 和中划线(-)。此外不会存在两个连续的中划线,也不会以中划线 开头或结尾。

主要用到的库:

示例:

assert_eq!(
    slugify_paths_without_date("My Test String!!!1!1"),
    "my-test-string-1-1"
);
assert_eq!(
    slugify_paths_without_date("2016-08-17-正文内容按换行用标签包装"),
    "zheng-wen-nei-rong-an-huan-xing-yong-biao-qian-bao-zhuang"
);
assert_eq!(
    slugify_paths_without_date("2015-07-17-移动页面基本结构"),
    "yi-dong-ye-mian-ji-ben-jie-gou"
);

功能实现

参考zola中的代码功能简化一下:

use regex::Regex;
use once_cell::sync::Lazy;
use std::path::Path;
 
// 正则分解文件名中的日期
// Based on https://regex101.com/r/H2n38Z/1/tests
// A regex parsing RFC3339 date followed by {_,-} and some characters
pub static RFC3339_DATE: Lazy<Regex> = Lazy::new(|| {
    Regex::new(
        r"^(?P<datetime>(\d{4})-(0[1-9]|1[0-2])
            -(0[1-9]|[12][0-9]|3[01])(T([01][0-9]|2[0-3]):([0-5][0-9])
            :([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)([01][0-9]|2[0-3])
            :([0-5][0-9])))?)\s?(_|-)(?P<slug>.+$)"
    ).unwrap()
});
 
/// 结构化文件名
/// 拆分为路径、文件名、后缀
pub struct CapturedFile {
    // 文件前缀路径
    pub parent: String,
    // 文件名
    pub file_stem: String,
    // 后缀
    pub ext: String,
}
 
impl CapturedFile {
    /// 将文件名结构转为字符串
    pub fn stringify(&self) -> String {
        if !self.file_stem.is_empty()
            && !self.ext.is_empty()
            && !self.parent.is_empty()
        {
            return format!("{}/{}.{}", self.parent, self.file_stem, self.ext);
        }
 
        if !self.file_stem.is_empty()
            && !self.ext.is_empty()
        {
            return format!("{}.{}", self.file_stem, self.ext);
        }
 
        String::from(&self.file_stem)
    }
}
 
/// 将文件路径转为结构化数据 CapturedFile
fn capture_file_name(s: &str) -> CapturedFile {
    let valid_str = s.replace("\\", "/");
    let p = Path::new(&valid_str);
 
    let mut parent = String::new();
    let mut file_stem = String::new();
    let mut ext = String::new();
 
    if let Some(parent_path) = p.parent() {
        parent = parent_path.to_str().unwrap_or("").to_string();
    }
    if let Some(stem) = p.file_stem() {
        file_stem = stem.to_str().unwrap_or("").to_string();
    }
    if let Some(extension) = p.extension() {
        ext = extension.to_str().unwrap_or("").to_string();
    }
 
    CapturedFile {
        parent,
        file_stem,
        ext,
    }
}
 
/// 语义化文件名并去除文件名的日期
pub fn slugify_paths_without_date(s: &str) -> String {
    let mut captured_file = capture_file_name(s);
    let mut file_path = String::from(captured_file.file_stem.as_str());
 
    // 正则匹配包含日期的文件名 无日期则不会匹配
    if let Some(caps) = RFC3339_DATE.captures(file_path.as_str()) {
        if let Some(s) = caps.name("slug") {
            file_path = s.as_str().to_string();
        }
    }
    // 将unicode转为ascii
    let res_slug = slug::slugify(file_path.as_str());
    // 更新文件名为 slug
    captured_file.file_stem = res_slug;
    captured_file.stringify()
}