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}