Skip to main content

mdbook_embedify/
utils.rs

1use mdbook::Config;
2use minify::html::minify;
3use pulldown_cmark;
4use regex::Regex;
5
6pub fn remove_script_and_style_comments(content: String) -> String {
7    let script_tag_re = Regex::new(r"(?s)<script.*?>.*?</script>").unwrap();
8    let style_tag_re = Regex::new(r"(?s)<style.*?>.*?</style>").unwrap();
9
10    let block_comment_re = Regex::new(r"(?s)/\*.*?\*/").unwrap();
11    let line_comment_re = Regex::new(r"(?m)//.*?$").unwrap(); // only for <script>
12
13    // Remove comments in <script>
14    let content = script_tag_re
15        .replace_all(&content, |caps: &regex::Captures| {
16            let script_block = &caps[0];
17            if let Some(body_start) = script_block.find('>') {
18                if let Some(body_end) = script_block.rfind("</script>") {
19                    let head = &script_block[..=body_start];
20                    let body = &script_block[body_start + 1..body_end];
21                    let tail = &script_block[body_end..];
22
23                    let body = block_comment_re.replace_all(body, "");
24                    let body = line_comment_re.replace_all(&body, "");
25
26                    format!("{head}{}{tail}", body.trim())
27                } else {
28                    script_block.to_string()
29                }
30            } else {
31                script_block.to_string()
32            }
33        })
34        .to_string();
35
36    // Remove comments in <style>
37    let content = style_tag_re
38        .replace_all(&content, |caps: &regex::Captures| {
39            let style_block = &caps[0];
40            if let Some(body_start) = style_block.find('>') {
41                if let Some(body_end) = style_block.rfind("</style>") {
42                    let head = &style_block[..=body_start];
43                    let body = &style_block[body_start + 1..body_end];
44                    let tail = &style_block[body_end..];
45
46                    let body = block_comment_re.replace_all(body, "");
47
48                    format!("{head}{}{tail}", body.trim())
49                } else {
50                    style_block.to_string()
51                }
52            } else {
53                style_block.to_string()
54            }
55        })
56        .to_string();
57
58    content
59}
60
61pub fn minify_html(content: String) -> String {
62    let result = remove_script_and_style_comments(content);
63    minify(&result)
64}
65
66pub fn render_to_markdown(content: String) -> String {
67    let mut html = String::new();
68    let parser = pulldown_cmark::Parser::new(&content);
69    pulldown_cmark::html::push_html(&mut html, parser);
70    minify_html(html.trim().into())
71}
72
73pub fn get_config_bool(config: &Config, key: &str) -> bool {
74    config
75        .get(format!("preprocessor.embedify.{}", key).as_str())
76        .and_then(|v| v.as_bool())
77        .unwrap_or(false)
78}
79
80pub fn get_config_string<'a>(config: &'a Config, key: &str, default: &'a str) -> &'a str {
81    config
82        .get(format!("preprocessor.embedify.{}", key).as_str())
83        .and_then(|v| v.as_str())
84        .unwrap_or(default)
85}
86
87pub fn create_embed(name: &str, options: Vec<(&str, &str)>) -> String {
88    let mut option_str = String::new();
89    for (key, value) in options {
90        option_str.push_str(&format!(r#"{}="{}""#, key, value));
91    }
92    format!("{{% embed {} {} %}}", name, option_str)
93}
94
95pub fn create_announcement_banner(config: &Config) -> String {
96    // get the config
97    let id = get_config_string(config, "announcement-banner.id", "");
98    let theme = get_config_string(config, "announcement-banner.theme", "default");
99    let message = get_config_string(config, "announcement-banner.message", "");
100
101    create_embed(
102        "announcement-banner",
103        vec![("id", id), ("theme", theme), ("message", message)],
104    )
105}
106
107pub fn create_giscus(config: &Config) -> String {
108    // get the config
109    let repo = get_config_string(config, "giscus.repo", "");
110    let repo_id = get_config_string(config, "giscus.repo-id", "");
111    let category = get_config_string(config, "giscus.category", "");
112    let category_id = get_config_string(config, "giscus.category-id", "");
113    let reactions_enabled = get_config_string(config, "giscus.reactions-enabled", "1");
114    let theme = get_config_string(config, "giscus.theme", "book");
115    let lang = get_config_string(config, "giscus.lang", "en");
116    let loading = get_config_string(config, "giscus.loading", "lazy");
117
118    let options = vec![
119        ("repo", repo),
120        ("repo-id", repo_id),
121        ("category", category),
122        ("category-id", category_id),
123        ("reactions-enabled", reactions_enabled),
124        ("theme", theme),
125        ("lang", lang),
126        ("loading", loading),
127    ];
128
129    create_embed("giscus", options)
130}
131
132pub fn create_footer(config: &Config) -> String {
133    // get the config
134    let message = get_config_string(config, "footer.message", "");
135    create_embed("footer", vec![("message", message)])
136}
137
138pub fn create_scroll_to_top() -> String {
139    create_embed("scroll-to-top", vec![])
140}