use std::{collections::HashSet, error::Error, fs, path::Path}; use convert_case::{Case, Casing}; use log::info; use regex::Regex; use tempfile::tempdir; use crate::{ UPSTREAM_GIT_URL, framework::Framework, frameworks::{dioxus::Dioxus, leptos::Leptos, yew::Yew}, metadata::Metadata, repository::git_checkout, }; pub fn generate( upstream_repository_rev: &str, repository_path: &Path, ) -> Result<(), Box> { let frameworks: [Box; 3] = [Box::new(Dioxus), Box::new(Leptos), Box::new(Yew)]; let upstream_repository_path = tempdir()?; let repository_icons_path = upstream_repository_path.path().join("icons"); git_checkout( &upstream_repository_path, UPSTREAM_GIT_URL, upstream_repository_rev, )?; info!("Generating icons."); let mut paths = vec![]; for entry in fs::read_dir(repository_icons_path)? { let path = entry?.path(); if !path.is_file() || path.extension().is_none_or(|extension| extension != "svg") { continue; } let file_path = path.clone(); let file_stem = file_path .file_stem() .expect("File stem should exist.") .to_string_lossy() .to_string(); paths.push((file_path, file_stem)); } paths.sort_by_key(|(_, file_stem)| file_stem.clone()); let mut modules = vec![]; let mut component_names = vec![]; let mut metadatas = vec![]; for (path, file_stem) in paths { let file_contents = fs::read_to_string(&path)?; let mut metadata_path = path.clone(); metadata_path.set_extension("json"); let metadata = serde_json::from_str::(&fs::read_to_string(&metadata_path)?)?; metadatas.push(metadata.clone()); let module = file_stem.to_case(Case::Snake); modules.push(module.clone()); let component_name = file_stem.to_case(Case::Pascal); component_names.push(component_name.clone()); info!("{module} - {component_name}"); for framework in &frameworks { generate_icon( repository_path, &**framework, module.clone(), component_name.clone(), file_contents.clone(), )?; } } for framework in &frameworks { generate_lib(repository_path, &**framework, &modules, &metadatas)?; generate_example(repository_path, &**framework, &component_names)?; generate_features(repository_path, &**framework, &metadatas)?; framework.format( format!("lucide-{}", framework.name()), repository_path, repository_path .join("packages") .join(framework.name()) .join("src"), )?; framework.format( format!("lucide-{}-book", framework.name()), repository_path, repository_path .join("book-examples") .join(framework.name()) .join("src"), )?; } update_repository_rev(repository_path, upstream_repository_rev)?; Ok(()) } fn generate_icon( path: &Path, framework: &dyn Framework, module: String, component_name: String, input: String, ) -> Result<(), Box> { let output_path = path .join("packages") .join(framework.name()) .join("src") .join(format!("{module}.rs")); let output_tokens = framework.generate(component_name, input)?; let output = prettyplease::unparse(&syn::parse2(output_tokens)?); fs::write(output_path, output)?; Ok(()) } fn generate_example( path: &Path, framework: &dyn Framework, component_names: &[String], ) -> Result<(), Box> { let output_path = path .join("book-examples") .join(framework.name()) .join("src") .join("icons.rs"); let output_tokens = framework.generate_example(component_names)?; let output = prettyplease::unparse(&syn::parse2(output_tokens)?); fs::write(output_path, output)?; Ok(()) } fn generate_lib( path: &Path, framework: &dyn Framework, modules: &[String], metadatas: &[Metadata], ) -> Result<(), Box> { let attributes = metadatas .iter() .map(|metadata| { let conditions = metadata .categories .iter() .map(|category| format!("feature = \"{category}\"")) .collect::>(); if conditions.len() == 1 { format!("#[cfg({})]", conditions.join(", ")) } else { format!("#[cfg(any({}))]", conditions.join(", ")) } }) .collect::>(); let output_path = path .join("packages") .join(framework.name()) .join("src") .join("lib.rs"); let output_modules = modules .iter() .zip(&attributes) .map(|(module, attribute)| { format!("{attribute}\nmod {};", sanitize_identifier(module.as_str())) }) .collect::>() .join("\n"); let output_uses = modules .iter() .zip(&attributes) .map(|(module, attribute)| { format!( "{attribute}\npub use {}::*;", sanitize_identifier(module.as_str()) ) }) .collect::>() .join("\n"); let output = format!( "{}{}\n\n{}\n", match framework.lib_header() { Some(header) => format!("{header}\n\n"), None => "".into(), }, output_modules, output_uses ); fs::write(output_path, output)?; Ok(()) } fn generate_features( path: &Path, framework: &dyn Framework, metadatas: &[Metadata], ) -> Result<(), Box> { let file_path = path .join("packages") .join(framework.name()) .join("Cargo.toml"); let mut file_contents = fs::read_to_string(&file_path)?; let index = file_contents.find("[features]"); if let Some(index) = index { file_contents = file_contents[0..index].to_string(); } file_contents = file_contents.trim_end_matches("\n").to_string(); let mut features = metadatas .iter() .flat_map(|metadata| metadata.categories.clone()) .collect::>() .into_iter() .collect::>(); features.sort(); let output_all_features = features .iter() .map(|feature| format!(" \"{feature}\",")) .collect::>() .join("\n"); let output_features = features .into_iter() .map(|feature| format!("{feature} = []")) .collect::>() .join("\n"); let output = format!( "{file_contents}\n\n[features]\ndefault = []\n{output_features}\nall-icons = [\n{output_all_features}\n]\n" ); fs::write(&file_path, output)?; Ok(()) } fn update_repository_rev(path: &Path, repository_rev: &str) -> Result<(), Box> { let file_path = path.join("scripts").join("src").join("lib.rs"); let file_contents = fs::read_to_string(&file_path)?; let regex = Regex::new(r"\d+\.\d+\.\d+")?; let output = regex.replace(&file_contents, repository_rev).to_string(); fs::write(&file_path, output)?; Ok(()) } fn sanitize_identifier(identifier: &str) -> &str { match identifier { "box" => "r#box", "move" => "r#move", "type" => "r#type", identifier => identifier, } }