1use anyhow::{Context, Result};
2use indoc::{formatdoc, indoc};
3use std::collections::{BTreeMap, HashMap, HashSet};
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use crate::api::IconifyIcon;
8use crate::naming::IconIdentifier;
9
10const MOD_RS_TEMPLATE: &str = indoc! {r#"// Auto-generated by dioxus-iconify - DO NOT EDIT
11 use dioxus::prelude::*;
12
13 #[derive(Clone, Copy, PartialEq)]
14 pub struct IconData {
15 pub name: &'static str,
16 pub body: &'static str,
17 pub view_box: &'static str,
18 pub width: &'static str,
19 pub height: &'static str,
20 }
21
22 #[component]
23 pub fn Icon(
24 data: IconData,
25 /// Additional attributes to extend the svg element
26 #[props(extends = GlobalAttributes)]
27 attributes: Vec<Attribute>,
28 ) -> Element {
29 rsx! {
30 svg {
31 view_box: "{data.view_box}",
32 width: "{data.width}",
33 height: "{data.height}",
34 dangerous_inner_html: "{data.body}",
35 ..attributes,
36 }
37 }
38 }
39 "#};
40
41#[derive(Debug, Clone)]
43struct IconConst {
44 name: String,
45 full_icon_name: String,
46 body: String,
47 view_box: String,
48 width: String,
49 height: String,
50}
51
52impl IconConst {
53 fn from_api_icon(identifier: &IconIdentifier, icon: &IconifyIcon) -> Self {
54 Self {
55 name: identifier.to_const_name(),
56 full_icon_name: identifier.full_name.clone(),
57 body: icon.body.clone(),
58 view_box: icon
59 .view_box
60 .clone()
61 .unwrap_or_else(|| "0 0 24 24".to_string()),
62 width: icon.width.unwrap_or(24).to_string(),
63 height: icon.height.unwrap_or(24).to_string(),
64 }
65 }
66
67 fn to_rust_code(&self) -> String {
68 formatdoc! { "
70 #[allow(non_upper_case_globals)]
71 pub const {}: IconData = IconData {{
72 name: \"{}\",
73 body: r#\"{}\"#,
74 view_box: \"{}\",
75 width: \"{}\",
76 height: \"{}\",
77 }};
78 ",
79 self.name,
80 self.full_icon_name,
81 self.body,
82 self.view_box,
83 self.width,
84 self.height
85 }
86 }
87}
88
89pub struct Generator {
91 icons_dir: PathBuf,
92}
93
94impl Generator {
95 pub fn new(icons_dir: PathBuf) -> Self {
97 Self { icons_dir }
98 }
99
100 pub fn init(&self) -> Result<()> {
102 if !self.icons_dir.exists() {
104 fs::create_dir_all(&self.icons_dir).context("Failed to create icons directory")?;
105 }
106
107 let mod_rs_path = self.icons_dir.join("mod.rs");
109 if !mod_rs_path.exists() {
110 fs::write(&mod_rs_path, MOD_RS_TEMPLATE).context("Failed to create mod.rs")?;
111 }
112
113 Ok(())
114 }
115
116 pub fn add_icons(&self, icons: &[(IconIdentifier, IconifyIcon)]) -> Result<()> {
118 self.init()?;
120
121 let mut icons_by_collection: HashMap<String, Vec<(IconIdentifier, IconifyIcon)>> =
123 HashMap::new();
124
125 for (identifier, icon) in icons {
126 icons_by_collection
127 .entry(identifier.collection.clone())
128 .or_default()
129 .push((identifier.clone(), icon.clone()));
130 }
131
132 for (collection, collection_icons) in &icons_by_collection {
134 self.update_collection_file(collection, collection_icons)?;
135 }
136
137 self.update_mod_rs(&icons_by_collection.keys().cloned().collect::<Vec<_>>())?;
139
140 Ok(())
141 }
142
143 fn update_collection_file(
145 &self,
146 collection: &str,
147 new_icons: &[(IconIdentifier, IconifyIcon)],
148 ) -> Result<()> {
149 let module_name = collection.replace('-', "_");
150 let file_path = self.icons_dir.join(format!("{}.rs", module_name));
151
152 let mut existing_icons: BTreeMap<String, IconConst> = BTreeMap::new();
154 if file_path.exists() {
155 existing_icons = self.parse_collection_file(&file_path)?;
156 }
157
158 for (identifier, icon) in new_icons {
160 let icon_const = IconConst::from_api_icon(identifier, icon);
161 existing_icons.insert(icon_const.name.clone(), icon_const);
162 }
163
164 let content = self.generate_collection_file(collection, &existing_icons)?;
166
167 fs::write(&file_path, content)
169 .context(format!("Failed to write collection file {:?}", file_path))?;
170
171 println!(
172 "✓ Updated {}.rs with {} icon(s)",
173 module_name,
174 new_icons.len()
175 );
176
177 Ok(())
178 }
179
180 fn parse_collection_file(&self, path: &Path) -> Result<BTreeMap<String, IconConst>> {
182 let content =
183 fs::read_to_string(path).context(format!("Failed to read file {:?}", path))?;
184
185 let mut icons = BTreeMap::new();
186
187 let lines: Vec<&str> = content.lines().collect();
190 let mut i = 0;
191
192 while i < lines.len() {
193 let line = lines[i].trim();
194
195 if line.starts_with("pub const ")
197 && line.contains(": IconData")
198 && let Some(name_end) = line.find(':')
199 {
200 let name = line[10..name_end].trim().to_string();
201
202 if let Some(icon_const) = self.parse_icon_data(&lines, &mut i, &name) {
204 icons.insert(name, icon_const);
205 }
206 }
207
208 i += 1;
209 }
210
211 Ok(icons)
212 }
213
214 fn parse_icon_data(
216 &self,
217 lines: &[&str],
218 index: &mut usize,
219 const_name: &str,
220 ) -> Option<IconConst> {
221 let mut full_icon_name = String::new();
222 let mut body = String::new();
223 let mut view_box = String::new();
224 let mut width = String::new();
225 let mut height = String::new();
226
227 let mut j = *index;
229 while j < lines.len() {
230 let line = lines[j].trim();
231
232 if line.contains("name:") {
233 full_icon_name = extract_string_value(line);
234 } else if line.contains("body:") {
235 body = extract_raw_string_value(lines, &mut j);
237 } else if line.contains("view_box:") {
238 view_box = extract_string_value(line);
239 } else if line.contains("width:") {
240 width = extract_string_value(line);
241 } else if line.contains("height:") {
242 height = extract_string_value(line);
243 }
244
245 if line.contains("};") {
247 break;
248 }
249
250 j += 1;
251 }
252
253 *index = j;
254
255 if !full_icon_name.is_empty() && !body.is_empty() {
256 Some(IconConst {
257 name: const_name.to_string(),
258 full_icon_name,
259 body,
260 view_box,
261 width,
262 height,
263 })
264 } else {
265 None
266 }
267 }
268
269 fn generate_collection_file(
271 &self,
272 collection: &str,
273 icons: &BTreeMap<String, IconConst>,
274 ) -> Result<String> {
275 let mut content = format!(
276 "// Auto-generated by dioxus-iconify - DO NOT EDIT\n// Collection: {}\n\nuse super::IconData;\n\n",
277 collection
278 );
279
280 for icon_const in icons.values() {
282 content.push_str(&icon_const.to_rust_code());
283 content.push_str("\n\n");
284 }
285
286 Ok(content)
287 }
288
289 fn update_mod_rs(&self, collections: &[String]) -> Result<()> {
291 let mod_rs_path = self.icons_dir.join("mod.rs");
292
293 let content = fs::read_to_string(&mod_rs_path).context("Failed to read mod.rs")?;
295
296 let mut existing_modules: HashSet<String> = HashSet::new();
298 for line in content.lines() {
299 if line.trim().starts_with("pub mod ")
300 && let Some(module_name) = line
301 .trim()
302 .strip_prefix("pub mod ")
303 .and_then(|s| s.strip_suffix(';'))
304 {
305 existing_modules.insert(module_name.trim().to_string());
306 }
307 }
308
309 let mut needs_update = false;
311 for collection in collections {
312 let module_name = collection.replace('-', "_");
313 if !existing_modules.contains(&module_name) {
314 existing_modules.insert(module_name);
315 needs_update = true;
316 }
317 }
318
319 if needs_update {
321 let mut new_content = MOD_RS_TEMPLATE.to_string();
322 new_content.push('\n');
323
324 let mut sorted_modules: Vec<_> = existing_modules.iter().collect();
326 sorted_modules.sort();
327
328 for module in sorted_modules {
329 new_content.push_str(&format!("pub mod {};\n", module));
330 }
331
332 fs::write(&mod_rs_path, new_content).context("Failed to update mod.rs")?;
333 }
334
335 Ok(())
336 }
337}
338
339fn extract_string_value(line: &str) -> String {
341 if let Some(start) = line.find('"')
342 && let Some(end) = line.rfind('"')
343 && end > start
344 {
345 return line[start + 1..end].to_string();
346 }
347 String::new()
348}
349
350fn extract_raw_string_value(lines: &[&str], index: &mut usize) -> String {
352 let line = lines[*index];
353
354 if let Some(start) = line.find("r#\"") {
356 let start_pos = start + 3;
357
358 if let Some(end) = line[start_pos..].find("\"#") {
360 return line[start_pos..start_pos + end].to_string();
361 }
362
363 let mut result = line[start_pos..].to_string();
365 *index += 1;
366
367 while *index < lines.len() {
368 let next_line = lines[*index];
369 if let Some(end) = next_line.find("\"#") {
370 result.push_str(&next_line[..end]);
371 break;
372 }
373 result.push_str(next_line);
374 result.push('\n');
375 *index += 1;
376 }
377
378 result
379 } else {
380 String::new()
381 }
382}