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>,
}
static TITLE_TO_SLUG_CHARS_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"[ +&/–—:.'’`,!?()\[\]{}*@#$%^=<>_|\";\\\\]"#).unwrap()
});
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
});
static TITLE_TO_SLUG_RANGE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\p{M}").unwrap());
pub fn title_to_slug(title: &str) -> String {
let lower = title.to_lowercase();
let replaced = TITLE_TO_SLUG_CHARS_REGEX.replace_all(&lower, |caps: ®ex::Captures| {
let ch = caps.get(0).unwrap().as_str().chars().next().unwrap();
TITLE_TO_SLUG_REPLACEMENTS.get(&ch).copied().unwrap_or("")
});
let normalized = replaced.nfd().collect::<String>();
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() {
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() {
let svg = Icon::get_svg("not-exist-slug");
assert_eq!(svg, None, "不存在的 slug 应返回 None");
}
}