Skip to main content

markdown_it_footnotes/
lib.rs

1use 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, // The amount of references to the definition
54
55    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; // Finds references
112struct FootnoteDefsBlockRule; // Finds definitions
113struct FootnoteCountCoreRule; // Counts references per definition
114struct FootnoteGroupCoreRule; // Groups adjacent definitions
115
116impl 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