markdown_it_footnote/
inline.rs

1//! Plugin to parse inline footnotes
2//!
3//! ```rust
4//! let parser = &mut markdown_it::MarkdownIt::new();
5//! markdown_it::plugins::cmark::add(parser);
6//! markdown_it_footnote::inline::add(parser);
7//! let root = parser.parse("Example^[This is a footnote]");
8//! let mut names = vec![];
9//! root.walk(|node,_| { names.push(node.name()); });
10//! assert_eq!(names, vec![
11//! "markdown_it::parser::core::root::Root",
12//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
13//! "markdown_it::parser::inline::builtin::skip_text::Text",
14//! "markdown_it_footnote::inline::InlineFootnote",
15//! "markdown_it_footnote::definitions::FootnoteDefinition",
16//! "markdown_it::parser::inline::builtin::skip_text::Text",
17//! "markdown_it_footnote::references::FootnoteReference"
18//! ]);
19//! ```
20use markdown_it::{
21    parser::inline::{InlineRule, InlineState},
22    MarkdownIt, Node, NodeValue,
23};
24
25use crate::{definitions::FootnoteDefinition, FootnoteMap};
26
27/// Add the inline footnote plugin to the parser
28pub fn add(md: &mut MarkdownIt) {
29    // insert this rule into inline subparser
30    md.inline.add_rule::<InlineFootnoteScanner>();
31}
32
33#[derive(Debug)]
34pub struct InlineFootnote;
35impl NodeValue for InlineFootnote {
36    fn render(&self, node: &Node, fmt: &mut dyn markdown_it::Renderer) {
37        // simply pass-through to children
38        fmt.contents(&node.children);
39    }
40}
41
42// This is an extension for the inline subparser.
43struct InlineFootnoteScanner;
44
45impl InlineRule for InlineFootnoteScanner {
46    const MARKER: char = '^';
47
48    fn check(state: &mut InlineState) -> Option<usize> {
49        let mut chars = state.src[state.pos..state.pos_max].chars();
50
51        // check line starts with the correct syntax
52        let Some('^') = chars.next() else { return None; };
53        let Some('[') = chars.next() else { return None; };
54
55        let content_start = state.pos + 2;
56
57        match parse_footnote(state, content_start) {
58            Some(content_end) => Some(content_end + 1 - state.pos),
59            None => None,
60        }
61    }
62
63    fn run(state: &mut InlineState) -> Option<(Node, usize)> {
64        let mut chars = state.src[state.pos..state.pos_max].chars();
65
66        // check line starts with the correct syntax
67        let Some('^') = chars.next() else { return None; };
68        let Some('[') = chars.next() else { return None; };
69
70        let content_start = state.pos + 2;
71
72        match parse_footnote(state, content_start) {
73            Some(content_end) => {
74                let foot_map = state.root_ext.get_or_insert_default::<FootnoteMap>();
75                let (def_id, ref_id) = foot_map.add_inline_def();
76
77                // create node and set it as current
78                let current_node = std::mem::replace(
79                    &mut state.node,
80                    Node::new(FootnoteDefinition {
81                        label: None,
82                        def_id: Some(def_id),
83                        inline: true,
84                    }),
85                );
86
87                // perform nested parsing
88                let start = state.pos;
89                let max = state.pos_max;
90                state.pos = content_start;
91                state.pos_max = content_end;
92                state.md.inline.tokenize(state);
93                state.pos = start;
94                state.pos_max = max;
95
96                // restore current node
97                let def_node = std::mem::replace(&mut state.node, current_node);
98
99                let ref_node = Node::new(crate::references::FootnoteReference {
100                    label: None,
101                    ref_id,
102                    def_id,
103                });
104
105                // wrap the footnote definition and reference in an outer node to return
106                let mut outer_node = Node::new(InlineFootnote);
107                outer_node.children = vec![def_node, ref_node];
108
109                Some((outer_node, content_end + 1 - state.pos))
110            }
111            None => None,
112        }
113    }
114}
115
116// returns the end position of the footnote
117// this function assumes that first character ("[") already matches;
118fn parse_footnote(state: &mut InlineState, start: usize) -> Option<usize> {
119    let old_pos = state.pos;
120    let mut label_end = None;
121    state.pos = start + 1;
122    let mut found = false;
123    while let Some(ch) = state.src[state.pos..state.pos_max].chars().next() {
124        if ch == ']' {
125            found = true;
126            break;
127        }
128        state.md.inline.skip_token(state);
129    }
130
131    if found {
132        label_end = Some(state.pos);
133    }
134
135    // restore old state
136    state.pos = old_pos;
137
138    label_end
139}