Skip to main content

lepticons_data/
lucide_icon_impl.rs

1use std::collections::BTreeMap;
2use std::str::FromStr;
3use std::sync::OnceLock;
4
5use convert_case::{Case, Casing};
6use lucide_icon_data::LucideGlyph;
7use strum::{EnumProperty, IntoEnumIterator};
8
9use crate::lucide_icon_data;
10
11/// Trait for types that provide SVG content for rendering.
12pub trait Glyph: Copy {
13    /// Returns the inner SVG content as a static string.
14    fn svg(&self) -> &'static str;
15}
16
17impl Glyph for LucideGlyph {
18    fn svg(&self) -> &'static str {
19        self.get_str("svg").unwrap_or("")
20    }
21}
22
23/// Splits a comma-separated `&'static str` into trimmed, non-empty fragments.
24fn split_csv(raw: &'static str) -> impl Iterator<Item = &'static str> {
25    raw.split(',').map(str::trim).filter(|s| !s.is_empty())
26}
27
28/// Pre-built search entry: (icon variant, concatenated searchable text).
29struct SearchEntry {
30    glyph: LucideGlyph,
31    text: String,
32}
33
34/// Global flat search index, built once on first use.
35static SEARCH_INDEX: OnceLock<Vec<SearchEntry>> = OnceLock::new();
36
37/// Global categories cache, built once on first use.
38static CATEGORIES: OnceLock<BTreeMap<String, u16>> = OnceLock::new();
39
40/// Cached icon count, computed once on first use.
41static COUNT: OnceLock<usize> = OnceLock::new();
42
43fn build_search_index() -> Vec<SearchEntry> {
44    LucideGlyph::iter()
45        .map(|glyph| {
46            let name: &'static str = glyph.into();
47            let name_lower = name.to_lowercase();
48            let tags = glyph.get_str("tags").unwrap_or("").to_lowercase();
49            let categories = glyph.get_str("categories").unwrap_or("").to_lowercase();
50            let text = format!("{},{},{}", name_lower, tags, categories);
51            SearchEntry { glyph, text }
52        })
53        .collect()
54}
55
56fn build_categories() -> BTreeMap<String, u16> {
57    let mut categories: BTreeMap<String, u16> = BTreeMap::new();
58    for icon in LucideGlyph::iter() {
59        let cats = icon.get_str("categories").unwrap_or("");
60        for cat in cats.split(',') {
61            let cat = cat.trim();
62            if !cat.is_empty() {
63                let count = categories
64                    .entry(cat.to_case(Case::Title).to_string())
65                    .or_insert(0);
66                *count += 1;
67            }
68        }
69    }
70    categories
71}
72
73impl LucideGlyph {
74    /// Returns the variant name as a static string (e.g. "AArrowDown").
75    /// Zero-allocation.
76    pub fn name(&self) -> &'static str {
77        (*self).into()
78    }
79
80    /// Returns the kebab-case display name (e.g. "a-arrow-down").
81    pub fn kebab_name(&self) -> String {
82        self.name().to_case(Case::Kebab)
83    }
84
85    /// Looks up an icon by its variant name (e.g. "Activity", "ArrowRight").
86    /// Returns `None` if the name doesn't match or the icon's category feature is disabled.
87    pub fn by_name(name: &str) -> Option<LucideGlyph> {
88        LucideGlyph::from_str(name).ok()
89    }
90
91    /// Returns the total number of available icon variants.
92    pub fn count() -> usize {
93        *COUNT.get_or_init(|| LucideGlyph::iter().count())
94    }
95
96    /// Returns the raw categories string from the icon metadata.
97    pub fn categories_str(&self) -> &'static str {
98        self.get_str("categories").unwrap_or("")
99    }
100
101    /// Returns the raw tags string from the icon metadata.
102    pub fn tags_str(&self) -> &'static str {
103        self.get_str("tags").unwrap_or("")
104    }
105
106    /// Returns categories as an iterator of string slices.
107    pub fn categories(&self) -> impl Iterator<Item = &'static str> {
108        split_csv(self.categories_str())
109    }
110
111    /// Returns tags as an iterator of string slices.
112    pub fn tags(&self) -> impl Iterator<Item = &'static str> {
113        split_csv(self.tags_str())
114    }
115
116    /// Returns contributors as an iterator of string slices.
117    pub fn contributors(&self) -> impl Iterator<Item = &'static str> {
118        split_csv(self.get_str("contributors").unwrap_or(""))
119    }
120
121    /// Returns a sorted map of all categories and their icon count.
122    /// Computed once and cached.
123    pub fn all_categories() -> &'static BTreeMap<String, u16> {
124        CATEGORIES.get_or_init(build_categories)
125    }
126
127    /// Finds icons matching the filter string.
128    /// Case-insensitive. Multiple words use AND logic (all must match).
129    /// Uses a pre-built flat index for zero per-call allocations.
130    pub fn find(filter: &str) -> Vec<LucideGlyph> {
131        if filter.is_empty() {
132            return LucideGlyph::iter().collect();
133        }
134
135        let index = SEARCH_INDEX.get_or_init(build_search_index);
136        let filter_lower = filter.to_lowercase();
137        let terms: Vec<&str> = filter_lower.split_whitespace().collect();
138
139        index
140            .iter()
141            .filter(|entry| {
142                terms
143                    .iter()
144                    .all(|term| entry.text.contains(term))
145            })
146            .map(|entry| entry.glyph)
147            .collect()
148    }
149}