free_icons/
lib.rs

1use flate2::bufread::GzDecoder;
2use std::{borrow::Cow, collections::HashMap, io::Read};
3
4mod gen;
5
6const MAX_ATTRS: usize = 16;
7#[derive(Debug, Default, Clone, PartialEq, Eq)]
8pub struct IconAttrs<'a> {
9    data: [(&'a str, Cow<'a, str>); MAX_ATTRS],
10    pos: u8,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub enum IconType {
15    #[cfg(feature = "bootstrap")]
16    Bootstrap(Bootstrap),
17    #[cfg(feature = "feather")]
18    Feather(Feather),
19    #[cfg(feature = "font-awesome")]
20    FontAwesome(FontAwesome),
21    #[cfg(feature = "heroicons")]
22    Heroicons(Heroicons),
23    #[cfg(feature = "ionicons")]
24    Ionicons(Ionicons),
25    #[cfg(feature = "octicons")]
26    Octicons(Octicons),
27}
28
29#[cfg(feature = "bootstrap")]
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub enum Bootstrap {
32    Fill,
33    Normal,
34}
35
36#[cfg(feature = "feather")]
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub enum Feather {
39    Normal,
40}
41
42#[cfg(feature = "font-awesome")]
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub enum FontAwesome {
45    Regular,
46    Solid,
47}
48
49#[cfg(feature = "heroicons")]
50#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
51pub enum Heroicons {
52    Outline,
53    Solid,
54}
55
56#[cfg(feature = "ionicons")]
57#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
58pub enum Ionicons {
59    Outline,
60    Sharp,
61    Normal,
62}
63
64#[cfg(feature = "octicons")]
65#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
66pub enum Octicons {
67    Normal,
68}
69
70/// retrieve the SVG from incon_type and name
71pub fn get(icon_type: IconType, name: &str) -> Option<&'static String> {
72    match icon_type {
73        #[cfg(feature = "bootstrap")]
74        IconType::Bootstrap(icon_type) => match icon_type {
75            Bootstrap::Fill => gen::bootstrap::FILL.get(name),
76            Bootstrap::Normal => gen::bootstrap::NORMAL.get(name),
77        },
78        #[cfg(feature = "feather")]
79        IconType::Feather(icon_type) => match icon_type {
80            Feather::Normal => gen::feather::NORMAL.get(name),
81        },
82        #[cfg(feature = "font-awesome")]
83        IconType::FontAwesome(icon_type) => match icon_type {
84            FontAwesome::Regular => gen::font_awesome::REGULAR.get(name),
85            FontAwesome::Solid => gen::font_awesome::SOLID.get(name),
86        },
87        #[cfg(feature = "heroicons")]
88        IconType::Heroicons(icon_type) => match icon_type {
89            Heroicons::Outline => gen::heroicons::OUTLINE.get(name),
90            Heroicons::Solid => gen::heroicons::SOLID.get(name),
91        },
92        #[cfg(feature = "ionicons")]
93        IconType::Ionicons(icon_type) => match icon_type {
94            Ionicons::Outline => gen::ionicons::OUTLINE.get(name),
95            Ionicons::Sharp => gen::ionicons::SHARP.get(name),
96            Ionicons::Normal => gen::ionicons::NORMAL.get(name),
97        },
98        #[cfg(feature = "octicons")]
99        IconType::Octicons(icon_type) => match icon_type {
100            Octicons::Normal => gen::octicons::NORMAL.get(name),
101        },
102    }
103}
104
105#[cfg(feature = "bootstrap")]
106#[inline(always)]
107pub fn bootstrap(name: &str, filled: bool, attrs: IconAttrs) -> Option<String> {
108    let svg = if filled {
109        gen::bootstrap::FILL.get(name)
110    } else {
111        gen::bootstrap::NORMAL.get(name)
112    };
113    attrs.add_to_svg(svg)
114}
115
116#[cfg(feature = "feather")]
117#[inline(always)]
118pub fn feather(name: &str, attrs: IconAttrs) -> Option<String> {
119    let svg = gen::feather::NORMAL.get(name);
120    attrs.add_to_svg(svg)
121}
122
123#[cfg(feature = "font-awesome")]
124#[inline(always)]
125pub fn font_awesome(name: &str, category: FontAwesome, attrs: IconAttrs) -> Option<String> {
126    let svg = match category {
127        FontAwesome::Regular => gen::font_awesome::REGULAR.get(name),
128        FontAwesome::Solid => gen::font_awesome::SOLID.get(name),
129    };
130    attrs.add_to_svg(svg)
131}
132
133#[cfg(feature = "heroicons")]
134#[inline(always)]
135pub fn heroicons(name: &str, outline: bool, attrs: IconAttrs) -> Option<String> {
136    let svg = if outline {
137        gen::heroicons::OUTLINE.get(name)
138    } else {
139        gen::heroicons::SOLID.get(name)
140    };
141    attrs.add_to_svg(svg)
142}
143
144#[cfg(feature = "ionicons")]
145#[inline(always)]
146pub fn ionicons(name: &str, category: Ionicons, attrs: IconAttrs) -> Option<String> {
147    let svg = match category {
148        Ionicons::Outline => gen::ionicons::OUTLINE.get(name),
149        Ionicons::Sharp => gen::ionicons::SHARP.get(name),
150        Ionicons::Normal => gen::ionicons::NORMAL.get(name),
151    };
152    attrs.add_to_svg(svg)
153}
154
155#[cfg(feature = "octicons")]
156#[inline(always)]
157pub fn octicons(name: &str, attrs: IconAttrs) -> Option<String> {
158    let svg = gen::octicons::NORMAL.get(name);
159    attrs.add_to_svg(svg)
160}
161
162pub(crate) fn decap(bytes: &[u8]) -> HashMap<String, HashMap<String, String>> {
163    let mut gz = GzDecoder::new(bytes);
164    let mut uncompressed = Vec::new();
165    gz.read_to_end(&mut uncompressed).expect("should decap");
166    let (ret, _) = bincode::decode_from_slice(&uncompressed, bincode::config::standard())
167        .expect("should deserialize");
168    ret
169}
170
171impl<'a> IconAttrs<'a> {
172    #[inline(always)]
173    pub fn class(self, class: &'a str) -> Self {
174        self.with("class", class)
175    }
176
177    #[inline(always)]
178    pub fn fill(self, fill: &'a str) -> Self {
179        self.with("fill", fill)
180    }
181
182    #[inline(always)]
183    pub fn stroke_color(self, stroke_color: &'a str) -> Self {
184        self.with("stroke", stroke_color)
185    }
186
187    #[inline(always)]
188    pub fn stroke_width(self, stroke_width: &'a str) -> Self {
189        self.with("stroke-width", stroke_width)
190    }
191
192    #[inline(always)]
193    pub fn with(self, attr: &'a str, value: &'a str) -> Self {
194        let mut data = self.data;
195        let mut pos = self.pos;
196        data[pos as usize] = (attr, value.into());
197        pos = (pos + 1) % MAX_ATTRS as u8;
198
199        Self { data, pos }
200    }
201    fn add_to_svg(&self, svg: Option<&String>) -> Option<String> {
202        if let Some(svg) = svg {
203            let mut svg = svg.to_owned();
204            let mut attrs = String::new();
205            for i in 0..self.pos {
206                let (k, v) = &self.data[i as usize];
207                attrs.push_str(&format!(" {k}=\"{v}\""));
208            }
209
210            if !attrs.is_empty() {
211                svg.insert_str(4, &attrs);
212            }
213            Some(svg)
214        } else {
215            None
216        }
217    }
218}
219
220#[cfg(feature = "json")]
221impl<'a> From<&'a serde_json::Value> for IconAttrs<'a> {
222    fn from(value: &'a serde_json::Value) -> Self {
223        let mut attrs = IconAttrs::default();
224        if let serde_json::Value::Object(map) = value {
225            for (k, v) in map {
226                let s = match v {
227                    serde_json::Value::String(s) => s.into(),
228                    serde_json::Value::Number(n) => n.to_string().into(),
229                    serde_json::Value::Bool(b) => b.to_string().into(),
230                    _ => continue,
231                };
232
233                attrs.data[attrs.pos as usize] = (k, s);
234                attrs.pos = (attrs.pos + 1) % MAX_ATTRS as u8;
235            }
236        }
237        attrs
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use minify_html::{minify, Cfg};
245    const CFG: Cfg = Cfg {
246        keep_closing_tags: true,
247        do_not_minify_doctype: false,
248        ensure_spec_compliant_unquoted_attribute_values: true,
249        keep_html_and_head_opening_tags: true,
250        keep_spaces_between_attributes: true,
251        keep_comments: false,
252        minify_css: true,
253        minify_js: true,
254        remove_bangs: false,
255        remove_processing_instructions: false,
256        keep_input_type_text_attr: true,
257        keep_ssi_comments: false,
258        preserve_brace_template_syntax: true,
259        preserve_chevron_percent_template_syntax: false,
260    };
261
262    #[cfg(feature = "bootstrap")]
263    #[test]
264    fn bootstrap_icon_fill_should_work() {
265        assert_eq!(
266            get(IconType::Bootstrap(Bootstrap::Fill), "alarm"),
267            Some(&expected(include_str!(
268                "../icon_resources/bootstrap/icons/alarm-fill.svg"
269            )))
270        );
271    }
272
273    #[cfg(feature = "bootstrap")]
274    #[test]
275    fn bootstrap_icon_should_work() {
276        assert_eq!(
277            get(IconType::Bootstrap(Bootstrap::Normal), "alarm"),
278            Some(&expected(include_str!(
279                "../icon_resources/bootstrap/icons/alarm.svg"
280            )))
281        );
282    }
283
284    #[cfg(feature = "feather")]
285    #[test]
286    fn feather_icon_should_work() {
287        assert_eq!(
288            get(IconType::Feather(Feather::Normal), "activity"),
289            Some(&expected(include_str!(
290                "../icon_resources/feather/icons/activity.svg"
291            )))
292        );
293    }
294
295    #[cfg(feature = "font-awesome")]
296    #[test]
297    fn font_awesome_icon_brands_should_work() {
298        assert_eq!(
299            get(IconType::FontAwesome(FontAwesome::Regular), "500px"),
300            Some(&expected(include_str!(
301                "../icon_resources/font-awesome/svgs/brands/500px.svg"
302            )))
303        );
304    }
305
306    #[cfg(feature = "font-awesome")]
307    #[test]
308    fn font_awesome_icon_regular_should_work() {
309        assert_eq!(
310            get(IconType::FontAwesome(FontAwesome::Regular), "address-book"),
311            Some(&expected(include_str!(
312                "../icon_resources/font-awesome/svgs/regular/address-book.svg"
313            )))
314        );
315    }
316
317    #[cfg(feature = "font-awesome")]
318    #[test]
319    fn font_awesome_icon_solid_should_work() {
320        assert_eq!(
321            get(IconType::FontAwesome(FontAwesome::Solid), "address-book"),
322            Some(&expected(include_str!(
323                "../icon_resources/font-awesome/svgs/solid/address-book.svg"
324            )))
325        );
326    }
327
328    #[cfg(feature = "heroicons")]
329    #[test]
330    fn heroicons_icon_outline_should_work() {
331        assert_eq!(
332            get(IconType::Heroicons(Heroicons::Outline), "academic-cap"),
333            Some(&expected(include_str!(
334                "../icon_resources/heroicons/optimized/24/outline/academic-cap.svg"
335            )))
336        );
337    }
338
339    #[cfg(feature = "heroicons")]
340    #[test]
341    fn heroicons_icon_solid_should_work() {
342        assert_eq!(
343            get(IconType::Heroicons(Heroicons::Solid), "academic-cap"),
344            Some(&expected(include_str!(
345                "../icon_resources/heroicons/optimized/24/solid/academic-cap.svg"
346            )))
347        );
348    }
349
350    #[cfg(feature = "ionicons")]
351    #[test]
352    fn ionicons_icon_outline_should_work() {
353        assert_eq!(
354            get(IconType::Ionicons(Ionicons::Outline), "alarm"),
355            Some(&expected(include_str!(
356                "../icon_resources/ionicons/src/svg/alarm-outline.svg"
357            )))
358        );
359    }
360
361    #[cfg(feature = "ionicons")]
362    #[test]
363    fn ionicons_icon_sharp_should_work() {
364        assert_eq!(
365            get(IconType::Ionicons(Ionicons::Sharp), "alarm"),
366            Some(&expected(include_str!(
367                "../icon_resources/ionicons/src/svg/alarm-sharp.svg"
368            )))
369        );
370    }
371
372    #[cfg(feature = "ionicons")]
373    #[test]
374    fn ionicons_icon_should_work() {
375        assert_eq!(
376            get(IconType::Ionicons(Ionicons::Normal), "alarm"),
377            Some(&expected(include_str!(
378                "../icon_resources/ionicons/src/svg/alarm.svg"
379            )))
380        );
381    }
382
383    #[cfg(feature = "octicons")]
384    #[test]
385    fn octicons_icon_should_work() {
386        assert_eq!(
387            get(IconType::Octicons(Octicons::Normal), "alert"),
388            Some(&expected(include_str!(
389                "../icon_resources/octicons/icons/alert-24.svg"
390            )))
391        );
392    }
393
394    #[cfg(feature = "bootstrap")]
395    #[test]
396    fn bootstrap_not_filled_should_work() {
397        assert_eq!(
398            bootstrap("alarm", false, IconAttrs::default()),
399            Some(expected(include_str!(
400                "../icon_resources/bootstrap/icons/alarm.svg"
401            )))
402        );
403    }
404
405    #[cfg(feature = "bootstrap")]
406    #[test]
407    fn bootstrap_filled_should_work() {
408        assert_eq!(
409            bootstrap("alarm", true, IconAttrs::default()),
410            Some(expected(include_str!(
411                "../icon_resources/bootstrap/icons/alarm-fill.svg"
412            )))
413        );
414    }
415
416    #[cfg(feature = "feather")]
417    #[test]
418    fn feather_should_work() {
419        assert_eq!(
420            feather("activity", IconAttrs::default()),
421            Some(expected(include_str!(
422                "../icon_resources/feather/icons/activity.svg"
423            )))
424        );
425    }
426
427    #[cfg(feature = "font-awesome")]
428    #[test]
429    fn font_awesome_brands_should_work() {
430        assert_eq!(
431            font_awesome("github", FontAwesome::Solid, IconAttrs::default()),
432            Some(expected(include_str!(
433                "../icon_resources/font-awesome/svgs/brands/github.svg"
434            )))
435        );
436    }
437
438    #[cfg(feature = "font-awesome")]
439    #[test]
440    fn font_awesome_regular_should_work() {
441        assert_eq!(
442            font_awesome("address-book", FontAwesome::Regular, IconAttrs::default()),
443            Some(expected(include_str!(
444                "../icon_resources/font-awesome/svgs/regular/address-book.svg"
445            )))
446        );
447    }
448
449    #[cfg(feature = "font-awesome")]
450    #[test]
451    fn font_awesome_solid_should_work() {
452        assert_eq!(
453            font_awesome("address-book", FontAwesome::Solid, IconAttrs::default()),
454            Some(expected(include_str!(
455                "../icon_resources/font-awesome/svgs/solid/address-book.svg"
456            )))
457        );
458    }
459
460    #[cfg(feature = "heroicons")]
461    #[test]
462    fn heroicons_outline_should_work() {
463        assert_eq!(
464            heroicons("academic-cap", true, IconAttrs::default()),
465            Some(expected(include_str!(
466                "../icon_resources/heroicons/optimized/24/outline/academic-cap.svg"
467            )))
468        );
469    }
470
471    #[cfg(feature = "heroicons")]
472    #[test]
473    fn heroicons_solid_should_work() {
474        assert_eq!(
475            heroicons("academic-cap", false, IconAttrs::default()),
476            Some(expected(include_str!(
477                "../icon_resources/heroicons/optimized/24/solid/academic-cap.svg"
478            )))
479        );
480    }
481
482    #[cfg(feature = "ionicons")]
483    #[test]
484    fn ionicons_outline_should_work() {
485        assert_eq!(
486            ionicons("alarm", Ionicons::Outline, IconAttrs::default()),
487            Some(expected(include_str!(
488                "../icon_resources/ionicons/src/svg/alarm-outline.svg"
489            )))
490        );
491    }
492
493    #[cfg(feature = "ionicons")]
494    #[test]
495    fn ionicons_sharp_should_work() {
496        assert_eq!(
497            ionicons("alarm", Ionicons::Sharp, IconAttrs::default()),
498            Some(expected(include_str!(
499                "../icon_resources/ionicons/src/svg/alarm-sharp.svg"
500            )))
501        );
502    }
503
504    #[cfg(feature = "ionicons")]
505    #[test]
506    fn ionicons_should_work() {
507        assert_eq!(
508            ionicons("logo-github", Ionicons::Normal, IconAttrs::default()),
509            Some(expected(include_str!(
510                "../icon_resources/ionicons/src/svg/logo-github.svg"
511            )))
512        );
513    }
514
515    #[cfg(feature = "octicons")]
516    #[test]
517    fn octicons_should_work() {
518        assert_eq!(
519            octicons("alert", IconAttrs::default()),
520            Some(expected(include_str!(
521                "../icon_resources/octicons/icons/alert-24.svg"
522            )))
523        );
524    }
525
526    #[cfg(feature = "bootstrap")]
527    #[test]
528    fn bootstrap_with_class_should_work() {
529        let attrs = IconAttrs::default()
530            .class("h-8 w-8 text-white")
531            .fill("none")
532            .stroke_color("currentColor");
533
534        let icon = bootstrap("alarm", false, attrs).expect("exists");
535        assert_eq!(&icon[..32], "<svg class=\"h-8 w-8 text-white\" ");
536    }
537
538    #[cfg(all(feature = "heroicons", feature = "json"))]
539    #[test]
540    fn json_attribute_should_work() {
541        let attrs = &serde_json::json!({
542            "class": "h-8 w-8 text-white",
543            "fill": "none",
544            "stroke_color": "currentColor",
545        });
546
547        let icon = heroicons("academic-cap", true, attrs.into()).expect("exists");
548        assert_eq!(&icon[..32], "<svg class=\"h-8 w-8 text-white\" ");
549    }
550
551    #[test]
552    fn icon_should_not_exist() {
553        assert_eq!(get(IconType::Feather(Feather::Normal), "not_exist"), None);
554    }
555
556    fn expected(s: &str) -> String {
557        String::from_utf8(minify(s.as_bytes(), &CFG)).unwrap()
558    }
559}