markdown_it_footnote/
definitions.rs

1//! Plugin to parse footnote definitions
2//!
3//! ```rust
4//! let parser = &mut markdown_it::MarkdownIt::new();
5//! markdown_it::plugins::cmark::add(parser);
6//! markdown_it_footnote::definitions::add(parser);
7//! let root = parser.parse("[^label]: 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_footnote::definitions::FootnoteDefinition",
13//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
14//! "markdown_it::parser::inline::builtin::skip_text::Text",
15//! ]);
16//! ```
17
18use markdown_it::parser::block::{BlockRule, BlockState};
19use markdown_it::plugins::cmark::block::reference::ReferenceScanner;
20use markdown_it::{MarkdownIt, Node, NodeValue, Renderer};
21
22use crate::FootnoteMap;
23
24/// Add the footnote definition plugin to the parser
25pub fn add(md: &mut MarkdownIt) {
26    // insert this rule into block subparser
27    md.block
28        .add_rule::<FootnoteDefinitionScanner>()
29        .before::<ReferenceScanner>();
30}
31
32#[derive(Debug)]
33/// AST node for footnote definition
34pub struct FootnoteDefinition {
35    pub label: Option<String>,
36    pub def_id: Option<usize>,
37    pub inline: bool,
38}
39
40impl NodeValue for FootnoteDefinition {
41    fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
42        let mut attrs = node.attrs.clone();
43        if let Some(def_id) = self.def_id {
44            attrs.push(("id", format!("fn{}", def_id)));
45        }
46        attrs.push(("class", "footnote-item".into()));
47
48        fmt.cr();
49        fmt.open("li", &attrs);
50        fmt.contents(&node.children);
51        fmt.close("li");
52        fmt.cr();
53    }
54}
55
56/// An extension for the block subparser.
57struct FootnoteDefinitionScanner;
58
59impl FootnoteDefinitionScanner {
60    fn is_def(state: &mut BlockState) -> Option<(String, usize)> {
61        if state.line_indent(state.line) >= state.md.max_indent {
62            return None;
63        }
64
65        let mut chars = state.get_line(state.line).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                    if let Some(':') = chars.next() {
80                        break;
81                    } else {
82                        return None;
83                    }
84                }
85                Some(' ') => return None,
86                Some(c) => label.push(c),
87            }
88        }
89        if label.is_empty() {
90            return None;
91        }
92        // get number of spaces to next non-space character
93        let mut spaces = 0;
94        loop {
95            match chars.next() {
96                None => break,
97                Some(' ') => spaces += 1,
98                Some('\t') => spaces += 1, // spaces += 4 - spaces % 4,
99                Some(_) => break,
100            }
101        }
102        Some((label, spaces))
103    }
104}
105
106impl BlockRule for FootnoteDefinitionScanner {
107    fn check(state: &mut BlockState) -> Option<()> {
108        // can interrupt a block elements,
109        // but only if its a child of another footnote definition
110        // TODO I think strictly only paragraphs should be interrupted, but this is not yet possible in markdown-it.rs
111        if state.node.is::<FootnoteDefinition>() && Self::is_def(state).is_some() {
112            return Some(());
113        }
114        None
115    }
116
117    fn run(state: &mut BlockState) -> Option<(Node, usize)> {
118        let (label, spaces) = Self::is_def(state)?;
119
120        // record the footnote label, so we can match references to it later
121        let foot_map = state.root_ext.get_or_insert_default::<FootnoteMap>();
122        let def_id = foot_map.add_def(&label);
123
124        // temporarily set the current node to the footnote definition
125        // so child nodes are added to it
126        let new_node = Node::new(FootnoteDefinition {
127            label: Some(label.clone()),
128            def_id,
129            inline: false,
130        });
131        let old_node = std::mem::replace(&mut state.node, new_node);
132
133        // store the current line and its offsets, so we can restore later
134        let first_line = state.line;
135        let first_line_offsets = state.line_offsets[first_line].clone();
136
137        // temporarily change the first line offsets to account for the footnote label
138        // TODO this is not quite the same as pandoc where spaces >= 8 is code block (here >= 4)
139        state.line_offsets[first_line].first_nonspace += "[^]:".len() + label.len() + spaces;
140        state.line_offsets[first_line].indent_nonspace += "[^]:".len() as i32 + spaces as i32;
141        // tokenize with a +4 space indent
142        state.blk_indent += 4;
143        state.md.block.tokenize(state);
144        state.blk_indent -= 4;
145
146        // get the number of lines the footnote definition occupies
147        let num_lines = state.line - first_line;
148
149        // restore the first line and its offsets
150        state.line_offsets[first_line] = first_line_offsets;
151        state.line = first_line;
152
153        // restore the original node and return the footnote and number of lines it occupies
154        Some((std::mem::replace(&mut state.node, old_node), num_lines))
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn it_works() {
164        let parser = &mut markdown_it::MarkdownIt::new();
165        markdown_it::plugins::cmark::add(parser);
166        markdown_it::plugins::sourcepos::add(parser);
167        add(parser);
168        let node = parser.parse("[^note]: a\n\nhallo\nthere\n");
169        // println!("{:#?}", node);
170        assert!(node.children.first().unwrap().is::<FootnoteDefinition>());
171
172        // let text = node.render();
173        // assert_eq!(text, "hallo\n")
174    }
175}