|
| 1 | +//! Support for admonitions using markdown blockquotes. |
| 2 | +//! |
| 3 | +//! To add support for a new admonition: |
| 4 | +//! |
| 5 | +//! 1. Modify the [`admonitions`] function below to include an icon. |
| 6 | +//! 2. Modify `theme/reference.css` to set the color for the different themes. |
| 7 | +//! Look at one of the other admonitions as a guide. |
| 8 | +//! 3. Update `src/introduction.md` and describe what this new block is for |
| 9 | +//! with an example. |
| 10 | +//! 4. Update `docs/authoring.md` to show an example of your new admonition. |
| 11 | +
|
| 12 | +use crate::{Diagnostics, warn_or_err}; |
| 13 | +use mdbook::book::Chapter; |
| 14 | +use regex::{Captures, Regex}; |
| 15 | +use std::sync::LazyLock; |
| 16 | + |
| 17 | +/// The Regex for the syntax for blockquotes that have a specific CSS class, |
| 18 | +/// like `> [!WARNING]`. |
| 19 | +static ADMONITION_RE: LazyLock<Regex> = LazyLock::new(|| { |
| 20 | + Regex::new(r"(?m)^ *> \[!(?<admon>[^]]+)\]\n(?<blockquote>(?: *>.*\n)+)").unwrap() |
| 21 | +}); |
| 22 | + |
| 23 | +// This icon is from GitHub, MIT License, see https://github.com/primer/octicons |
| 24 | +const ICON_NOTE: &str = r#"<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path>"#; |
| 25 | + |
| 26 | +// This icon is from GitHub, MIT License, see https://github.com/primer/octicons |
| 27 | +const ICON_WARNING: &str = r#"<path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path>"#; |
| 28 | + |
| 29 | +// This icon is from GitHub, MIT License, see https://github.com/primer/octicons |
| 30 | +const ICON_EXAMPLE: &str = r#"<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm4.879-2.773 4.264 2.559a.25.25 0 0 1 0 .428l-4.264 2.559A.25.25 0 0 1 6 10.559V5.442a.25.25 0 0 1 .379-.215Z"></path>"#; |
| 31 | + |
| 32 | +/// Converts blockquotes with special headers into admonitions. |
| 33 | +/// |
| 34 | +/// The blockquote should look something like: |
| 35 | +/// |
| 36 | +/// ```markdown |
| 37 | +/// > [!WARNING] |
| 38 | +/// > ... |
| 39 | +/// ``` |
| 40 | +/// |
| 41 | +/// This will add a `<div class="alert alert-warning">` around the |
| 42 | +/// blockquote so that it can be styled differently, and injects an icon. |
| 43 | +/// The actual styling needs to be added in the `reference.css` CSS file. |
| 44 | +pub fn admonitions(chapter: &Chapter, diag: &mut Diagnostics) -> String { |
| 45 | + ADMONITION_RE |
| 46 | + .replace_all(&chapter.content, |caps: &Captures<'_>| { |
| 47 | + let lower = caps["admon"].to_lowercase(); |
| 48 | + let term = to_initial_case(&caps["admon"]); |
| 49 | + let blockquote = &caps["blockquote"]; |
| 50 | + let initial_spaces = blockquote.chars().position(|ch| ch != ' ').unwrap_or(0); |
| 51 | + let space = &blockquote[..initial_spaces]; |
| 52 | + |
| 53 | + let format_div = |class, content| { |
| 54 | + format!( |
| 55 | + "{space}<div class=\"alert alert-{class}\">\n\ |
| 56 | + \n\ |
| 57 | + {space}> <p class=\"alert-title\">\ |
| 58 | + {content}</p>\n\ |
| 59 | + {space} >\n\ |
| 60 | + {blockquote}\n\ |
| 61 | + \n\ |
| 62 | + {space}</div>\n", |
| 63 | + ) |
| 64 | + }; |
| 65 | + |
| 66 | + if lower.starts_with("edition-") { |
| 67 | + let edition = &lower[8..]; |
| 68 | + return format_div( |
| 69 | + "edition", |
| 70 | + format!( |
| 71 | + "<span class=\"alert-title-edition\">{edition}</span> Edition differences" |
| 72 | + ), |
| 73 | + ); |
| 74 | + } |
| 75 | + |
| 76 | + let svg = match lower.as_str() { |
| 77 | + "note" => ICON_NOTE, |
| 78 | + "warning" => ICON_WARNING, |
| 79 | + "example" => ICON_EXAMPLE, |
| 80 | + _ => { |
| 81 | + warn_or_err!( |
| 82 | + diag, |
| 83 | + "admonition `{lower}` in {:?} is incorrect or not yet supported", |
| 84 | + chapter.path.as_ref().unwrap() |
| 85 | + ); |
| 86 | + "" |
| 87 | + } |
| 88 | + }; |
| 89 | + format_div( |
| 90 | + &lower, |
| 91 | + format!( |
| 92 | + "<svg viewBox=\"0 0 16 16\" width=\"18\" height=\"18\">\ |
| 93 | + {svg}\ |
| 94 | + </svg>{term}" |
| 95 | + ), |
| 96 | + ) |
| 97 | + }) |
| 98 | + .to_string() |
| 99 | +} |
| 100 | + |
| 101 | +fn to_initial_case(s: &str) -> String { |
| 102 | + let mut chars = s.chars(); |
| 103 | + let first = chars.next().expect("not empty").to_uppercase(); |
| 104 | + let rest = chars.as_str().to_lowercase(); |
| 105 | + format!("{first}{rest}") |
| 106 | +} |
0 commit comments