dioxus_iconify/
naming.rs

1use anyhow::{Result, anyhow};
2use heck::ToPascalCase;
3
4/// Represents a parsed icon identifier (collection:icon-name)
5#[derive(Debug, Clone)]
6pub struct IconIdentifier {
7    pub collection: String,
8    pub icon_name: String,
9    pub full_name: String,
10}
11
12impl IconIdentifier {
13    /// Parse an icon identifier from the format "collection:icon-name"
14    pub fn parse(input: &str) -> Result<Self> {
15        let parts: Vec<&str> = input.split(':').collect();
16        if parts.len() != 2 {
17            return Err(anyhow!(
18                "Invalid icon identifier format. Expected 'collection:icon-name', got '{}'",
19                input
20            ));
21        }
22
23        let collection = parts[0].trim().to_string();
24        let icon_name = parts[1].trim().to_string();
25
26        if collection.is_empty() || icon_name.is_empty() {
27            return Err(anyhow!(
28                "Both collection and icon name must be non-empty in '{}'",
29                input
30            ));
31        }
32
33        Ok(Self {
34            collection,
35            icon_name,
36            full_name: input.to_string(),
37        })
38    }
39
40    /// Get the module name for this collection (e.g., "mdi")
41    pub fn module_name(&self) -> String {
42        self.collection.replace('-', "_")
43    }
44
45    /// Convert the icon name to a valid Rust constant name (PascalCase)
46    pub fn to_const_name(&self) -> String {
47        // Convert to PascalCase
48        let mut const_name = self.icon_name.to_pascal_case();
49
50        // Handle leading numbers (Rust identifiers can't start with numbers)
51        if const_name.chars().next().is_some_and(|c| c.is_numeric()) {
52            const_name = format!("_{}", const_name);
53        }
54
55        // Check for Rust keywords and append suffix if needed
56        if is_rust_keyword(&const_name) {
57            const_name.push_str("Icon");
58        }
59
60        const_name
61    }
62}
63
64/// Check if a string is a Rust keyword
65fn is_rust_keyword(name: &str) -> bool {
66    matches!(
67        name.to_lowercase().as_str(),
68        "as" | "break"
69            | "const"
70            | "continue"
71            | "crate"
72            | "else"
73            | "enum"
74            | "extern"
75            | "false"
76            | "fn"
77            | "for"
78            | "if"
79            | "impl"
80            | "in"
81            | "let"
82            | "loop"
83            | "match"
84            | "mod"
85            | "move"
86            | "mut"
87            | "pub"
88            | "ref"
89            | "return"
90            | "self"
91            | "Self"
92            | "static"
93            | "struct"
94            | "super"
95            | "trait"
96            | "true"
97            | "type"
98            | "unsafe"
99            | "use"
100            | "where"
101            | "while"
102            | "async"
103            | "await"
104            | "dyn"
105    )
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use rstest::rstest;
112
113    #[test]
114    fn test_parse_valid_identifier() {
115        let id = IconIdentifier::parse("mdi:home").unwrap();
116        assert_eq!(id.collection, "mdi");
117        assert_eq!(id.icon_name, "home");
118        assert_eq!(id.full_name, "mdi:home");
119    }
120
121    #[rstest]
122    #[case("invalid")]
123    #[case("too:many:colons")]
124    #[case(":empty-collection")]
125    #[case("empty-name:")]
126    fn test_parse_invalid_identifier(#[case] input: &str) {
127        assert!(IconIdentifier::parse(input).is_err());
128    }
129
130    #[rstest]
131    #[case("mdi:home", "mdi")]
132    #[case("simple-icons:github", "simple_icons")]
133    #[case("heroicons-outline:arrow", "heroicons_outline")]
134    fn test_module_name(#[case] input: &str, #[case] expected: &str) {
135        let id = IconIdentifier::parse(input).unwrap();
136        assert_eq!(id.module_name(), expected);
137    }
138
139    #[rstest]
140    #[case("mdi:home", "Home")]
141    #[case("heroicons:arrow-left", "ArrowLeft")]
142    #[case("lucide:shopping-cart", "ShoppingCart")]
143    #[case("mdi:numeric-1-box", "Numeric1Box")]
144    #[case("mdi:1password", "_1password")] // Leading number
145    #[case("mdi:type", "TypeIcon")] // Rust keyword
146    fn test_to_const_name(#[case] input: &str, #[case] expected: &str) {
147        let id = IconIdentifier::parse(input).unwrap();
148        assert_eq!(id.to_const_name(), expected);
149    }
150}