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
70pub 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}