tetratto_core/
html.rs

1use std::{
2    collections::HashMap,
3    fs::{exists, read_to_string, write},
4    sync::LazyLock,
5};
6use tokio::sync::RwLock;
7
8use pathbufd::PathBufD;
9
10/// A container for all loaded icons.
11pub static ICONS: LazyLock<RwLock<HashMap<String, String>>> =
12    LazyLock::new(|| RwLock::new(HashMap::new()));
13
14/// Pull an icon given its name and insert it into [`ICONS`].
15pub async fn pull_icon(icon: &str, icons_dir: &str) {
16    let writer = &mut ICONS.write().await;
17
18    let icon_url = format!(
19        "https://raw.githubusercontent.com/lucide-icons/lucide/refs/heads/main/icons/{icon}.svg"
20    );
21
22    let file_path = PathBufD::current().extend(&[icons_dir, &format!("{icon}.svg")]);
23
24    if exists(&file_path).unwrap_or(false) {
25        writer.insert(icon.to_string(), read_to_string(&file_path).unwrap());
26        return;
27    }
28
29    println!("download icon: {icon}");
30    let svg = reqwest::get(icon_url)
31        .await
32        .unwrap()
33        .text()
34        .await
35        .unwrap()
36        .replace("\n", "");
37
38    write(&file_path, &svg).unwrap();
39    writer.insert(icon.to_string(), svg);
40}
41
42/// Read a string and pull all icons found within it.
43pub async fn pull_icons(mut input: String, icon_dir: &str) -> String {
44    // icon (with class)
45    let icon_with_class =
46        regex::Regex::new("(\\{\\{)\\s*(icon)\\s*(.*?)\\s*c\\((.*?)\\)\\s*(\\}\\})").unwrap();
47
48    for cap in icon_with_class.captures_iter(&input.clone()) {
49        let cap_str = &cap.get(3).unwrap().as_str().replace("\"", "");
50        let icon = &(if cap_str.contains(" }}") {
51            cap_str.split(" }}").next().unwrap().to_string()
52        } else {
53            cap_str.to_string()
54        });
55
56        pull_icon(icon, icon_dir).await;
57
58        let reader = ICONS.read().await;
59        let icon_text = reader.get(icon).unwrap().replace(
60            "<svg",
61            &format!("<svg class=\"icon {}\"", cap.get(4).unwrap().as_str()),
62        );
63
64        input = input.replace(
65            &format!(
66                "{{{{ icon \"{cap_str}\" c({}) }}}}",
67                cap.get(4).unwrap().as_str()
68            ),
69            &icon_text,
70        );
71    }
72
73    // icon (without class)
74    let icon_without_class = regex::Regex::new("(\\{\\{)\\s*(icon)\\s*(.*?)\\s*(\\}\\})").unwrap();
75
76    for cap in icon_without_class.captures_iter(&input.clone()) {
77        let cap_str = &cap.get(3).unwrap().as_str().replace("\"", "");
78        let icon = &(if cap_str.contains(" }}") {
79            cap_str.split(" }}").next().unwrap().to_string()
80        } else {
81            cap_str.to_string()
82        });
83
84        pull_icon(icon, icon_dir).await;
85
86        let reader = ICONS.read().await;
87        let icon_text = reader
88            .get(icon)
89            .unwrap()
90            .replace("<svg", "<svg class=\"icon\"");
91
92        input = input.replace(&format!("{{{{ icon \"{cap_str}\" }}}}",), &icon_text);
93    }
94
95    // ...
96    input
97}