1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
//! Markdown filters for adjusting the way footnotes show up.
use pulldown_cmark::{CowStr, Event, Tag, TagEnd};
use tracing::debug;
/// Gathers all footnote definitions and pulls them to the end
pub fn collect_footnotes<'a>(
parser: impl Iterator<Item = Event<'a>>,
) -> impl Iterator<Item = Event<'a>> {
CollectFootnotes::Parsing {
parser,
footnotes: vec![],
in_footnote: None,
count: 0,
}
}
enum CollectFootnotes<'a, I> {
Parsing {
parser: I,
footnotes: Vec<Event<'a>>,
in_footnote: Option<CowStr<'a>>,
/// How many footnotes we've encountered so far.
count: usize,
},
Finishing {
footnotes: std::vec::IntoIter<Event<'a>>,
},
}
impl<'a, I> Iterator for CollectFootnotes<'a, I>
where
I: Iterator<Item = Event<'a>>,
{
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
loop {
match self {
CollectFootnotes::Parsing {
parser,
footnotes,
in_footnote,
count,
} => {
match parser.next() {
Some(e) => {
match e {
Event::FootnoteReference(tag) => {
// Manually render footnote here so we can add a backlink id
*count += 1;
let html = format!(
r##"<sup class="footnote-reference"><a href="#{tag}" id="fnref:{tag}">{count}</a></sup>"##,
);
return Some(Event::Html(html.into()));
}
Event::Start(Tag::FootnoteDefinition(tag)) => {
*in_footnote = Some(tag.clone());
footnotes.push(Event::Start(Tag::FootnoteDefinition(tag)));
}
Event::End(TagEnd::FootnoteDefinition) => {
let tag =
in_footnote.take().expect("end footnote without start");
debug!("ending footnote, last event = {:?}", footnotes.last());
assert_eq!(
footnotes.last(),
Some(&Event::End(TagEnd::Paragraph))
);
footnotes.insert(footnotes.len() - 1, Event::Html(format!(r##"<a href="#fnref:{tag}" class="footnote-backref">↩</a>"##).into()));
footnotes.push(Event::End(TagEnd::FootnoteDefinition));
}
e => {
if in_footnote.is_some() {
footnotes.push(e)
} else {
return Some(e);
}
}
}
}
None => {
*self = Self::Finishing {
footnotes: std::mem::take(footnotes).into_iter(),
};
return Some(Event::Rule);
}
}
}
CollectFootnotes::Finishing { footnotes } => return footnotes.next(),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pulldown_cmark::{Options, Parser};
#[test]
fn test_collect_footnotes() {
let input = r##"
This is a footnote[^1].
[^1]: this is the footnote text
The footnote should come after this.
"##;
let events = Parser::new_ext(input, Options::ENABLE_FOOTNOTES);
let events = collect_footnotes(events);
assert!(matches!(
events.last(),
Some(Event::End(TagEnd::FootnoteDefinition))
));
}
}