#![allow(dead_code)]
use inflections::Inflect;
use lazy_static::lazy_static;
use regex::Regex;
use serde::Deserialize;
use std::collections::HashMap;
use std::error::Error;
use std::ffi::OsStr;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
use zu_util::icon::{get_svg_path_data, need_update_with_name};
const SVG_DIR: &str = "icons";
const CUSTOM_DIR: &str = "custom";
const TEMPLATE_FILE: &str = include_str!("src/template.rs");
#[derive(Debug, Clone, Deserialize)]
struct IconsIndex {
host: String,
asset_url_pattern: String,
families: Vec<String>,
icons: Vec<Icon>,
}
#[derive(Debug, Clone, Deserialize)]
struct Icon {
name: String,
version: i32,
popularity: i32,
codepoint: i32,
unsupported_families: Vec<String>,
categories: Vec<String>,
tags: Vec<String>,
sizes_px: Vec<i32>,
}
fn download_index() -> Result<IconsIndex, Box<dyn Error>> {
let url = "https://fonts.google.com/metadata/icons";
let resp = reqwest::blocking::get(url)?.text()?;
let resp = resp.replace(")]}'", "");
let index: IconsIndex = serde_json::from_str(&resp)?;
Ok(index)
}
const IGNORED_NAMES: &[&str] = &[
"123",
"6_ft_apart",
"add_chart", "area_chart",
"compost",
"cruelty_free",
"data_exploration",
"disabled_visible",
"drive_file_move_rtl",
"emergency",
"exposure_neg_1", "exposure_neg_2", "exposure_plus_1", "exposure_plus_2", "exposure_zero", "free_cancellation",
"front_hand",
"generating_tokens",
"group_off",
"horizontal_distribute", "hotel_class",
"incomplete_circle",
"motion_photos_on", "motion_photos_pause", "motion_photos_paused", "new_label",
"personal_injury",
"pin_end",
"pin_invoke",
"polymer", "private_connectivity",
"real_estate_agent",
"vertical_distribute", ];
fn is_icon_ignored(icon: &Icon) -> bool {
IGNORED_NAMES.contains(&icon.name.as_str())
}
fn download_icons(index: &IconsIndex) -> Result<i32, Box<dyn Error>> {
let theme_map = &[
("baseline", ""), ("outline", "_outlined"),
("round", "_round"),
("twotone", "_two_tone"),
("sharp", "_sharp"),
];
let theme_file_map = [
("baseline", ""), ("outline", "_outlined"),
("round", "_rounded"),
("twotone", "_two_tone"),
("sharp", "_sharp"),
];
let theme_file_map = HashMap::from(theme_file_map);
let _ret = fs::create_dir(SVG_DIR);
let mut count = 0;
for (theme, value) in theme_map {
let formatted_theme = value.split('_').collect::<Vec<_>>().join("");
for icon in &index.icons {
if is_icon_ignored(icon) {
continue;
}
let name = &icon.name;
let version = &icon.version;
let url = format!("https://fonts.gstatic.com/s/i/materialicons{formatted_theme}/{name}/v{version}/24px.svg");
println!("Downloading icon {url}");
let file_map = theme_file_map.get(theme).unwrap();
let output_file = format!("{SVG_DIR}/{name}{file_map}_24px.svg");
if fs::exists(&output_file).unwrap() {
count += 1;
continue;
}
let resp = reqwest::blocking::get(url)?.text()?;
fs::write(output_file, resp)?;
count += 1;
}
}
Ok(count)
}
fn map_filename(name: &str) -> String {
let name: String = name.replace("_24px", "");
const KEYWORD_NAMES: &[&str] = &["box", "try", "loop", "html"];
if KEYWORD_NAMES.contains(&name.as_str()) {
return format!("icon-{name}");
}
const NAME_MAP: &[(&str, &str)] = &[
("1x", "TimesOne"),
("3d_", "ThreeD_"),
("3g", "ThreeG"),
("3p", "ThreeP"),
("30fps", "ThirtyFps"),
("4g", "FourG"),
("5g", "FiveG"),
("60fps", "SixtyFps"),
("360", "ThreeSixty"),
];
for (old, new) in NAME_MAP {
if name.starts_with(old) {
return name.replace(old, new);
}
}
const SINGLE_DIGIT_NUMBERS: &[&str] = &[
"Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine",
];
const TWO_DIGIT_NUMBERS1: &[&str] = &[
"Ten",
"Eleven",
"Twelve",
"Thirteen",
"Fourteen",
"Fifteen",
"Sixteen",
"Seventeen",
"Eighteen",
"Nineteen",
];
const TWO_DIGIT_NUMBERS2: &[&str] = &[
"Twenty", "Thirty", "Fourty", "Fifty", "Sxity", "Seventy", "Eighty", "Ninety",
];
lazy_static! {
static ref RE: Regex = Regex::new(r"^(\d+)([km]?)").unwrap();
}
if let Some(cap) = RE.captures(&name) {
let integer = cap[1].parse::<usize>().unwrap();
let m = cap.get(0).unwrap();
let digits = if integer < 10 {
SINGLE_DIGIT_NUMBERS[integer].to_owned()
} else if integer < 20 {
TWO_DIGIT_NUMBERS1[integer - 10].to_owned()
} else if integer < 100 {
let quotient = integer / 10;
let remainder = integer % 10;
format!(
"{}{}",
TWO_DIGIT_NUMBERS2[quotient], SINGLE_DIGIT_NUMBERS[remainder]
)
} else {
panic!("Integer out of range");
};
let suffix = if "k" == &cap[2] {
"K"
} else if "m" == &cap[2] {
"M"
} else {
""
};
return format!("{digits}{suffix}{}", &name[m.end()..]);
}
name
}
fn build_icons(icons_dir: &str) -> Result<Vec<(String, String)>, Box<dyn Error>> {
let mut dir = PathBuf::new();
dir.push(icons_dir);
let mut module_names = Vec::new();
let svg_extension = OsStr::new("svg");
for entry in fs::read_dir(&dir)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
if path.extension() != Some(svg_extension) {
println!("Ignore non svg file {path:?}");
continue;
}
let stem = path.file_stem().unwrap();
let stem_str = stem.to_str().unwrap();
let stem_str = map_filename(stem_str);
let node_name = stem_str.to_pascal_case();
let module_name = stem_str.to_snake_case();
let mut rs_filepath = PathBuf::new();
rs_filepath.push("src");
rs_filepath.push(icons_dir);
rs_filepath.push(&module_name);
rs_filepath.set_extension("rs");
let svg_content = fs::read_to_string(&path)?;
let path_data = get_svg_path_data(&svg_content)?;
let rs_content = TEMPLATE_FILE
.replace("{MODULE_NAME}", &module_name)
.replace("{NODE_NAME}", &node_name)
.replace("{ICON_NAME}", &node_name)
.replace("{PATH_DATA}", &path_data);
fs::write(rs_filepath, rs_content).unwrap();
module_names.push((module_name, node_name));
}
module_names.sort();
Ok(module_names)
}
fn generate_components(icons_dir: &str) -> Result<(), Box<dyn Error>> {
let module_names = build_icons(icons_dir)?;
let mut icons_file = OpenOptions::new()
.append(true)
.open(format!("src/{icons_dir}.rs"))?;
for (module_name, node_name) in module_names.iter() {
let line = format!(
r#"mod {module_name};
pub use {module_name}::{node_name};
"#
);
icons_file.write_all(line.as_bytes())?;
}
drop(icons_file);
Ok(())
}
fn run() -> Result<(), Box<dyn Error>> {
if std::env::var_os("MATERIAL_DOWNLOAD").map_or(false, |val| !val.is_empty()) {
let icons_index = download_index()?;
let _count = download_icons(&icons_index)?;
}
generate_components(SVG_DIR)?;
generate_components(CUSTOM_DIR)?;
Ok(())
}
fn main() {
if need_update_with_name("material") {
run().unwrap();
}
}