use anyhow::{Context, Result};
use indoc::{formatdoc, indoc};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use crate::api::IconifyIcon;
use crate::naming::IconIdentifier;
const MOD_RS_TEMPLATE: &str = indoc! {r#"// Auto-generated by dioxus-iconify - DO NOT EDIT
use dioxus::prelude::*;
#[derive(Clone, Copy, PartialEq)]
pub struct IconData {
pub name: &'static str,
pub body: &'static str,
pub view_box: &'static str,
pub width: &'static str,
pub height: &'static str,
}
#[component]
pub fn Icon(
data: IconData,
/// Additional attributes to extend the svg element
#[props(extends = GlobalAttributes)]
attributes: Vec<Attribute>,
) -> Element {
rsx! {
svg {
view_box: "{data.view_box}",
width: "{data.width}",
height: "{data.height}",
dangerous_inner_html: "{data.body}",
..attributes,
}
}
}
"#};
#[derive(Debug, Clone)]
struct IconConst {
name: String,
full_icon_name: String,
body: String,
view_box: String,
width: String,
height: String,
}
impl IconConst {
fn from_api_icon(identifier: &IconIdentifier, icon: &IconifyIcon) -> Self {
Self {
name: identifier.to_const_name(),
full_icon_name: identifier.full_name.clone(),
body: icon.body.clone(),
view_box: icon
.view_box
.clone()
.unwrap_or_else(|| "0 0 24 24".to_string()),
width: icon.width.unwrap_or(24).to_string(),
height: icon.height.unwrap_or(24).to_string(),
}
}
fn to_rust_code(&self) -> String {
formatdoc! { "
#[allow(non_upper_case_globals)]
pub const {}: IconData = IconData {{
name: \"{}\",
body: r#\"{}\"#,
view_box: \"{}\",
width: \"{}\",
height: \"{}\",
}};
",
self.name,
self.full_icon_name,
self.body,
self.view_box,
self.width,
self.height
}
}
}
pub struct Generator {
icons_dir: PathBuf,
}
impl Generator {
pub fn new(icons_dir: PathBuf) -> Self {
Self { icons_dir }
}
pub fn init(&self) -> Result<()> {
if !self.icons_dir.exists() {
fs::create_dir_all(&self.icons_dir).context("Failed to create icons directory")?;
}
let mod_rs_path = self.icons_dir.join("mod.rs");
if !mod_rs_path.exists() {
fs::write(&mod_rs_path, MOD_RS_TEMPLATE).context("Failed to create mod.rs")?;
}
Ok(())
}
pub fn add_icons(&self, icons: &[(IconIdentifier, IconifyIcon)]) -> Result<()> {
self.init()?;
let mut icons_by_collection: HashMap<String, Vec<(IconIdentifier, IconifyIcon)>> =
HashMap::new();
for (identifier, icon) in icons {
icons_by_collection
.entry(identifier.collection.clone())
.or_default()
.push((identifier.clone(), icon.clone()));
}
for (collection, collection_icons) in &icons_by_collection {
self.update_collection_file(collection, collection_icons)?;
}
self.update_mod_rs(&icons_by_collection.keys().cloned().collect::<Vec<_>>())?;
Ok(())
}
fn update_collection_file(
&self,
collection: &str,
new_icons: &[(IconIdentifier, IconifyIcon)],
) -> Result<()> {
let module_name = collection.replace('-', "_");
let file_path = self.icons_dir.join(format!("{}.rs", module_name));
let mut existing_icons: BTreeMap<String, IconConst> = BTreeMap::new();
if file_path.exists() {
existing_icons = self.parse_collection_file(&file_path)?;
}
for (identifier, icon) in new_icons {
let icon_const = IconConst::from_api_icon(identifier, icon);
existing_icons.insert(icon_const.name.clone(), icon_const);
}
let content = self.generate_collection_file(collection, &existing_icons)?;
fs::write(&file_path, content)
.context(format!("Failed to write collection file {:?}", file_path))?;
println!(
"✓ Updated {}.rs with {} icon(s)",
module_name,
new_icons.len()
);
Ok(())
}
fn parse_collection_file(&self, path: &Path) -> Result<BTreeMap<String, IconConst>> {
let content =
fs::read_to_string(path).context(format!("Failed to read file {:?}", path))?;
let mut icons = BTreeMap::new();
let lines: Vec<&str> = content.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i].trim();
if line.starts_with("pub const ")
&& line.contains(": IconData")
&& let Some(name_end) = line.find(':')
{
let name = line[10..name_end].trim().to_string();
if let Some(icon_const) = self.parse_icon_data(&lines, &mut i, &name) {
icons.insert(name, icon_const);
}
}
i += 1;
}
Ok(icons)
}
fn parse_icon_data(
&self,
lines: &[&str],
index: &mut usize,
const_name: &str,
) -> Option<IconConst> {
let mut full_icon_name = String::new();
let mut body = String::new();
let mut view_box = String::new();
let mut width = String::new();
let mut height = String::new();
let mut j = *index;
while j < lines.len() {
let line = lines[j].trim();
if line.contains("name:") {
full_icon_name = extract_string_value(line);
} else if line.contains("body:") {
body = extract_raw_string_value(lines, &mut j);
} else if line.contains("view_box:") {
view_box = extract_string_value(line);
} else if line.contains("width:") {
width = extract_string_value(line);
} else if line.contains("height:") {
height = extract_string_value(line);
}
if line.contains("};") {
break;
}
j += 1;
}
*index = j;
if !full_icon_name.is_empty() && !body.is_empty() {
Some(IconConst {
name: const_name.to_string(),
full_icon_name,
body,
view_box,
width,
height,
})
} else {
None
}
}
fn generate_collection_file(
&self,
collection: &str,
icons: &BTreeMap<String, IconConst>,
) -> Result<String> {
let mut content = format!(
"// Auto-generated by dioxus-iconify - DO NOT EDIT\n// Collection: {}\n\nuse super::IconData;\n\n",
collection
);
for icon_const in icons.values() {
content.push_str(&icon_const.to_rust_code());
content.push_str("\n\n");
}
Ok(content)
}
fn update_mod_rs(&self, collections: &[String]) -> Result<()> {
let mod_rs_path = self.icons_dir.join("mod.rs");
let content = fs::read_to_string(&mod_rs_path).context("Failed to read mod.rs")?;
let mut existing_modules: HashSet<String> = HashSet::new();
for line in content.lines() {
if line.trim().starts_with("pub mod ")
&& let Some(module_name) = line
.trim()
.strip_prefix("pub mod ")
.and_then(|s| s.strip_suffix(';'))
{
existing_modules.insert(module_name.trim().to_string());
}
}
let mut needs_update = false;
for collection in collections {
let module_name = collection.replace('-', "_");
if !existing_modules.contains(&module_name) {
existing_modules.insert(module_name);
needs_update = true;
}
}
if needs_update {
let mut new_content = MOD_RS_TEMPLATE.to_string();
new_content.push('\n');
let mut sorted_modules: Vec<_> = existing_modules.iter().collect();
sorted_modules.sort();
for module in sorted_modules {
new_content.push_str(&format!("pub mod {};\n", module));
}
fs::write(&mod_rs_path, new_content).context("Failed to update mod.rs")?;
}
Ok(())
}
}
fn extract_string_value(line: &str) -> String {
if let Some(start) = line.find('"')
&& let Some(end) = line.rfind('"')
&& end > start
{
return line[start + 1..end].to_string();
}
String::new()
}
fn extract_raw_string_value(lines: &[&str], index: &mut usize) -> String {
let line = lines[*index];
if let Some(start) = line.find("r#\"") {
let start_pos = start + 3;
if let Some(end) = line[start_pos..].find("\"#") {
return line[start_pos..start_pos + end].to_string();
}
let mut result = line[start_pos..].to_string();
*index += 1;
while *index < lines.len() {
let next_line = lines[*index];
if let Some(end) = next_line.find("\"#") {
result.push_str(&next_line[..end]);
break;
}
result.push_str(next_line);
result.push('\n');
*index += 1;
}
result
} else {
String::new()
}
}