Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support definition lists #168

Merged
merged 1 commit into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ hosted-html = "https://doc.rust-lang.org/book" # URL of a HTML version of the bo
[output.pandoc.markdown.extensions] # enable additional Markdown extensions
gfm = false # enable pulldown-cmark's GitHub Flavored Markdown extensions
math = false # parse inline ($a^b$) and display ($$a^b$$) math
definition-lists = false # parse definition lists

[output.pandoc.code]
# Display hidden lines in code blocks (e.g., lines in Rust blocks prefixed by '#').
Expand Down Expand Up @@ -116,7 +117,8 @@ variable-name = "value"
- [x] [Math](https://docs.rs/pulldown-cmark/0.13.0/pulldown_cmark/struct.Options.html#associatedconstant.ENABLE_MATH)
(Enable by setting `output.pandoc.markdown.extensions.math` to `true`)

- [ ] [Definition Lists](https://docs.rs/pulldown-cmark/0.13.0/pulldown_cmark/struct.Options.html#associatedconstant.ENABLE_DEFINITION_LIST)
- [x] [Definition Lists](https://docs.rs/pulldown-cmark/0.13.0/pulldown_cmark/struct.Options.html#associatedconstant.ENABLE_DEFINITION_LIST)
(Enable by setting `output.pandoc.markdown.extensions.definition-lists` to `true`)

- [ ] [Superscript](https://docs.rs/pulldown-cmark/0.13.0/pulldown_cmark/struct.Options.html#associatedconstant.ENABLE_SUPERSCRIPT)

Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ struct MarkdownExtensionConfig {
/// Enable [`pulldown_cmark::Options::ENABLE_MATH`].
#[serde(default = "defaults::disabled")]
pub math: bool,
/// Enable [`pulldown_cmark::Options::ENABLE_DEFINITION_LIST`].
#[serde(default = "defaults::disabled")]
pub definition_lists: bool,
}

/// Configuration for tweaking how code blocks are rendered.
Expand Down
55 changes: 55 additions & 0 deletions src/pandoc/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,18 @@ impl SerializeElement for Cell {
}
}

impl SerializeElement for DefinitionListItem {
type Serializer<'a, 'book: 'a, 'p: 'a + 'book, W: io::Write + 'a> =
SerializeDefinitionListItem<'a, 'book, 'p, W>;

fn serializer<'a, 'book, 'p, W: io::Write>(
&mut self,
serializer: &'a mut Serializer<'p, 'book, W>,
) -> Self::Serializer<'a, 'book, 'p, W> {
SerializeDefinitionListItem { serializer }
}
}

impl<Item: SerializeElement + Copy> SerializeElement for List<Item> {
type Serializer<'a, 'book: 'a, 'p: 'a + 'book, W: io::Write + 'a> =
anyhow::Result<SerializeList<'a, 'book, 'p, W, Item>>;
Expand Down Expand Up @@ -476,6 +488,8 @@ pub struct List<T>(T);
pub struct Row;
#[derive(Copy, Clone)]
pub struct Cell;
#[derive(Copy, Clone)]
pub struct DefinitionListItem;

pub type SerializeBlocks<'a, 'book, 'p, W> = SerializeList<'a, 'book, 'p, W, Block>;
pub type SerializeInlines<'a, 'book, 'p, W> = SerializeList<'a, 'book, 'p, W, Inline>;
Expand Down Expand Up @@ -517,6 +531,11 @@ pub struct SerializeCell<'a, 'book, 'p, W: io::Write> {
serializer: &'a mut Serializer<'p, 'book, W>,
}

#[must_use]
pub struct SerializeDefinitionListItem<'a, 'book, 'p, W: io::Write> {
serializer: &'a mut Serializer<'p, 'book, W>,
}

impl<'book, 'p, W: io::Write> SerializeInline<'_, 'book, 'p, W> {
/// Text (string)
pub fn serialize_str(self, s: &str) -> anyhow::Result<()> {
Expand Down Expand Up @@ -808,6 +827,20 @@ impl<'a, 'book, 'p, W: io::Write> SerializeBlock<'a, 'book, 'p, W> {
serializer.finish()
}

/// Definition list. Each list item is a pair consisting of a term (a list of inlines)
/// and one or more definitions (each a list of blocks)
pub fn serialize_definition_list(
self,
items: impl FnOnce(
&mut SerializeList<'_, 'book, 'p, W, DefinitionListItem>,
) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
write!(self.serializer.unescaped(), "DefinitionList ")?;
let mut serializer = SerializeList::new(self.serializer, DefinitionListItem)?;
items(&mut serializer)?;
serializer.finish()
}

/// Horizontal rule
pub fn serialize_horizontal_rule(self) -> anyhow::Result<()> {
write!(self.serializer.unescaped(), "HorizontalRule")?;
Expand Down Expand Up @@ -971,6 +1004,28 @@ impl<'book, 'p, W: io::Write> SerializeCell<'_, 'book, 'p, W> {
}
}

