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}