markdown_it_footnote/
back_refs.rs

1//! Plugin to add anchor(s) to footnote definitions,
2//! with links back to the reference(s).
3//!
4//! ```rust
5//! let parser = &mut markdown_it::MarkdownIt::new();
6//! markdown_it::plugins::cmark::add(parser);
7//! markdown_it_footnote::references::add(parser);
8//! markdown_it_footnote::definitions::add(parser);
9//! markdown_it_footnote::back_refs::add(parser);
10//! let root = parser.parse("[^label]\n\n[^label]: This is a footnote");
11//! let mut names = vec![];
12//! root.walk(|node,_| { names.push(node.name()); });
13//! assert_eq!(names, vec![
14//! "markdown_it::parser::core::root::Root",
15//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
16//! "markdown_it_footnote::references::FootnoteReference",
17//! "markdown_it_footnote::definitions::FootnoteDefinition",
18//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
19//! "markdown_it::parser::inline::builtin::skip_text::Text",
20//! "markdown_it_footnote::back_refs::FootnoteRefAnchor",
21//! ]);
22//! ```
23use markdown_it::{
24    parser::core::{CoreRule, Root},
25    plugins::cmark::block::paragraph::Paragraph,
26    MarkdownIt, Node, NodeValue,
27};
28
29use crate::{definitions::FootnoteDefinition, FootnoteMap};
30
31pub fn add(md: &mut MarkdownIt) {
32    // insert this rule into parser
33    md.add_rule::<FootnoteBackrefRule>();
34}
35
36#[derive(Debug)]
37pub struct FootnoteRefAnchor {
38    pub ref_ids: Vec<usize>,
39}
40impl NodeValue for FootnoteRefAnchor {
41    fn render(&self, _: &Node, fmt: &mut dyn markdown_it::Renderer) {
42        for ref_id in self.ref_ids.iter() {
43            fmt.text(" ");
44            fmt.open(
45                "a",
46                &[
47                    ("href", format!("#fnref{}", ref_id)),
48                    ("class", String::from("footnote-backref")),
49                ],
50            );
51            // # ↩ with escape code to prevent display as Apple Emoji on iOS
52            fmt.text("\u{21a9}\u{FE0E}");
53            fmt.close("a");
54        }
55    }
56}
57
58// This is an extension for the markdown parser.
59struct FootnoteBackrefRule;
60
61impl CoreRule for FootnoteBackrefRule {
62    fn run(root: &mut Node, _: &MarkdownIt) {
63        // TODO this seems very cumbersome
64        // but it is also how the markdown_it::InlineParserRule works
65        let data = root.cast_mut::<Root>().unwrap();
66        let root_ext = std::mem::take(&mut data.ext);
67        let map = match root_ext.get::<FootnoteMap>() {
68            Some(map) => map,
69            None => return,
70        };
71
72        // walk through the AST and add backref anchors to footnote definitions
73        root.walk_mut(|node, _| {
74            if let Some(def_node) = node.cast::<FootnoteDefinition>() {
75                let ref_ids = {
76                    match def_node.def_id {
77                        Some(def_id) => map.referenced_by(def_id),
78                        None => Vec::new(),
79                    }
80                };
81                if !ref_ids.is_empty() {
82                    // if the final child is a paragraph node,
83                    // append the anchor to its children,
84                    // otherwise simply append to the end of the node children
85                    match node.children.last_mut() {
86                        Some(last) => {
87                            if last.is::<Paragraph>() {
88                                last.children.push(Node::new(FootnoteRefAnchor { ref_ids }));
89                            } else {
90                                node.children.push(Node::new(FootnoteRefAnchor { ref_ids }));
91                            }
92                        }
93                        _ => {
94                            node.children.push(Node::new(FootnoteRefAnchor { ref_ids }));
95                        }
96                    }
97                }
98            }
99        });
100
101        let data = root.cast_mut::<Root>().unwrap();
102        data.ext = root_ext;
103    }
104}