markdown_it_footnotes/
lib.rs1use markdown_it::{
2 MarkdownIt, Node, NodeValue, Renderer,
3 parser::{
4 inline::{ InlineRule, InlineState },
5 block::{ BlockRule, BlockState },
6 core::CoreRule,
7 extset::MarkdownItExt
8 },
9 plugins::cmark::block::reference::ReferenceScanner
10};
11use std::collections::HashMap;
12
13#[derive(Debug)]
14pub struct FootnoteOptions {
15 fn_def_id_pref: String,
16 fn_ref_id_pref: String,
17
18 fn_br_text: String,
19 fn_br_class: String,
20
21 fn_def_class: String,
22 fn_ref_class: String,
23
24 fn_list_class: String,
25}
26
27impl Default for FootnoteOptions {
28 fn default() -> Self {
29 Self {
30 fn_def_class: "footnotes-def".to_string(),
31 fn_def_id_pref: "fnd".to_string(),
32
33 fn_br_text: "↑".to_string(),
34 fn_br_class: "footnote-back".to_string(),
35
36 fn_ref_id_pref: "fnr".to_string(),
37 fn_ref_class: "footnotes-ref".to_string(),
38
39 fn_list_class: "footnotes-list".to_string(),
40 }
41 }
42}
43impl MarkdownItExt for FootnoteOptions {}
44
45#[derive(Debug)]
46struct FootnoteReference {
47 pub r#ref: String,
48 pub count: usize,
49}
50#[derive(Debug)]
51struct FootnoteDefinition {
52 pub id: String,
53 pub count: usize, pub br_text: String,
56 pub br_class: String,
57
58 pub ref_id_prefix: String,
59}
60#[derive(Debug)]
61struct FootnoteList(usize);
62
63fn sluggify(name: &str) -> String {
64 name.to_string().replace(|c| !char::is_alphanumeric(c) && c != ' ', "").replace(" ", "-").to_lowercase()
65}
66
67impl NodeValue for FootnoteReference {
68 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
69 fmt.open("a", &node.attrs);
70 fmt.text(&self.r#ref);
71 fmt.close("a");
72 }
73}
74
75
76impl NodeValue for FootnoteDefinition {
77 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
78 fmt.open("li", &node.attrs);
79 fmt.cr();
80 for i in 0..self.count {
81 let fn_i = (i+1).to_string();
82 let fn_ref = self.ref_id_prefix.clone() + &fn_i;
83 fmt.open("a", &[("href", fn_ref), ("class", self.br_class.clone())]);
84 fmt.text_raw(&self.br_text);
85 fmt.close("a");
86 fmt.cr();
87 }
88 fmt.open("strong", &[]);
89 fmt.text(&self.id);
90 fmt.close("strong");
91 fmt.text(":");
92 fmt.cr();
93 fmt.contents(&node.children);
94 fmt.cr();
95 fmt.close("li");
96 fmt.cr();
97 }
98}
99
100impl NodeValue for FootnoteList {
101 fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
102 fmt.cr();
103 fmt.open("ul", &node.attrs);
104 fmt.cr();
105 fmt.contents(&node.children);
106 fmt.close("ul");
107 fmt.cr();
108 }
109}
110
111struct FootnoteRefsInlinRule; struct FootnoteDefsBlockRule; struct FootnoteCountCoreRule; struct FootnoteGroupCoreRule; impl InlineRule for FootnoteRefsInlinRule {
117 const MARKER: char = '[';
118 fn run(state: &mut InlineState) -> Option<(Node, usize)> {
119 let input = &state.src[state.pos..];
120 if !input.starts_with("[^") || &state.src[state.pos-1..] == "\n" {
121 return None;
122 }
123
124 let mut last_pos = 0;
125 for i in 2..input.len() {
126 if !input[i..].starts_with("]") || input[i-1..].starts_with(r"\") {
127 continue;
128 }
129 last_pos = i;
130 break;
131 }
132 let label = String::from(&input[2..last_pos]);
133
134 let options = state.md.ext.get::<FootnoteOptions>().unwrap();
135
136 let ref_class = options.fn_ref_class.clone();
137
138 let def_id_pref = options.fn_def_id_pref.clone();
139 let mut node = Node::new(FootnoteReference {
140 r#ref: label.clone(),
141 count: 0,
142 });
143
144 node.attrs.push(("class", ref_class));
145 node.attrs.push(("href", String::from("#") + &def_id_pref + "-" + &sluggify(&label)));
146 Some((
147 node,
148 last_pos+1
149 ))
150 }
151}
152
153impl FootnoteDefsBlockRule {
154 fn get_label(state: &mut BlockState) -> Option<String> {
155 let line = state.get_line(state.line);
156 if !line.starts_with("[^") { return None; }
157
158 let mut character = 2;
159 while character < line.len() {
160 if line[character..].starts_with("]") { break; }
161 character += 1;
162 }
163 let label = &line[2..character];
164
165 Some(label.to_string())
166 }
167}
168impl BlockRule for FootnoteDefsBlockRule {
169 fn run(state: &mut BlockState) -> Option<(Node, usize)> {
170 let label = Self::get_label(state)?;
171
172 let options = state.md.ext.get::<FootnoteOptions>().unwrap();
173
174 let def_id_pref = options.fn_def_id_pref.clone();
175 let def_class = options.fn_def_class.clone();
176
177 let br_text = options.fn_br_text.clone();
178 let br_class = options.fn_br_class.clone();
179
180 let ref_id_pref = options.fn_ref_id_pref.clone();
181
182 let mut node = Node::new(FootnoteDefinition {
183 id: label.clone(),
184 count: 0,
185
186 br_text: br_text,
187 br_class: br_class,
188
189 ref_id_prefix: ref_id_pref + "-" + &sluggify(&label) + "-"
190 });
191
192 node.attrs.push(("id", def_id_pref + "-" + &sluggify(&label)));
193 node.attrs.push(("class", def_class));
194
195 let old_node = std::mem::replace(&mut state.node, node);
196
197 let init_line = state.line;
198 let init_offsets = state.line_offsets[init_line].clone();
199
200 state.line_offsets[init_line].first_nonspace += 5 + label.clone().len();
201 state.line_offsets[init_line].indent_nonspace += 5i32;
202
203 state.blk_indent += 4;
204 state.md.block.tokenize(state);
205 state.blk_indent -= 4;
206
207 let content_lines = state.line - init_line;
208
209 state.line = init_line;
210 state.line_offsets[init_line] = init_offsets;
211
212 Some((
213 std::mem::replace(&mut state.node, old_node),
214 content_lines
215 ))
216 }
217}
218
219impl CoreRule for FootnoteCountCoreRule {
220 fn run(root: &mut Node, md: &MarkdownIt) {
221 let mut counts: HashMap<String, usize> = HashMap::new();
222
223 let options = md.ext.get::<FootnoteOptions>().unwrap();
224 let footnote_reference = |id: &str, count: usize| options.fn_ref_id_pref.clone() + "-" + &sluggify(id) + "-" + &count.to_string();
225 root.walk_mut(|node, _| {
226 let reference = match node.cast_mut::<FootnoteReference>() {
227 Some(r) => r,
228 None => return ()
229 };
230 let ref_id = reference.r#ref.clone();
231 counts.entry(ref_id.clone()).and_modify(|c| *c += 1).or_insert(1);
232 let count = counts[&ref_id];
233 reference.count = count;
234
235 node.attrs.push(("id", footnote_reference(&ref_id, count)));
236 });
237 root.walk_mut(|node, _| {
238 let definition = match node.cast_mut::<FootnoteDefinition>() {
239 Some(r) => r,
240 None => return ()
241 };
242 let def_id = definition.id.clone();
243 counts.entry(def_id.clone()).or_insert(1);
244 let count = counts[&def_id];
245 definition.count = count;
246
247 });
248 }
249}
250
251#[derive(Debug)]
252struct PlaceholderNode();
253impl NodeValue for PlaceholderNode {}
254
255impl CoreRule for FootnoteGroupCoreRule {
256 fn run(root: &mut Node, md: &MarkdownIt) {
257 let mut index = 0;
258 let mut open_index = 0;
259 let options = md.ext.get::<FootnoteOptions>().unwrap();
260 let class = options.fn_list_class.clone();
261 let mut defs: Vec<(usize, Box<Node>)> = Vec::new();
262 root.walk_mut(|node, _depth| {
263 if !node.is::<FootnoteDefinition>() {
264 index += 1;
265 open_index = index;
266 return;
267 }
268 let mut replaced = if index != open_index { Node::new(PlaceholderNode()) } else { Node::new(FootnoteList(open_index)) };
269 replaced.attrs.push(("class", class.clone()));
270 let extract = std::mem::replace(node, replaced);
271 defs.push((open_index, Box::new(extract)));
272 });
273 index = 0;
274 root.children.retain(|child| {
275 if !child.is::<PlaceholderNode>() {
276 return true;
277 }
278 index += 1;
279 false
280 });
281 root.walk_mut(|node, _depth| {
282 let def_list_id = match node.cast_mut::<FootnoteList>() {
283 Some(FootnoteList(n)) => *n,
284 None => return (),
285 };
286 for def in &mut defs {
287 if def.0 != def_list_id { continue; }
288 let a = std::mem::replace(&mut def.1, Box::new(Node::new(PlaceholderNode())));
289 node.children.push(*a);
290 }
291 });
292 }
293}
294
295pub fn add(md: &mut MarkdownIt) {
296 md.ext.get_or_insert_default::<FootnoteOptions>();
297 md.inline.add_rule::<FootnoteRefsInlinRule>();
298 md.block.add_rule::<FootnoteDefsBlockRule>().before::<ReferenceScanner>();
299 md.add_rule::<FootnoteCountCoreRule>();
300 md.add_rule::<FootnoteGroupCoreRule>().after::<FootnoteCountCoreRule>();
301}
302
303pub fn add_with_options(md: &mut MarkdownIt, options: FootnoteOptions) {
304 md.ext.insert(options);
305 md.inline.add_rule::<FootnoteRefsInlinRule>();
306 md.block.add_rule::<FootnoteDefsBlockRule>().before::<ReferenceScanner>();
307 md.add_rule::<FootnoteCountCoreRule>();
308 md.add_rule::<FootnoteGroupCoreRule>().after::<FootnoteCountCoreRule>();
309}
310