mdbook_admonish/
custom.rs

1//! This module is responsible for generating custom CSS for new admonition variants.
2//!
3//! It has unit tests to ensure the output matches that of the compile_assets CSS.
4
5use 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
15// Do some simple things to make the svg input probably a valid data url
16// Based on this gist: https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0
17fn svg_to_data_url(svg: &str) -> String {
18    const XMLNS: &str = r#"http://www.w3.org/2000/svg"#;
19    //
20    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
34/// Given a valid set of inputs, generate the relevant CSS.
35///
36/// It is up to the caller to validate inputs.
37fn 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    // Verify the generated CSS here against a sample from the compile_assets output.
101    //
102    // The ensures that any new custom CSS will be in line with official styles.
103    #[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}