1use std::{
56 env, fs,
57 path::PathBuf,
58 time::{Duration, SystemTime},
59};
60
61use proc_macro::TokenStream;
62use quote::{format_ident, quote};
63use syn::{
64 Attribute, Expr, Lit, Meta, Result, Token, bracketed,
65 parse::{Parse, ParseStream},
66 parse_macro_input,
67 punctuated::Punctuated,
68};
69
70#[derive(Debug)]
71struct IconInner {
72 display_name: Option<String>,
73 name: String,
74 styles: Vec<String>,
75}
76
77impl Parse for IconInner {
78 fn parse(input: ParseStream) -> Result<Self> {
79 let mut display_name = None;
80 let attrs = Attribute::parse_outer(input)?;
81 for attr in attrs {
82 if let Meta::NameValue(meta) = attr.meta {
83 if meta.path.is_ident("name") {
84 if let Expr::Lit(expr_lit) = meta.value {
85 if let Lit::Str(lit_str) = expr_lit.lit {
86 display_name = Some(lit_str.value());
87 }
88 }
89 }
90 }
91 }
92
93 let name = input.parse::<syn::Ident>()?.to_string();
94
95 let mut styles = Vec::new();
96 if input.peek(syn::token::Bracket) {
97 let content;
98 bracketed!(content in input);
99 let style_list = content.parse_terminated(syn::Ident::parse, Token![,])?;
100 styles = style_list
101 .into_iter()
102 .map(|ident| ident.to_string())
103 .collect();
104 }
105 Ok(IconInner {
106 display_name,
107 name,
108 styles,
109 })
110 }
111}
112#[derive(Debug)]
113struct IconItem {
114 icons: Vec<IconInner>,
115}
116
117impl Parse for IconItem {
118 fn parse(input: ParseStream) -> Result<Self> {
119 let content = Punctuated::<IconInner, Token![,]>::parse_terminated(input)?;
120 Ok(Self {
121 icons: content.into_iter().collect(),
122 })
123 }
124}
125#[derive(Debug)]
126struct TablerEnter {
127 attrs: Vec<Attribute>,
128 icons: IconItem,
129}
130
131impl Parse for TablerEnter {
132 fn parse(input: ParseStream) -> Result<Self> {
133 let attrs = Attribute::parse_outer(input).unwrap_or_default();
134 let icons: IconItem = input.parse()?;
137 Ok(TablerEnter {
138 attrs,
139 icons,
140 })
141 }
142}
143
144#[proc_macro]
201pub fn tabler_icon(input: TokenStream) -> TokenStream {
202 let TablerEnter {
203 mut attrs,
204 icons: mut icon_set,
205 } = parse_macro_input!(input as TablerEnter);
206
207 set_first_name_icon_by_name_attribut(&mut attrs, &mut icon_set);
208
209 let default_derives = quote! {
210 #[derive(Debug, Clone, PartialEq, Default)]
211 };
212
213 let attributes = if !attrs.is_empty() {
214 quote! {
215 #(#attrs)*
216 #default_derives
217 }
218 } else {
219 default_derives
220 };
221
222 let mut formated_ident = vec![]; let mut ident_from_str = vec![]; let mut ident_display_merget = vec![]; let mut ident_display = vec![]; let mut download_data = vec![]; for icon in icon_set.icons.iter() {
229 let icon_name = icon.display_name.clone().unwrap_or(
230 icon.name
231 .to_string()
232 .as_str()
233 .trim_end_matches('_')
234 .to_string(),
235 );
236
237 for style in icon.styles.iter() {
239 download_data.push((
241 style.to_string().to_lowercase(),
242 icon.name
243 .to_string()
244 .trim_end_matches("_")
245 .to_lowercase()
246 .replace("_", "-"),
247 ));
248
249 let variant_name = format_ident!(
250 "{}_{}",
251 style.to_string().to_uppercase(),
252 icon_name.to_uppercase()
253 );
254
255 let str_icon_name = format!(
256 "{}_{}",
257 style.to_string().to_lowercase(),
258 icon_name.to_lowercase()
259 );
260
261 ident_from_str.push(quote! {
262 #str_icon_name => Ok(Self::#variant_name),
263 });
264
265 ident_display.push(quote! {
266 Self::#variant_name => write! (f, #str_icon_name)
267 });
268
269 ident_display_merget.push(quote! {
270 Self::#variant_name => #icon_name
271 });
272
273 formated_ident.push(format_ident!(
275 "{}_{}",
276 style.to_string().to_uppercase(),
277 icon_name.to_uppercase()
278 ));
279 }
280 }
281
282 let as_str_match_arms: Vec<_> = formated_ident
283 .iter()
284 .enumerate()
285 .map(|(num, formated_ident)| {
286 let (style, name) = &download_data[num];
287 let icon_url = download_link(style, name);
288
289 let cache_filename = format!("{}_{}.svg", style, name)
290 .replace("/", "_")
291 .replace("\\", "_");
292 let cache_path = get_cache_dir().join(cache_filename);
293 let max_cache_age = Duration::from_secs(60 * 60 * 24 * 30); let icon_content = if cache_path.exists() && is_cache_fresh(&cache_path, max_cache_age)
296 {
297 fs::read_to_string(&cache_path).expect("Failed to read cached icon")
299 } else {
300 let content = reqwest::blocking::get(&icon_url)
302 .expect("Failed to download icon")
303 .text()
304 .expect("Failed to read icon content");
305
306 let content = content.trim_start();
307 let content = {
308 if let Some(position) = content.find("<svg") {
309 &content[position..]
310 } else {
311 panic!(
312 "{} is not correct name with this style: {}!\nPlease check tabler for correct name!\nGot content:{:#?}",
313 name, style, &content
314 );
315 }
316 }.to_string();
317
318 fs::write(&cache_path, &content).expect("Failed to cache icon");
319 content
320 };
321
322 quote! {
323 Self::#formated_ident => {#icon_content}
324 }
325 })
326 .collect();
327
328 let expanded = quote! {
330 #attributes
335 pub enum Tabler {
336 #[default]
337 #(#formated_ident),*
338 }
339
340 impl Tabler {
341 pub fn as_str(&self) -> &str {
343 match self {
344 #(#as_str_match_arms),*
345 }
346 }
347
348 pub fn as_str_merget(&self) -> &str {
352 match self {
353 #(#ident_display_merget),*
354 }
355 }
356
357 pub fn all_icons() -> Vec<Self> {
359 vec![#(Self::#formated_ident),*]
360 }
361
362 #[cfg(feature = "leptos")]
363 pub fn to_leptos(&self) -> leptos::prelude::AnyView {
364 use leptos::html::div;
365 use leptos::prelude::*;
366 match self {
367 #(Self::#formated_ident => {
368 let svg = Self::#formated_ident.as_str();
369 leptos::prelude::view! {<div inner_html=svg />}.into_any()
370 })*
371 }
372 }
373 }
374
375 impl std::str::FromStr for Tabler {
376 type Err = String;
377
378 fn from_str(s: &str) -> Result<Self, Self::Err> {
379 let normalized = s.trim().to_lowercase();
380 match normalized.as_str() {
381 #(#ident_from_str)*
382 _ => Err(format!("Unknown icon variant: {}", normalized))
383 }
384 }
385 }
386
387 impl std::fmt::Display for Tabler {
388 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
389 match self {
390 #(#ident_display),*
391 }
392 }
393 }
394
395
396 };
397
398 expanded.into()
399}
400
401fn get_cache_dir() -> PathBuf {
402 let target_dir = env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string());
404
405 let cache_dir = PathBuf::from(target_dir).join("icon_cache");
407
408 fs::create_dir_all(&cache_dir).expect("Failed to create cache directory");
409 cache_dir
410}
411
412fn is_cache_fresh(
413 cache_path: &PathBuf,
414 max_age: Duration,
415) -> bool {
416 if let Ok(metadata) = fs::metadata(cache_path) {
417 if let Ok(modified) = metadata.modified() {
418 if let Ok(duration) = SystemTime::now().duration_since(modified) {
419 return duration < max_age;
420 }
421 }
422 }
423 false
424}
425
426fn download_link(
428 style_variant: &str,
429 name: &str,
430) -> String {
431 format!(
432 "https://raw.githubusercontent.com/tabler/tabler-icons/refs/heads/main/icons/{}/{}.svg",
433 style_variant, name
434 )
435}
436
437fn set_first_name_icon_by_name_attribut(
438 attrs: &mut Vec<Attribute>,
439 icon_set: &mut IconItem,
440) {
441 if icon_set.icons.is_empty() {
443 panic!("Icon set is empty - must define at least one icon");
444 }
445
446 if let Some(name_index) = attrs
448 .iter()
449 .position(|attr| matches!(&attr.meta, Meta::NameValue(meta) if meta.path.is_ident("name")))
450 {
451 let name_attr = attrs.remove(name_index);
453
454 let name_str = match name_attr.meta {
456 Meta::NameValue(name_value) => match name_value.value {
457 Expr::Lit(lit) => match lit.lit {
458 Lit::Str(lit_str) => lit_str.value(),
459 _ => panic!("Invalid literal type in name attribute - expected string literal"),
460 },
461 _ => panic!("Expected literal expression in name attribute"),
462 },
463 _ => panic!("Unexpected meta format for name attribute"),
464 };
465
466 icon_set
468 .icons
469 .first_mut()
470 .expect("Failed to get first icon")
471 .display_name = Some(name_str);
472 }
473}