Add support for admonitions

This enables the admonitions support from pulldown-cmark. This includes
a config option in case it causes problems with existing books.

I would like to make this extensible in the future, though I'm not sure
what that would look like. There's also some concerns with how this will
affect translations like mdbook-i18n-helpers, which we may need to work
out in a different way.

Closes https://github.com/rust-lang/mdBook/issues/2771
This commit is contained in:
Eric Huss
2025-09-18 19:53:45 -07:00
parent 604d4dd78a
commit 873e4fe40f
16 changed files with 314 additions and 36 deletions

View File

@@ -436,6 +436,8 @@ pub struct HtmlConfig {
pub smart_punctuation: bool,
/// Support for definition lists.
pub definition_lists: bool,
/// Support for admonitions.
pub admonitions: bool,
/// Should mathjax be enabled?
pub mathjax_support: bool,
/// Additional CSS stylesheets to include in the rendered page's `<head>`.
@@ -504,6 +506,7 @@ impl Default for HtmlConfig {
preferred_dark_theme: None,
smart_punctuation: true,
definition_lists: true,
admonitions: true,
mathjax_support: false,
additional_css: Vec::new(),
additional_js: Vec::new(),

View File

@@ -156,6 +156,8 @@ blockquote {
border-block-end: .1em solid var(--quote-border);
}
/* TODO: Remove .warning in a future version of mdbook, it is replaced by
blockquote tags. */
.warning {
margin: 20px;
padding: 0 20px;
@@ -324,3 +326,83 @@ dd > p {
space before the definition. */
margin-top: 0;
}
/* Remove some excess space from the bottom. */
.blockquote-tag p:last-child {
margin-bottom: 2px;
}
.blockquote-tag {
/* Add some padding to make the vertical bar a little taller than the text.*/
padding: 2px 0px 2px 20px;
/* Add a solid color bar on the left side. */
border-inline-start-style: solid;
border-inline-start-width: 4px;
/* Disable the background color from normal blockquotes . */
background-color: inherit;
/* Disable border blocks from blockquotes. */
border-block-start: none;
border-block-end: none;
}
.blockquote-tag-title svg {
fill: currentColor;
/* Add space between the icon and the title. */
margin-right: 8px;
}
.blockquote-tag-note {
border-inline-start-color: var(--blockquote-note-color);
}
.blockquote-tag-tip {
border-inline-start-color: var(--blockquote-tip-color);
}
.blockquote-tag-important {
border-inline-start-color: var(--blockquote-important-color);
}
.blockquote-tag-warning {
border-inline-start-color: var(--blockquote-warning-color);
}
.blockquote-tag-caution {
border-inline-start-color: var(--blockquote-caution-color);
}
.blockquote-tag-note .blockquote-tag-title {
color: var(--blockquote-note-color);
}
.blockquote-tag-tip .blockquote-tag-title {
color: var(--blockquote-tip-color);
}
.blockquote-tag-important .blockquote-tag-title {
color: var(--blockquote-important-color);
}
.blockquote-tag-warning .blockquote-tag-title {
color: var(--blockquote-warning-color);
}
.blockquote-tag-caution .blockquote-tag-title {
color: var(--blockquote-caution-color);
}
.blockquote-tag-title {
/* Slightly increase the weight for more emphasis. */
font-weight: 600;
/* Vertically center the icon with the text. */
display: flex;
align-items: center;
/* Remove default large margins for a more compact display. */
margin: 2px 0 8px 0;
}
.blockquote-tag-title .fa-svg {
fill: currentColor;
/* Add some space between the icon and the text. */
margin-right: 8px;
}

View File

@@ -67,6 +67,12 @@
--footnote-highlight: #2668a6;
--overlay-bg: rgba(33, 40, 48, 0.4);
--blockquote-note-color: #74b9ff;
--blockquote-tip-color: #09ca09;
--blockquote-important-color: #d3abff;
--blockquote-warning-color: #f0b72f;
--blockquote-caution-color: #f21424;
}
.coal {
@@ -120,6 +126,12 @@
--footnote-highlight: #4079ae;
--overlay-bg: rgba(33, 40, 48, 0.4);
--blockquote-note-color: #4493f8;
--blockquote-tip-color: #08ae08;
--blockquote-important-color: #ab7df8;
--blockquote-warning-color: #d29922;
--blockquote-caution-color: #d91b29;
}
.light, html:not(.js) {
@@ -173,6 +185,12 @@
--footnote-highlight: #7e7eff;
--overlay-bg: rgba(200, 200, 205, 0.4);
--blockquote-note-color: #0969da;
--blockquote-tip-color: #008000;
--blockquote-important-color: #8250df;
--blockquote-warning-color: #9a6700;
--blockquote-caution-color: #b52731;
}
.navy {
@@ -226,6 +244,12 @@
--footnote-highlight: #4079ae;
--overlay-bg: rgba(33, 40, 48, 0.4);
--blockquote-note-color: #4493f8;
--blockquote-tip-color: #09ca09;
--blockquote-important-color: #ab7df8;
--blockquote-warning-color: #d29922;
--blockquote-caution-color: #f21424;
}
.rust {
@@ -277,6 +301,12 @@
--footnote-highlight: #d3a17a;
--overlay-bg: rgba(150, 150, 150, 0.25);
--blockquote-note-color: #023b95;
--blockquote-tip-color: #007700;
--blockquote-important-color: #8250df;
--blockquote-warning-color: #603700;
--blockquote-caution-color: #aa1721;
}
@media (prefers-color-scheme: dark) {
@@ -331,5 +361,11 @@
--footnote-highlight: #4079ae;
--overlay-bg: rgba(33, 40, 48, 0.4);
--blockquote-note-color: #4493f8;
--blockquote-tip-color: #08ae08;
--blockquote-important-color: #ab7df8;
--blockquote-warning-color: #d29922;
--blockquote-caution-color: #d91b29;
}
}

View File

@@ -0,0 +1,26 @@
use pulldown_cmark::BlockQuoteKind;
// 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_TIP: &str = r#"<path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path>"#;
// This icon is from GitHub, MIT License, see https://github.com/primer/octicons
const ICON_IMPORTANT: &str = r#"<path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 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_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_CAUTION: &str = r#"<path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path>"#;
pub(crate) fn select_tag(kind: BlockQuoteKind) -> (&'static str, &'static str, &'static str) {
match kind {
BlockQuoteKind::Note => ("note", ICON_NOTE, "Note"),
BlockQuoteKind::Tip => ("tip", ICON_TIP, "Tip"),
BlockQuoteKind::Important => ("important", ICON_IMPORTANT, "Important"),
BlockQuoteKind::Warning => ("warning", ICON_WARNING, "Warning"),
BlockQuoteKind::Caution => ("caution", ICON_CAUTION, "Caution"),
}
}

View File

@@ -16,6 +16,7 @@ use mdbook_core::config::{HtmlConfig, RustEdition};
use mdbook_markdown::{MarkdownOptions, new_cmark_parser};
use std::path::{Path, PathBuf};
mod admonitions;
mod hide_lines;
mod print;
mod serialize;
@@ -51,6 +52,7 @@ impl<'a> HtmlRenderOptions<'a> {
let mut markdown_options = MarkdownOptions::default();
markdown_options.smart_punctuation = config.smart_punctuation;
markdown_options.definition_lists = config.definition_lists;
markdown_options.admonitions = config.admonitions;
HtmlRenderOptions {
markdown_options,
path,

View File

@@ -15,9 +15,7 @@ use html5ever::{LocalName, QualName};
use indexmap::IndexMap;
use mdbook_core::config::RustEdition;
use mdbook_core::static_regex;
use pulldown_cmark::{
Alignment, BlockQuoteKind, CodeBlockKind, CowStr, Event, LinkType, Tag, TagEnd,
};
use pulldown_cmark::{Alignment, CodeBlockKind, CowStr, Event, LinkType, Tag, TagEnd};
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::ops::Deref;
@@ -407,14 +405,27 @@ where
Tag::BlockQuote(kind) => {
let mut b = Element::new("blockquote");
if let Some(kind) = kind {
let class = match kind {
BlockQuoteKind::Note => "markdown-alert-note",
BlockQuoteKind::Tip => "markdown-alert-tip",
BlockQuoteKind::Important => "markdown-alert-important",
BlockQuoteKind::Warning => "markdown-alert-warning",
BlockQuoteKind::Caution => "markdown-alert-caution",
};
let (class_kind, icon, text) = super::admonitions::select_tag(kind);
let class = format!("blockquote-tag blockquote-tag-{class_kind}");
b.insert_attr("class", class.into());
self.push(Node::Element(b));
let mut title = Element::new("p");
title.insert_attr("class", "blockquote-tag-title".into());
self.push(Node::Element(title));
let mut svg = Element::new("svg");
svg.insert_attr("viewbox", "0 0 16 16".into());
svg.insert_attr("width", "18".into());
svg.insert_attr("height", "18".into());
self.push(Node::Element(svg));
self.append_html(icon);
self.pop();
self.append(Node::Text(text.into()));
self.pop();
return;
}
b
}

View File

@@ -28,6 +28,10 @@ pub struct MarkdownOptions {
///
/// This is `true` by default.
pub definition_lists: bool,
/// Enables admonitions.
///
/// This is `true` by default.
pub admonitions: bool,
}
impl Default for MarkdownOptions {
@@ -35,6 +39,7 @@ impl Default for MarkdownOptions {
MarkdownOptions {
smart_punctuation: true,
definition_lists: true,
admonitions: true,
}
}
}
@@ -53,5 +58,8 @@ pub fn new_cmark_parser<'text>(text: &'text str, options: &MarkdownOptions) -> P
if options.definition_lists {
opts.insert(Options::ENABLE_DEFINITION_LIST);
}
if options.admonitions {
opts.insert(Options::ENABLE_GFM);
}
Parser::new_ext(text, opts)
}

View File

@@ -99,6 +99,7 @@ default-theme = "light"
preferred-dark-theme = "navy"
smart-punctuation = true
definition-lists = true
admonitions = true
mathjax-support = false
additional-css = ["custom.css", "custom2.css"]
additional-js = ["custom.js"]
@@ -127,6 +128,7 @@ The following configuration options are available:
See [Smart Punctuation](../markdown.md#smart-punctuation).
Defaults to `true`.
- **definition-lists:** Enables [definition lists](../markdown.md#definition-lists). Defaults to `true`.
- **admonitions:** Enables [admonitions](../markdown.md#admonitions). Defaults to `true`.
- **mathjax-support:** Adds support for [MathJax](../mathjax.md). Defaults to
`false`.
- **additional-css:** If you need to slightly change the appearance of your book

View File

@@ -270,3 +270,46 @@ term B
Terms are clickable just like headers, which will set the browser's URL to point directly to that term.
See the [definition lists spec](https://github.com/pulldown-cmark/pulldown-cmark/blob/HEAD/pulldown-cmark/specs/definition_lists.txt) for more information on the specifics of the syntax. See the [Wikipedia guidelines for glossaries](https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Glossaries#General_guidelines_for_writing_glossaries) for some guidelines on how to write a glossary.
### Admonitions
An admonition is a special type of callout or notice block used to highlight important information. It is written as a blockquote with a special tag on the first line.
```md
> [!NOTE]
> General information or additional context.
> [!TIP]
> A helpful suggestion or best practice.
> [!IMPORTANT]
> Key information that shouldn't be missed.
> [!WARNING]
> Critical information that highlights a potential risk.
> [!CAUTION]
> Information about potential issues that require caution.
```
These will render as:
> [!NOTE]
> General information or additional context.
> [!TIP]
> A helpful suggestion or best practice.
> [!IMPORTANT]
> Key information that shouldn't be missed.
> [!WARNING]
> Critical information that highlights a potential risk.
> [!CAUTION]
> Information about potential issues that require caution.
This feature is enabled by default.
To disable it, see the [`output.html.admonitions`] config option.
[`output.html.admonitions`]: configuration/renderers.md#html-renderer-options

View File

@@ -338,32 +338,6 @@ HTML tags with class `hidden` will not be shown.
<div class="hidden">This will not be seen.</div>
### `class="warning"`
To make a warning or similar note stand out, wrap it in a warning div.
```html
<div class="warning">
This is a bad thing that you should pay attention to.
Warning blocks should be used sparingly in documentation, to avoid "warning
fatigue," where people are trained to ignore them because they usually don't
matter for what they're doing.
</div>
```
<div class="warning">
This is a bad thing that you should pay attention to.
Warning blocks should be used sparingly in documentation, to avoid "warning
fatigue," where people are trained to ignore them because they usually don't
matter for what they're doing.
</div>
## Font-Awesome icons
mdBook includes a copy of [Font Awesome Free's](https://fontawesome.com)

View File

@@ -159,3 +159,16 @@ fn definition_lists() {
file!["markdown/definition_lists/expected_disabled/definition_lists.html"],
);
}
#[test]
fn admonitions() {
BookTest::from_dir("markdown/admonitions")
.check_all_main_files()
.run("build", |cmd| {
cmd.env("MDBOOK_OUTPUT__HTML__ADMONITIONS", "false");
})
.check_main_file(
"book/admonitions.html",
file!["markdown/admonitions/expected_disabled/admonitions.html"],
);
}

View File

@@ -0,0 +1,2 @@
[book]
title = "admonitions"

View File

@@ -0,0 +1,26 @@
<h1 id="admonitions"><a class="header" href="#admonitions">Admonitions</a></h1>
<blockquote class="blockquote-tag blockquote-tag-note">
<p class="blockquote-tag-title"><svg viewbox="0 0 16 16" width="18" height="18"><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></svg>Note</p>
<p>This is a note.</p>
<p>There are multiple paragraphs.</p>
</blockquote>
<blockquote class="blockquote-tag blockquote-tag-tip">
<p class="blockquote-tag-title"><svg viewbox="0 0 16 16" width="18" height="18"><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"></path></svg>Tip</p>
<p>This is a tip.</p>
</blockquote>
<blockquote class="blockquote-tag blockquote-tag-important">
<p class="blockquote-tag-title"><svg viewbox="0 0 16 16" width="18" height="18"><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"></path></svg>Important</p>
<p>This is important.</p>
</blockquote>
<blockquote class="blockquote-tag blockquote-tag-warning">
<p class="blockquote-tag-title"><svg viewbox="0 0 16 16" width="18" height="18"><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></svg>Warning</p>
<p>This is a warning.</p>
</blockquote>
<blockquote class="blockquote-tag blockquote-tag-caution">
<p class="blockquote-tag-title"><svg viewbox="0 0 16 16" width="18" height="18"><path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path></svg>Caution</p>
<p>This is a caution.</p>
</blockquote>
<blockquote>
<p>[!UNKNOWN]
This is an unknown tag.</p>
</blockquote>

View File

@@ -0,0 +1,26 @@
<h1 id="admonitions"><a class="header" href="#admonitions">Admonitions</a></h1>
<blockquote>
<p>[!NOTE]
This is a note.</p>
<p>There are multiple paragraphs.</p>
</blockquote>
<blockquote>
<p>[!TIP]
This is a tip.</p>
</blockquote>
<blockquote>
<p>[!IMPORTANT]
This is important.</p>
</blockquote>
<blockquote>
<p>[!WARNING]
This is a warning.</p>
</blockquote>
<blockquote>
<p>[!CAUTION]
This is a caution.</p>
</blockquote>
<blockquote>
<p>[!UNKNOWN]
This is an unknown tag.</p>
</blockquote>

View File

@@ -0,0 +1,3 @@
# Summary
- [Admonitions](./admonitions.md)

View File

@@ -0,0 +1,21 @@
# Admonitions
> [!NOTE]
> This is a note.
>
> There are multiple paragraphs.
> [!TIP]
> This is a tip.
> [!IMPORTANT]
> This is important.
> [!WARNING]
> This is a warning.
> [!CAUTION]
> This is a caution.
> [!UNKNOWN]
> This is an unknown tag.