impl<'book, 'p, W: io::Write> SerializeDefinitionListItem<'_, 'book, 'p, W> {
/// A term (a list of inlines) and one or more definitions (each a list of blocks)
pub fn serialize_item(
self,
term: impl FnOnce(&mut SerializeInlines<'_, 'book, 'p, W>) -> anyhow::Result<()>,
definitions: impl FnOnce(
&mut SerializeList<'_, 'book, 'p, W, List<Block>>,
) -> anyhow::Result<()>,
) -> anyhow::Result<()> {
write!(self.serializer.unescaped(), "(")?;
let mut serializer = SerializeList::new(self.serializer, Inline)?;
term(&mut serializer)?;
serializer.finish()?;
write!(self.serializer.unescaped(), ", ")?;
let mut serializer = SerializeList::new(self.serializer, List(Block))?;
definitions(&mut serializer)?;
serializer.finish()?;
write!(self.serializer.unescaped(), ")")?;
Ok(())
}
}

impl<W: io::Write> SerializeText<'_, '_, '_, W> {
pub fn serialize_text(self, s: &str) -> anyhow::Result<()> {
let writer = self.serializer.escaped();
Expand Down
30 changes: 25 additions & 5 deletions src/preprocess.rs
Original file line number Diff line number Diff line change
Expand Up @@ -627,13 +627,20 @@ impl<'book> Parser<'book> {

let options = {
let mut options = PARSER_OPTIONS;
let MarkdownExtensionConfig { gfm, math } = extensions;
let MarkdownExtensionConfig {
gfm,
math,
definition_lists,
} = extensions;
if gfm {
options |= Options::ENABLE_GFM;
}
if math {
options |= Options::ENABLE_MATH;
}
if definition_lists {
options |= Options::ENABLE_DEFINITION_LIST;
}
options
};

Expand Down Expand Up @@ -817,6 +824,18 @@ impl<'book, 'preprocessor> PreprocessChapter<'book, 'preprocessor> {
push_element(self, tree, MdElement::List(start_number))
}
Tag::Item => push_element(self, tree, MdElement::Item),
Tag::DefinitionList => {
self.preprocessor.ctx.cur_list_depth += 1;
self.preprocessor.ctx.max_list_depth = cmp::max(
self.preprocessor.ctx.max_list_depth,
self.preprocessor.ctx.cur_list_depth,
);
push_html_element(self, tree, local_name!("dl"))
}
Tag::DefinitionListTitle => push_html_element(self, tree, local_name!("dt")),
Tag::DefinitionListDefinition => {
push_html_element(self, tree, local_name!("dd"))
}
Tag::FootnoteDefinition(label) => {
let node = push_element(self, tree, MdElement::FootnoteDefinition)?;
tree.footnote(label, node);
Expand Down Expand Up @@ -916,10 +935,6 @@ impl<'book, 'preprocessor> PreprocessChapter<'book, 'preprocessor> {
}
return Ok(());
}
// Definition list parsing is not enabled
Tag::DefinitionList
| Tag::DefinitionListTitle
| Tag::DefinitionListDefinition => unreachable!(),
// Not enabled
Tag::Superscript | Tag::Subscript => unreachable!(),
}?;
Expand All @@ -940,6 +955,11 @@ impl<'book, 'preprocessor> PreprocessChapter<'book, 'preprocessor> {
Element::Markdown(MdElement::List(_)) => {
self.preprocessor.ctx.cur_list_depth -= 1
}
Element::Html(element)
if element.name.expanded() == expanded_name!(html "dl") =>
{
self.preprocessor.ctx.cur_list_depth -= 1
}
Element::Html(element)
if element.name.expanded() == expanded_name!(html "thead") =>
{
Expand Down
78 changes: 76 additions & 2 deletions src/preprocess/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,8 @@ impl<'book> Emitter<'book> {
cell.children()
{
self.serialize_node(
node, serializer,
)?;
node, serializer,
)?;
}
Ok(())
},
Expand Down Expand Up @@ -643,6 +643,80 @@ impl<'book> Emitter<'book> {
}
}
}
local_name!("dl") => {
enum Component {
Term,
Definition,
}
let mut components = node
.children()
.filter_map(|node| match node.value() {
Node::Element(Element::Html(element)) => {
match element.name.expanded() {
expanded_name!(html "dt") => {
Some((Component::Term, node, &element.attrs))
}
expanded_name!(html "dd") => {
Some((Component::Definition, node, &element.attrs))
}
_ => None,
}
}
_ => None,
})
.peekable();
return serializer
.blocks()?
.serialize_element()?
.serialize_definition_list(|items| {
while let Some((component, node, attrs)) = components.next() {
match component {
Component::Term => {}
Component::Definition => {
anyhow::bail!("definition list definition with no term")
}
};
items.serialize_element()?.serialize_item(
|term| {
if attrs.is_empty() {
term.serialize_nested(|serializer| {
self.serialize_children(node, serializer)
})
} else {
// Wrap term in a span with the attributes since Pandoc
// doesn't support attributes on definition list terms
term.serialize_element()?.serialize_span(
attrs,
|inlines| {
inlines.serialize_nested(|serializer| {
self.serialize_children(
node, serializer,
)
})
},
)
}
},
|definitions| {
while let Some((_, definition, _)) =
components.next_if(|(component, _, _)| {
matches!(component, Component::Definition)
})
{
let mut serializer =
definitions.serialize_element()??;
serializer.serialize_nested(|serializer| {
self.serialize_children(definition, serializer)
})?;
serializer.finish()?;
}
Ok(())
},
)?
}
Ok(())
});
}
_ => {}
}
serializer.serialize_raw_html(|serializer| {
Expand Down
6 changes: 5 additions & 1 deletion src/preprocess/tree/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@ impl HtmlElement {
}

impl Attributes {
pub fn is_empty(&self) -> bool {
self.id.is_none() && self.classes.is_empty() && self.rest.is_empty()
}

pub fn iter(&self) -> impl Iterator<Item = (&QualName, &StrTendril)> {
const ID: &QualName = &html::name!("id");
const CLASS: &QualName = &html::name!("class");
Expand All @@ -303,7 +307,7 @@ impl fmt::Debug for Node<'_> {
match self {
Node::Document => write!(f, "Document"),
Node::HtmlComment(comment) => write!(f, "<!-- {comment} -->"),
Node::HtmlText(text) => write!(f, "Text({text})"),
Node::HtmlText(text) => write!(f, "HtmlText({:?})", text.as_ref()),
Node::Element(element) => write!(f, "{element:?}"),
}
}
Expand Down
86 changes: 86 additions & 0 deletions src/tests/definition_lists.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use indoc::indoc;

use super::{Chapter, Config, MDBook};

#[test]
fn basic() {
let diff = |source: &str, mut config: Config| {
let chapter = Chapter::new("", source, "chapter.md");
let without = MDBook::init()
.chapter(chapter.clone())
.config(config.clone())
.build();
let with = MDBook::init()
.chapter(chapter)
.config({
config.markdown.extensions.definition_lists = true;
config
})
.build();
similar::TextDiff::from_lines(&without.to_string(), &with.to_string())
.unified_diff()
.to_string()
};
let source = indoc! {"
title 1
: definition 1

title 2
: definition 2 a
: definition 2 b
"};
let latex = diff(source, Config::latex());
insta::assert_snapshot!(latex, @r#"
@@ -3,8 +3,14 @@
│ INFO mdbook_pandoc::pandoc::renderer: Running pandoc
│ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/latex/output.tex
├─ latex/output.tex
-│ title 1 : definition 1
+│ \begin{description}
+│ \tightlist
+│ \item[title 1]
+│ definition 1
+│ \item[title 2]
+│ definition 2 a
-│ title 2 : definition 2 a : definition 2 b
+│ definition 2 b
+│ \end{description}
├─ latex/src/chapter.md
-│ [Para [Str "title 1", SoftBreak, Str ": definition 1"], Para [Str "title 2", SoftBreak, Str ": definition 2 a", SoftBreak, Str ": definition 2 b"]]
+│ [DefinitionList [([Str "title 1"], [[Plain [Str "definition 1"]]]), ([Str "title 2"], [[Plain [Str "definition 2 a"]], [Plain [Str "definition 2 b"]]])]]
"#);
}

#[test]
fn dt_attributes() {
let source = indoc! {r#"
<dl>
<dt id="term1">term 1</dt>
<dd>definition 1</dd>
</dl>

[link to term 1](#term1)
"#};
let latex = MDBook::init()
.chapter(Chapter::new("", source, "chapter.md"))
.config(Config::latex())
.build();
insta::assert_snapshot!(latex, @r##"
├─ log output
│ INFO mdbook::book: Running the pandoc backend
│ INFO mdbook_pandoc::pandoc::renderer: Running pandoc
│ INFO mdbook_pandoc::pandoc::renderer: Wrote output to book/latex/output.tex
├─ latex/output.tex
│ \begin{description}
│ \tightlist
│ \item[\phantomsection\label{book__latex__src__chapter.md__term1}{term 1}]
│ definition 1
│ \end{description}
│ \hyperref[book__latex__src__chapter.md__term1]{link to term 1}
├─ latex/src/chapter.md
│ [DefinitionList [([Span ("term1", [], []) [Str "term 1"]], [[Plain [Str "definition 1"]]])], Plain [Str "
│ "], Para [Link ("", [], []) [Str "link to term 1"] ("#term1", "")]]
"##);
}
1 change: 1 addition & 0 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ mod escaping;
mod alerts;
mod code;
mod css;
mod definition_lists;
mod fonts;
mod footnotes;
mod headings;
Expand Down
Loading