markdown_it_footnote/collect.rs
1//! Plugin to collect footnote definitions,
2//! removing duplicate/unreferenced ones,
3//! and move them to be the last child of the root node.
4//!
5//! ```rust
6//! let parser = &mut markdown_it::MarkdownIt::new();
7//! markdown_it::plugins::cmark::add(parser);
8//! markdown_it_footnote::references::add(parser);
9//! markdown_it_footnote::definitions::add(parser);
10//! markdown_it_footnote::collect::add(parser);
11//! let root = parser.parse("[^label]\n\n[^label]: This is a footnote\n\n> quote");
12//! let mut names = vec![];
13//! root.walk(|node,_| { names.push(node.name()); });
14//! assert_eq!(names, vec![
15//! "markdown_it::parser::core::root::Root",
16//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
17//! "markdown_it_footnote::references::FootnoteReference",
18//! "markdown_it::plugins::cmark::block::blockquote::Blockquote",
19//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
20//! "markdown_it::parser::inline::builtin::skip_text::Text",
21//! "markdown_it_footnote::collect::FootnotesContainerNode",
22//! "markdown_it_footnote::definitions::FootnoteDefinition",
23//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
24//! "markdown_it::parser::inline::builtin::skip_text::Text",
25//! ]);
26//! ```
27use markdown_it::{
28 parser::core::{CoreRule, Root},
29 plugins::cmark::block::paragraph::Paragraph,
30 MarkdownIt, Node, NodeValue,
31};
32
33use crate::{definitions::FootnoteDefinition, FootnoteMap};
34
35pub fn add(md: &mut MarkdownIt) {
36 // insert this rule into parser
37 md.add_rule::<FootnoteCollectRule>();
38}
39
40#[derive(Debug)]
41struct PlaceholderNode;
42impl NodeValue for PlaceholderNode {}
43
44#[derive(Debug)]
45pub struct FootnotesContainerNode;
46impl NodeValue for FootnotesContainerNode {
47 fn render(&self, node: &Node, fmt: &mut dyn markdown_it::Renderer) {
48 let mut attrs = node.attrs.clone();
49 attrs.push(("class", "footnotes".into()));
50 fmt.cr();
51 fmt.self_close("hr", &[("class", "footnotes-sep".into())]);
52 fmt.cr();
53 fmt.open("section", &attrs);
54 fmt.cr();
55 fmt.open("ol", &[("class", "footnotes-list".into())]);
56 fmt.cr();
57 fmt.contents(&node.children);
58 fmt.cr();
59 fmt.close("ol");
60 fmt.cr();
61 fmt.close("section");
62 fmt.cr();
63 }
64}
65
66// This is an extension for the markdown parser.
67struct FootnoteCollectRule;
68
69impl CoreRule for FootnoteCollectRule {
70 // This is a custom function that will be invoked once per document.
71 //
72 // It has `root` node of the AST as an argument and may modify its
73 // contents as you like.
74 //
75 fn run(root: &mut Node, _: &MarkdownIt) {
76 // TODO this seems very cumbersome
77 // but it is also how the markdown_it::InlineParserRule works
78 let data = root.cast_mut::<Root>().unwrap();
79 let root_ext = std::mem::take(&mut data.ext);
80 let map = match root_ext.get::<FootnoteMap>() {
81 Some(map) => map,
82 None => return,
83 };
84
85 // walk through the AST and extract all footnote definitions
86 let mut defs = vec![];
87 root.walk_mut(|node, _| {
88 // TODO could use drain_filter if it becomes stable: https://github.com/rust-lang/rust/issues/43244
89 // defs.extend(
90 // node.children
91 // .drain_filter(|child| !child.is::<FootnoteDefinition>())
92 // .collect(),
93 // );
94
95 for child in node.children.iter_mut() {
96 if child.is::<FootnoteDefinition>() {
97 let mut extracted = std::mem::replace(child, Node::new(PlaceholderNode));
98 match extracted.cast::<FootnoteDefinition>() {
99 Some(def_node) => {
100 // skip footnotes that are not referenced
101 match def_node.def_id {
102 Some(def_id) => {
103 if map.referenced_by(def_id).is_empty() {
104 continue;
105 }
106 }
107 None => continue,
108 }
109 if def_node.inline {
110 // for inline footnotes,
111 // we need to wrap the definition's children in a paragraph
112 let mut para = Node::new(Paragraph);
113 std::mem::swap(&mut para.children, &mut extracted.children);
114 extracted.children = vec![para];
115 }
116 }
117 None => continue,
118 }
119 defs.push(extracted);
120 }
121 }
122 node.children.retain(|child| !child.is::<PlaceholderNode>());
123 });
124 if defs.is_empty() {
125 return;
126 }
127
128 // wrap the definitions in a container and append them to the root
129 let mut wrapper = Node::new(FootnotesContainerNode);
130 wrapper.children = defs;
131 root.children.push(wrapper);
132
133 let data = root.cast_mut::<Root>().unwrap();
134 data.ext = root_ext;
135 }
136}