footnote_rewrite/footnote-rewrite.rs
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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
use std::collections::HashMap;
use std::fmt::Write as _;
use std::io::Write as _;
use pulldown_cmark::{html, CowStr, Event, Options, Parser, Tag, TagEnd};
/// This example shows how to do footnotes as bottom-notes, in the style of GitHub.
fn main() {
let markdown_input: &str = "This is an [^a] footnote [^a].\n\n[^a]: footnote contents";
println!("Parsing the following markdown string:\n{}", markdown_input);
// To generate this style, you have to collect the footnotes at the end, while parsing.
// You also need to count usages.
let mut footnotes = Vec::new();
let mut in_footnote = Vec::new();
let mut footnote_numbers = HashMap::new();
// ENABLE_FOOTNOTES is used in this example, but ENABLE_OLD_FOOTNOTES would work, too.
let parser = Parser::new_ext(markdown_input, Options::ENABLE_FOOTNOTES)
.filter_map(|event| {
match event {
Event::Start(Tag::FootnoteDefinition(_)) => {
in_footnote.push(vec![event]);
None
}
Event::End(TagEnd::FootnoteDefinition) => {
let mut f = in_footnote.pop().unwrap();
f.push(event);
footnotes.push(f);
None
}
Event::FootnoteReference(name) => {
let n = footnote_numbers.len() + 1;
let (n, nr) = footnote_numbers.entry(name.clone()).or_insert((n, 0usize));
*nr += 1;
let html = Event::Html(format!(r##"<sup class="footnote-reference" id="fr-{name}-{nr}"><a href="#fn-{name}">[{n}]</a></sup>"##).into());
if in_footnote.is_empty() {
Some(html)
} else {
in_footnote.last_mut().unwrap().push(html);
None
}
}
_ if !in_footnote.is_empty() => {
in_footnote.last_mut().unwrap().push(event);
None
}
_ => Some(event),
}
});
// Write to anything implementing the `Write` trait. This could also be a file
// or network socket.
let stdout = std::io::stdout();
let mut handle = stdout.lock();
handle.write_all(b"\nHTML output:\n").unwrap();
html::write_html_io(&mut handle, parser).unwrap();
// To make the footnotes look right, we need to sort them by their appearance order, not by
// the in-tree order of their actual definitions. Unused items are omitted entirely.
//
// For example, this code:
//
// test [^1] [^2]
// [^2]: second used, first defined
// [^1]: test
//
// Gets rendered like *this* if you copy it into a GitHub comment box:
//
// <p>test <sup>[1]</sup> <sup>[2]</sup></p>
// <hr>
// <ol>
// <li>test ↩</li>
// <li>second used, first defined ↩</li>
// </ol>
if !footnotes.is_empty() {
footnotes.retain(|f| match f.first() {
Some(Event::Start(Tag::FootnoteDefinition(name))) => {
footnote_numbers.get(name).unwrap_or(&(0, 0)).1 != 0
}
_ => false,
});
footnotes.sort_by_cached_key(|f| match f.first() {
Some(Event::Start(Tag::FootnoteDefinition(name))) => {
footnote_numbers.get(name).unwrap_or(&(0, 0)).0
}
_ => unreachable!(),
});
handle
.write_all(b"<hr><ol class=\"footnotes-list\">\n")
.unwrap();
html::write_html_io(
&mut handle,
footnotes.into_iter().flat_map(|fl| {
// To write backrefs, the name needs kept until the end of the footnote definition.
let mut name = CowStr::from("");
// Backrefs are included in the final paragraph of the footnote, if it's normal text.
// For example, this DOM can be produced:
//
// Markdown:
//
// five [^feet].
//
// [^feet]:
// A foot is defined, in this case, as 0.3048 m.
//
// Historically, the foot has not been defined this way, corresponding to many
// subtly different units depending on the location.
//
// HTML:
//
// <p>five <sup class="footnote-reference" id="fr-feet-1"><a href="#fn-feet">[1]</a></sup>.</p>
//
// <ol class="footnotes-list">
// <li id="fn-feet">
// <p>A foot is defined, in this case, as 0.3048 m.</p>
// <p>Historically, the foot has not been defined this way, corresponding to many
// subtly different units depending on the location. <a href="#fr-feet-1">↩</a></p>
// </li>
// </ol>
//
// This is mostly a visual hack, so that footnotes use less vertical space.
//
// If there is no final paragraph, such as a tabular, list, or image footnote, it gets
// pushed after the last tag instead.
let mut has_written_backrefs = false;
let fl_len = fl.len();
let footnote_numbers = &footnote_numbers;
fl.into_iter().enumerate().map(move |(i, f)| match f {
Event::Start(Tag::FootnoteDefinition(current_name)) => {
name = current_name;
has_written_backrefs = false;
Event::Html(format!(r##"<li id="fn-{name}">"##).into())
}
Event::End(TagEnd::FootnoteDefinition) | Event::End(TagEnd::Paragraph)
if !has_written_backrefs && i >= fl_len - 2 =>
{
let usage_count = footnote_numbers.get(&name).unwrap().1;
let mut end = String::with_capacity(
name.len() + (r##" <a href="#fr--1">↩</a></li>"##.len() * usage_count),
);
for usage in 1..=usage_count {
if usage == 1 {
write!(&mut end, r##" <a href="#fr-{name}-{usage}">↩</a>"##)
.unwrap();
} else {
write!(&mut end, r##" <a href="#fr-{name}-{usage}">↩{usage}</a>"##)
.unwrap();
}
}
has_written_backrefs = true;
if f == Event::End(TagEnd::FootnoteDefinition) {
end.push_str("</li>\n");
} else {
end.push_str("</p>\n");
}
Event::Html(end.into())
}
Event::End(TagEnd::FootnoteDefinition) => Event::Html("</li>\n".into()),
Event::FootnoteReference(_) => unreachable!("converted to HTML earlier"),
f => f,
})
}),
)
.unwrap();
handle.write_all(b"</ol>\n").unwrap();
}
}