blob: 8aaf6cf666e45e3d25fc550d35069901072e99f3 [file] [log] [blame]
//! Support for admonitions using markdown blockquotes.
//!
//! To add support for a new admonition:
//!
//! 1. Modify the [`admonitions`] function below to include an icon.
//! 2. Modify `theme/reference.css` to set the color for the different themes.
//! Look at one of the other admonitions as a guide.
//! 3. Update `src/introduction.md` and describe what this new block is for
//! with an example.
//! 4. Update `docs/authoring.md` to show an example of your new admonition.
use crate::{Diagnostics, warn_or_err};
use mdbook::book::Chapter;
use regex::{Captures, Regex};
use std::sync::LazyLock;
/// The Regex for the syntax for blockquotes that have a specific CSS class,
/// like `> [!WARNING]`.
static ADMONITION_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?m)^ *> \[!(?<admon>[^]]+)\]\n(?<blockquote>(?: *>.*\n)+)").unwrap()
});
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
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>"#;
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
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>"#;
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
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>"#;
/// Converts blockquotes with special headers into admonitions.
///
/// The blockquote should look something like:
///
/// ```markdown
/// > [!WARNING]
/// > ...
/// ```
///
/// This will add a `<div class="alert alert-warning">` around the
/// blockquote so that it can be styled differently, and injects an icon.
/// The actual styling needs to be added in the `reference.css` CSS file.
pub fn admonitions(chapter: &Chapter, diag: &mut Diagnostics) -> String {
ADMONITION_RE
.replace_all(&chapter.content, |caps: &Captures<'_>| {
let lower = caps["admon"].to_lowercase();
let term = to_initial_case(&caps["admon"]);
let blockquote = &caps["blockquote"];
let initial_spaces = blockquote.chars().position(|ch| ch != ' ').unwrap_or(0);
let space = &blockquote[..initial_spaces];
let format_div = |class, content| {
format!(
"{space}<div class=\"alert alert-{class}\">\n\
\n\
{space}> <p class=\"alert-title\">\
{content}</p>\n\
{space} >\n\
{blockquote}\n\
\n\
{space}</div>\n",
)
};
if lower.starts_with("edition-") {
let edition = &lower[8..];
return format_div(
"edition",
format!(
"<span class=\"alert-title-edition\">{edition}</span> Edition differences"
),
);
}
let svg = match lower.as_str() {
"note" => ICON_NOTE,
"warning" => ICON_WARNING,
"example" => ICON_EXAMPLE,
_ => {
warn_or_err!(
diag,
"admonition `{lower}` in {:?} is incorrect or not yet supported",
chapter.path.as_ref().unwrap()
);
""
}
};
format_div(
&lower,
format!(
"<svg viewBox=\"0 0 16 16\" width=\"18\" height=\"18\">\
{svg}\
</svg>{term}"
),
)
})
.to_string()
}
fn to_initial_case(s: &str) -> String {
let mut chars = s.chars();
let first = chars.next().expect("not empty").to_uppercase();
let rest = chars.as_str().to_lowercase();
format!("{first}{rest}")
}