simpleicons 0.3.0

A Rust library for loading and querying simple-icons data from a JSON file, providing convenient access to icon metadata by name.
Documentation
use include_dir::{Dir, include_dir};
use once_cell::sync::Lazy;
use regex::Regex;
use serde::Deserialize;
use std::collections::HashMap;
use unicode_normalization::UnicodeNormalization;

static ICONS_DIR: Dir = include_dir!("vendor/simple-icons/icons");

fn load_svg_map() -> HashMap<String, &'static str> {
    let mut map = HashMap::new();
    for file in ICONS_DIR.files() {
        if let Some(path) = file.path().file_name().and_then(|n| n.to_str()) {
            if path.ends_with(".svg") {
                if let Some(content) = file.contents_utf8() {
                    let slug = path.trim_end_matches(".svg");
                    map.insert(slug.to_string(), content);
                }
            }
        }
    }
    map
}
static SVG_MAP: Lazy<HashMap<String, &'static str>> = Lazy::new(load_svg_map);

#[derive(Debug, Deserialize, Clone)]
pub struct Icon {}

impl Icon {
    pub fn get_svg(slug: &str) -> Option<&'static str> {
        let slug = title_to_slug(slug);
        SVG_MAP.get(slug.as_str()).map(|&s| s)
    }
}

#[derive(Debug, Deserialize, Clone)]
pub struct Aliases {
    pub aka: Option<Vec<String>>,
}

#[derive(Debug, Deserialize, Clone)]
pub struct License {
    #[serde(rename = "type")]
    pub license_type: Option<String>,
}

/// 等价于 JS 的 TITLE_TO_SLUG_CHARS_REGEX
static TITLE_TO_SLUG_CHARS_REGEX: Lazy<Regex> = Lazy::new(|| {
    // Rust 原始字符串 r#""#,但 \ 需写成 \\\\(四个反斜杠)
    Regex::new(r#"[ +&/–—:.'’`,!?()\[\]{}*@#$%^=<>_|\";\\\\]"#).unwrap()
});

/// 等价于 JS 的 TITLE_TO_SLUG_REPLACEMENTS
static TITLE_TO_SLUG_REPLACEMENTS: Lazy<HashMap<char, &'static str>> = Lazy::new(|| {
    let mut m = HashMap::new();
    m.insert('+', "plus");
    m.insert('.', "dot");
    m.insert('&', "and");
    m.insert('đ', "d");
    m.insert('ħ', "h");
    m.insert('ı', "i");
    m.insert('ĸ', "k");
    m.insert('ŀ', "l");
    m.insert('ł', "l");
    m.insert('ß', "ss");
    m.insert('ŧ', "t");
    m.insert('ø', "o");
    m
});

/// 等价于 JS 的 TITLE_TO_SLUG_RANGE_REGEX
/// 用于去除所有 unicode combining marks(变音符号)
static TITLE_TO_SLUG_RANGE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\p{M}").unwrap());

/// 将标题字符串转换为 slug
pub fn title_to_slug(title: &str) -> String {
    // 1. 小写
    let lower = title.to_lowercase();
    // 2. 替换特殊字符
    let replaced = TITLE_TO_SLUG_CHARS_REGEX.replace_all(&lower, |caps: &regex::Captures| {
        let ch = caps.get(0).unwrap().as_str().chars().next().unwrap();
        TITLE_TO_SLUG_REPLACEMENTS.get(&ch).copied().unwrap_or("")
    });
    // 3. unicode 规范化(NFD)
    let normalized = replaced.nfd().collect::<String>();
    // 4. 去除变音符号
    let cleaned = TITLE_TO_SLUG_RANGE_REGEX.replace_all(&normalized, "");
    cleaned.to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_svg_existing_slug() {
        // 测试已存在 slug 能获取到非空 svg 内容
        // 以 .NET 为例,source 字段为 .../dotnet-logo.svg
        for slug in [
            "dotnet",
            "dotenv",
            "envoyproxy",
            "renpy",
            "42",
            "playstation5",
            "amazonroute53",
            "p5dotjs",
            "rust",
        ]
        .iter()
        {
            let svg = Icon::get_svg(slug);
            assert!(svg.is_some(), "{} 应该存在 svg", slug);
            assert!(!svg.unwrap().is_empty(), "{} svg 内容不应为空", slug);
        }
    }

    #[test]
    fn test_get_svg_nonexistent_slug() {
        // 测试不存在的 slug 返回 None
        let svg = Icon::get_svg("not-exist-slug");
        assert_eq!(svg, None, "不存在的 slug 应返回 None");
    }
}