markdown_it_footnote/
references.rs

1//! Plugin to parse footnote references
2//!
3//! ```rust
4//! let parser = &mut markdown_it::MarkdownIt::new();
5//! markdown_it::plugins::cmark::add(parser);
6//! markdown_it_footnote::references::add(parser);
7//! markdown_it_footnote::definitions::add(parser);
8//! let root = parser.parse("[^label]\n\n[^label]: This is a footnote");
9//! let mut names = vec![];
10//! root.walk(|node,_| { names.push(node.name()); });
11//! assert_eq!(names, vec![
12//! "markdown_it::parser::core::root::Root",
13//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
14//! "markdown_it_footnote::references::FootnoteReference",
15//! "markdown_it_footnote::definitions::FootnoteDefinition",
16//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
17//! "markdown_it::parser::inline::builtin::skip_text::Text"
18//! ]);
19//! ```
20use markdown_it::parser::inline::{InlineRule, InlineState};
21use markdown_it::{MarkdownIt, Node, NodeValue, Renderer};
22
23use crate::FootnoteMap;
24
25/// Add the footnote reference parsing to the markdown parser
26pub fn add(md: &mut MarkdownIt) {
27    // insert this rule into inline subparser
28    md.inline.add_rule::<FootnoteReferenceScanner>();
29}
30
31#[derive(Debug)]
32/// AST node for footnote reference
33pub struct FootnoteReference {
34    pub label: Option<String>,
35    pub ref_id: usize,
36    pub def_id: usize,
37}
38
39impl NodeValue for FootnoteReference {
40    fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
41        let mut attrs = node.attrs.clone();
42        attrs.push(("class", "footnote-ref".into()));
43
44        fmt.open("sup", &attrs);
45        fmt.open(
46            "a",
47            &[
48                ("href", format!("#fn{}", self.def_id)),
49                ("id", format!("fnref{}", self.ref_id)),
50            ],
51        );
52        fmt.text(&format!("[{}]", self.def_id));
53        fmt.close("a");
54        fmt.close("sup");
55    }
56}
57
58// This is an extension for the inline subparser.
59struct FootnoteReferenceScanner;
60
61impl InlineRule for FootnoteReferenceScanner {
62    const MARKER: char = '[';
63
64    fn run(state: &mut InlineState) -> Option<(Node, usize)> {
65        let mut chars = state.src[state.pos..state.pos_max].chars();
66
67        // check line starts with the correct syntax
68        let Some('[') = chars.next() else { return None; };
69        let Some('^') = chars.next() else { return None; };
70
71        // gather the label
72        let mut label = String::new();
73        // The labels in footnote references may not contain spaces, tabs, or newlines.
74        // Backslash escapes form part of the label and do not escape anything
75        loop {
76            match chars.next() {
77                None => return None,
78                Some(']') => {
79                    break;
80                }
81                Some(' ') => return None,
82                Some(c) => label.push(c),
83            }
84        }
85        if label.is_empty() {
86            return None;
87        }
88
89        let definitions = state.root_ext.get_or_insert_default::<FootnoteMap>();
90        let (def_id, ref_id) = match definitions.add_ref(&label) {
91            Some(value) => value,
92            // no definition found so this is not a footnote reference
93            None => return None,
94        };
95
96        let length = label.len() + 3; // 3 for '[^' and ']'
97
98        // return new node and length of this structure
99        Some((
100            Node::new(FootnoteReference {
101                label: Some(label),
102                ref_id,
103                def_id,
104            }),
105            length,
106        ))
107    }
108}