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}