Add support for definition lists

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

Closes https://github.com/rust-lang/mdBook/issues/2770
This commit is contained in:
Eric Huss 2025-09-17 16:40:56 -07:00
parent 53d39a8654
commit ba4c3ed873
13 changed files with 269 additions and 5 deletions

View file

@ -434,6 +434,8 @@ pub struct HtmlConfig {
pub preferred_dark_theme: Option<String>,
/// Supports smart quotes, apostrophes, ellipsis, en-dash, and em-dash.
pub smart_punctuation: bool,
/// Support for definition lists.
pub definition_lists: bool,
/// Should mathjax be enabled?
pub mathjax_support: bool,
/// Additional CSS stylesheets to include in the rendered page's `<head>`.
@ -501,6 +503,7 @@ impl Default for HtmlConfig {
default_theme: None,
preferred_dark_theme: None,
smart_punctuation: true,
definition_lists: true,
mathjax_support: false,
additional_css: Vec::new(),
additional_js: Vec::new(),

View file

@ -61,7 +61,8 @@ h2:target::before,
h3:target::before,
h4:target::before,
h5:target::before,
h6:target::before {
h6:target::before,
dt:target::before {
display: inline-block;
content: "»";
margin-inline-start: -30px;
@ -285,3 +286,41 @@ sup {
fill: currentColor;
margin-bottom: -0.1em;
}
dt {
font-weight: bold;
margin-top: 0.5em;
margin-bottom: 0.1em;
}
/* This uses a CSS counter to add numbers to definitions, but only if there is
more than one definition. */
dl, dt {
counter-reset: dd-counter;
}
/* When there is more than one definition, increment the counter. The first
selector selects the first definition, and the second one selects definitions
2 and beyond.*/
dd:has(+ dd), dd + dd {
counter-increment: dd-counter;
/* Use flex display to help with positioning the numbers when there is a p
tag inside the definition. */
display: flex;
align-items: flex-start;
}
/* Shows the counter for definitions. The first selector selects the first
definition, and the second one selections definitions 2 and beyond.*/
dd:has(+ dd)::before, dd + dd::before {
content: counter(dd-counter) ". ";
font-weight: 600;
display: inline-block;
margin-right: 0.5em;
}
dd > p {
/* For loose definitions that have a p tag inside, don't add a bunch of
space before the definition. */
margin-top: 0;
}

View file

@ -50,6 +50,7 @@ impl<'a> HtmlRenderOptions<'a> {
) -> HtmlRenderOptions<'a> {
let mut markdown_options = MarkdownOptions::default();
markdown_options.smart_punctuation = config.smart_punctuation;
markdown_options.definition_lists = config.definition_lists;
HtmlRenderOptions {
markdown_options,
path,

View file

@ -818,12 +818,14 @@ where
}
/// This is used after parsing is complete to add a unique `id` attribute
/// to all header elements, and to also add an `<a>` tag so that clicking
/// the header will set the current URL to that header's fragment.
/// to all header and dt elements, and to also add an `<a>` tag so that
/// clicking the element will set the current URL to that element's
/// fragment.
fn add_header_links(&mut self) {
let mut id_counter = HashSet::new();
let headings =
self.node_ids_for_tag(&|name| matches!(name, "h1" | "h2" | "h3" | "h4" | "h5" | "h6"));
let headings = self.node_ids_for_tag(&|name| {
matches!(name, "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "dt")
});
for heading in headings {
let node = self.tree.get(heading).unwrap();
let el = node.value().as_element().unwrap();

View file

@ -24,12 +24,17 @@ pub struct MarkdownOptions {
///
/// This is `true` by default.
pub smart_punctuation: bool,
/// Enables definition lists.
///
/// This is `true` by default.
pub definition_lists: bool,
}
impl Default for MarkdownOptions {
fn default() -> MarkdownOptions {
MarkdownOptions {
smart_punctuation: true,
definition_lists: true,
}
}
}
@ -45,5 +50,8 @@ pub fn new_cmark_parser<'text>(text: &'text str, options: &MarkdownOptions) -> P
if options.smart_punctuation {
opts.insert(Options::ENABLE_SMART_PUNCTUATION);
}
if options.definition_lists {
opts.insert(Options::ENABLE_DEFINITION_LIST);
}
Parser::new_ext(text, opts)
}

View file

@ -98,6 +98,7 @@ theme = "my-theme"
default-theme = "light"
preferred-dark-theme = "navy"
smart-punctuation = true
definition-lists = true
mathjax-support = false
additional-css = ["custom.css", "custom2.css"]
additional-js = ["custom.js"]
@ -125,6 +126,7 @@ The following configuration options are available:
- **smart-punctuation:** Converts quotes to curly quotes, `...` to `…`, `--` to en-dash, and `---` to em-dash.
See [Smart Punctuation](../markdown.md#smart-punctuation).
Defaults to `true`.
- **definition-lists:** Enables [definition lists](../markdown.md#definition-lists). 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

@ -240,3 +240,33 @@ Example:
This makes the level 1 heading with the content `Example heading`, ID `first`, and classes `class1` and `class2`. Note that the attributes should be space-separated.
More information can be found in the [heading attrs spec page](https://github.com/raphlinus/pulldown-cmark/blob/master/pulldown-cmark/specs/heading_attrs.txt).
### Definition lists
Definition lists can be used for things like glossary entries. The term is listed on a line by itself, followed by one or more definitions. Each definition must begin with a `:` (after 0-2 spaces).
Example:
```md
term A
: This is a definition of term A. Text
can span multiple lines.
term B
: This is a definition of term B.
: This has more than one definition.
```
This will render as:
term A
: This is a definition of term A. Text
can span multiple lines.
term B
: This is a definition of term B.
: This has more than one definition.
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.

View file

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

View file

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

View file

@ -0,0 +1,68 @@
<h1 id="definition-lists"><a class="header" href="#definition-lists">Definition Lists</a></h1>
<dl>
<dt id="apple"><a class="header" href="#apple">apple</a></dt>
<dd>red fruit</dd>
<dt id="orange"><a class="header" href="#orange">orange</a></dt>
<dd>orange fruit</dd>
<dt id="apple-1"><a class="header" href="#apple-1">apple</a></dt>
<dd>red fruit</dd>
<dd>computer company</dd>
<dt id="orange-1"><a class="header" href="#orange-1">orange</a></dt>
<dd>orange fruit</dd>
<dd>telecom company</dd>
<dt id="term"><a class="header" href="#term">term</a></dt>
<dd>
<ol>
<li>
<p>Para one</p>
<p>Para two</p>
</li>
</ol>
</dd>
</dl>
<h2 id="term-with-link"><a class="header" href="#term-with-link">Term with link</a></h2>
<dl>
<dt id="apple-2"><a class="header" href="#apple-2"><a href="some-page.html#apple">apple</a></a></dt>
<dd>red fruit</dd>
</dl>
<h2 id="multi-line-term"><a class="header" href="#multi-line-term">Multi-line term</a></h2>
<dl>
<dt id="a-bc"><a class="header" href="#a-bc">a
b<br>c</a></dt>
<dd>
<p>foo</p>
</dd>
</dl>
<h2 id="nested"><a class="header" href="#nested">Nested</a></h2>
<dl>
<dt id="level-one"><a class="header" href="#level-one">level one</a></dt>
<dd>
<dl>
<dt id="l1-level-two"><a class="header" href="#l1-level-two">l1
level two</a></dt>
<dd>
<dl>
<dt id="l2-level-three"><a class="header" href="#l2-level-three">l2
level three</a></dt>
<dd>l3</dd>
</dl>
</dd>
</dl>
</dd>
<dt id="level-one-1"><a class="header" href="#level-one-1">level one</a></dt>
<dd>l1</dd>
</dl>
<h2 id="loose"><a class="header" href="#loose">Loose</a></h2>
<dl>
<dt id="apple-3"><a class="header" href="#apple-3">apple</a></dt>
<dd>
<p>red fruit</p>
</dd>
<dd>
<p>computer company</p>
</dd>
<dt id="orange-2"><a class="header" href="#orange-2">orange</a></dt>
<dd>
<p>orange fruit</p>
</dd>
</dl>

View file

@ -0,0 +1,37 @@
<h1 id="definition-lists"><a class="header" href="#definition-lists">Definition Lists</a></h1>
<p>apple
: red fruit</p>
<p>orange
: orange fruit</p>
<p>apple
: red fruit
: computer company</p>
<p>orange
: orange fruit
: telecom company</p>
<p>term
: 1. Para one</p>
<pre><code> Para two
</code></pre>
<h2 id="term-with-link"><a class="header" href="#term-with-link">Term with link</a></h2>
<p><a href="some-page.html#apple">apple</a>
: red fruit</p>
<h2 id="multi-line-term"><a class="header" href="#multi-line-term">Multi-line term</a></h2>
<p>a
b<br>c</p>
<p>: foo</p>
<h2 id="nested"><a class="header" href="#nested">Nested</a></h2>
<p>level one
: l1
level two
: l2
level three
: l3</p>
<p>level one
: l1</p>
<h2 id="loose"><a class="header" href="#loose">Loose</a></h2>
<p>apple</p>
<p>: red fruit
: computer company</p>
<p>orange</p>
<p>: orange fruit</p>

View file

@ -0,0 +1,3 @@
# Summary
- [Definition lists](./definition_lists.md)

View file

@ -0,0 +1,56 @@
# Definition Lists
apple
: red fruit
orange
: orange fruit
apple
: red fruit
: computer company
orange
: orange fruit
: telecom company
term
: 1. Para one
Para two
## Term with link
[apple](some-page.md#apple)
: red fruit
## Multi-line term
a
b\
c
: foo
## Nested
level one
: l1
level two
: l2
level three
: l3
level one
: l1
## Loose
apple
: red fruit
: computer company
orange
: orange fruit