mdbook_admonish/
custom.rs1use anyhow::{anyhow, Context, Result};
6use hex_color::{Case, HexColor};
7use once_cell::sync::Lazy;
8use regex::Regex;
9use std::fs;
10use std::path::Path;
11
12static RX_COLLAPSE_NEWLINES: Lazy<Regex> =
13 Lazy::new(|| Regex::new(r"[\r\n]+\s*").expect("invalid whitespace regex"));
14
15fn svg_to_data_url(svg: &str) -> String {
18 const XMLNS: &str = r#"http://www.w3.org/2000/svg"#;
19 let mut svg = RX_COLLAPSE_NEWLINES.replace_all(svg, "").to_string();
21 if !svg.contains(XMLNS) {
22 log::warn!("Your SVG file does not contain '<svg xmlns=\"{XMLNS}\"', it will likely fail to render.");
23 }
24
25 svg = svg
26 .replace('"', "'")
27 .replace('%', "%25")
28 .replace('#', "%23")
29 .replace('{', "%7B")
30 .replace('}', "%7D");
31 format!("url(\"data:image/svg+xml;charset=utf-8,{}\")", svg)
32}
33
34fn directive_css(name: &str, svg_data: &str, tint: HexColor) -> String {
38 let data_url = svg_to_data_url(svg_data);
39 let tint_faint = format!("rgba({}, {}, {}, {})", tint.r, tint.g, tint.b, 0.1);
40 let tint = tint.display_rgb().with_case(Case::Lower);
41 format!(
42 ":root {{
43 --md-admonition-icon--admonish-{name}: {data_url};
44}}
45
46:is(.admonition):is(.admonish-{name}) {{
47 border-color: {tint};
48}}
49
50:is(.admonish-{name}) > :is(.admonition-title, summary.admonition-title) {{
51 background-color: {tint_faint};
52}}
53:is(.admonish-{name}) > :is(.admonition-title, summary.admonition-title)::before {{
54 background-color: {tint};
55 mask-image: var(--md-admonition-icon--admonish-{name});
56 -webkit-mask-image: var(--md-admonition-icon--admonish-{name});
57 mask-repeat: no-repeat;
58 -webkit-mask-repeat: no-repeat;
59 mask-size: contain;
60 -webkit-mask-repeat: no-repeat;
61}}
62",
63 name = name,
64 data_url = data_url,
65 tint = tint,
66 tint_faint = tint_faint
67 )
68}
69
70#[doc(hidden)]
71pub fn css_from_config(book_dir: &Path, config: &str) -> Result<String> {
72 let config = crate::book_config::admonish_config_from_str(config)?;
73 let custom_directives = config.directive.custom;
74
75 if custom_directives.is_empty() {
76 return Err(anyhow!("No custom directives provided"));
77 }
78
79 log::info!("Loaded {} custom directives", custom_directives.len());
80 let mut css = String::new();
81 for (directive_name, directive) in custom_directives.iter() {
82 let svg = fs::read_to_string(book_dir.join(&directive.icon))
83 .with_context(|| format!("can't read icon file '{}'", directive.icon.display()))?;
84 css.push_str(&directive_css(directive_name, &svg, directive.color));
85 }
86 Ok(css)
87}
88
89#[cfg(test)]
90mod test {
91 use super::*;
92 use pretty_assertions::assert_eq;
93
94 const GENERATED_CSS: &str = include_str!("./test_data/mdbook-admonish-custom-expected.css");
95 const NOTE_SVG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox='0 0 24 24'>
96 <path d='M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z'/>
97</svg>
98"#;
99
100 #[test]
104 fn verify_against_generated_css() {
105 let actual = directive_css("note", NOTE_SVG, HexColor::parse("#448aff").unwrap());
106 assert_eq!(
107 GENERATED_CSS, actual,
108 "Rust generated CSS is out of step with SCSS generated CSS"
109 )
110 }
111}