markdown_it_footnote/
definitions.rs1use 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
24pub fn add(md: &mut MarkdownIt) {
26 md.block
28 .add_rule::<FootnoteDefinitionScanner>()
29 .before::<ReferenceScanner>();
30}
31
32#[derive(Debug)]
33pub 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
56struct 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 let Some('[') = chars.next() else { return None; };
69 let Some('^') = chars.next() else { return None; };
70
71 let mut label = String::new();
73 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 let mut spaces = 0;
94 loop {
95 match chars.next() {
96 None => break,
97 Some(' ') => spaces += 1,
98 Some('\t') => spaces += 1, Some(_) => break,
100 }
101 }
102 Some((label, spaces))
103 }
104}
105
106impl BlockRule for FootnoteDefinitionScanner {
107 fn check(state: &mut BlockState) -> Option<()> {
108 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 let foot_map = state.root_ext.get_or_insert_default::<FootnoteMap>();
122 let def_id = foot_map.add_def(&label);
123
124 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 let first_line = state.line;
135 let first_line_offsets = state.line_offsets[first_line].clone();
136
137 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 state.blk_indent += 4;
143 state.md.block.tokenize(state);
144 state.blk_indent -= 4;
145
146 let num_lines = state.line - first_line;
148
149 state.line_offsets[first_line] = first_line_offsets;
151 state.line = first_line;
152
153 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 assert!(node.children.first().unwrap().is::<FootnoteDefinition>());
171
172 }
175}