diff --git a/components/config/src/config/markup.rs b/components/config/src/config/markup.rs
index b1ac7fa22f..580a665ae6 100644
--- a/components/config/src/config/markup.rs
+++ b/components/config/src/config/markup.rs
@@ -43,6 +43,8 @@ pub struct Markdown {
pub external_links_no_referrer: bool,
/// Whether smart punctuation is enabled (changing quotes, dashes, dots etc in their typographic form)
pub smart_punctuation: bool,
+ /// Whether footnotes are rendered at the bottom in the style of GitHub.
+ pub bottom_footnotes: bool,
/// A list of directories to search for additional `.sublime-syntax` and `.tmTheme` files in.
pub extra_syntaxes_and_themes: Vec,
/// The compiled extra syntaxes into a syntax set
@@ -203,6 +205,7 @@ impl Default for Markdown {
external_links_no_follow: false,
external_links_no_referrer: false,
smart_punctuation: false,
+ bottom_footnotes: false,
extra_syntaxes_and_themes: vec![],
extra_syntax_set: None,
extra_theme_set: Arc::new(None),
diff --git a/components/markdown/src/markdown.rs b/components/markdown/src/markdown.rs
index 40c94afac5..a8565176c8 100644
--- a/components/markdown/src/markdown.rs
+++ b/components/markdown/src/markdown.rs
@@ -1,5 +1,7 @@
+use std::collections::HashMap;
use std::fmt::Write;
+use crate::markdown::cmark::CowStr;
use errors::bail;
use libs::gh_emoji::Replacer as EmojiReplacer;
use libs::once_cell::sync::Lazy;
@@ -239,6 +241,158 @@ fn get_heading_refs(events: &[Event]) -> Vec {
heading_refs
}
+fn convert_footnotes_to_github_style(old_events: &mut Vec) {
+ let events = std::mem::take(old_events);
+ // step 1: We need to extract footnotes from the event stream and tweak footnote references
+
+ // footnotes bodies are stored in a stack of vectors, because it is possible to have footnotes
+ // inside footnotes
+ let mut footnote_bodies_stack = Vec::new();
+ let mut footnotes = Vec::new();
+ // this will allow to create a multiple back references
+ let mut footnote_numbers = HashMap::new();
+ let filtered_events = events.into_iter().filter_map(|event| {
+ match event {
+ // New footnote definition is pushed to the stack
+ Event::Start(Tag::FootnoteDefinition(_)) => {
+ footnote_bodies_stack.push(vec![event]);
+ None
+ }
+ // The topmost footnote definition is popped from the stack
+ Event::End(TagEnd::FootnoteDefinition) => {
+ // unwrap will never fail, because Tag::FootnoteDefinition always comes before
+ // TagEnd::FootnoteDefinition
+ let mut footnote_body = footnote_bodies_stack.pop().unwrap();
+ footnote_body.push(event);
+ footnotes.push(footnote_body);
+ None
+ }
+ Event::FootnoteReference(name) => {
+ // n will be a unique index of the footnote
+ let n = footnote_numbers.len() + 1;
+ // nr is a number of references to this footnote
+ let (n, nr) = footnote_numbers.entry(name.clone()).or_insert((n, 0usize));
+ *nr += 1;
+ let reference = Event::Html(format!(r##""##).into());
+
+ if footnote_bodies_stack.is_empty() {
+ // we are in the main text, just output the reference
+ Some(reference)
+ } else {
+ // we are inside other footnote, we have to push that reference into that
+ // footnote
+ footnote_bodies_stack.last_mut().unwrap().push(reference);
+ None
+ }
+ }
+ _ if !footnote_bodies_stack.is_empty() => {
+ footnote_bodies_stack.last_mut().unwrap().push(event);
+ None
+ }
+ _ => Some(event),
+ }
+ }
+ );
+
+ old_events.extend(filtered_events);
+
+ if footnotes.is_empty() {
+ return;
+ }
+
+ old_events.push(Event::Html("
\n");
+ }
+ Event::Html(end.into())
+ }
+ Event::End(TagEnd::FootnoteDefinition) => Event::Html("\n".into()),
+ Event::FootnoteReference(_) => unreachable!("converted to HTML earlier"),
+ f => f,
+ })
+ });
+
+ old_events.extend(footnotes);
+ old_events.push(Event::Html("\n".into()));
+}
+
pub fn markdown_to_html(
content: &str,
context: &RenderContext,
@@ -623,6 +777,10 @@ pub fn markdown_to_html(
insert_many(&mut events, anchors_to_insert);
}
+ if context.config.markdown.bottom_footnotes {
+ convert_footnotes_to_github_style(&mut events);
+ }
+
cmark::html::push_html(&mut html, events.into_iter());
}
@@ -641,11 +799,11 @@ pub fn markdown_to_html(
#[cfg(test)]
mod tests {
+ use super::*;
use config::Config;
+ use insta::assert_snapshot;
- use super::*;
#[test]
-
fn insert_many_works() {
let mut v = vec![1, 2, 3, 4, 5];
insert_many(&mut v, vec![(0, 0), (2, -1), (5, 6)]);
@@ -714,4 +872,106 @@ mod tests {
assert_eq!(body, &bottom_rendered);
}
}
+
+ #[test]
+ fn no_footnotes() {
+ let mut opts = Options::empty();
+ opts.insert(Options::ENABLE_TABLES);
+ opts.insert(Options::ENABLE_FOOTNOTES);
+ opts.insert(Options::ENABLE_STRIKETHROUGH);
+ opts.insert(Options::ENABLE_TASKLISTS);
+ opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
+
+ let content = "Some text *without* footnotes.\n\nOnly ~~fancy~~ formatting.";
+ let mut events: Vec<_> = Parser::new_ext(&content, opts).collect();
+ convert_footnotes_to_github_style(&mut events);
+ let mut html = String::new();
+ cmark::html::push_html(&mut html, events.into_iter());
+ assert_snapshot!(html);
+ }
+
+ #[test]
+ fn single_footnote() {
+ let mut opts = Options::empty();
+ opts.insert(Options::ENABLE_TABLES);
+ opts.insert(Options::ENABLE_FOOTNOTES);
+ opts.insert(Options::ENABLE_STRIKETHROUGH);
+ opts.insert(Options::ENABLE_TASKLISTS);
+ opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
+
+ let content = "This text has a footnote[^1]\n [^1]:But it is meaningless.";
+ let mut events: Vec<_> = Parser::new_ext(&content, opts).collect();
+ convert_footnotes_to_github_style(&mut events);
+ let mut html = String::new();
+ cmark::html::push_html(&mut html, events.into_iter());
+ assert_snapshot!(html);
+ }
+
+ #[test]
+ fn reordered_footnotes() {
+ let mut opts = Options::empty();
+ opts.insert(Options::ENABLE_TABLES);
+ opts.insert(Options::ENABLE_FOOTNOTES);
+ opts.insert(Options::ENABLE_STRIKETHROUGH);
+ opts.insert(Options::ENABLE_TASKLISTS);
+ opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
+
+ let content = "This text has two[^2] footnotes[^1]\n[^1]: not sorted.\n[^2]: But they are";
+ let mut events: Vec<_> = Parser::new_ext(&content, opts).collect();
+ convert_footnotes_to_github_style(&mut events);
+ let mut html = String::new();
+ cmark::html::push_html(&mut html, events.into_iter());
+ assert_snapshot!(html);
+ }
+
+ #[test]
+ fn def_before_use() {
+ let mut opts = Options::empty();
+ opts.insert(Options::ENABLE_TABLES);
+ opts.insert(Options::ENABLE_FOOTNOTES);
+ opts.insert(Options::ENABLE_STRIKETHROUGH);
+ opts.insert(Options::ENABLE_TASKLISTS);
+ opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
+
+ let content = "[^1]:It's before the reference.\n\n There is footnote definition?[^1]";
+ let mut events: Vec<_> = Parser::new_ext(&content, opts).collect();
+ convert_footnotes_to_github_style(&mut events);
+ let mut html = String::new();
+ cmark::html::push_html(&mut html, events.into_iter());
+ assert_snapshot!(html);
+ }
+
+ #[test]
+ fn multiple_refs() {
+ let mut opts = Options::empty();
+ opts.insert(Options::ENABLE_TABLES);
+ opts.insert(Options::ENABLE_FOOTNOTES);
+ opts.insert(Options::ENABLE_STRIKETHROUGH);
+ opts.insert(Options::ENABLE_TASKLISTS);
+ opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
+
+ let content = "This text has two[^1] identical footnotes[^1]\n[^1]: So one is present.\n[^2]: But another in not.";
+ let mut events: Vec<_> = Parser::new_ext(&content, opts).collect();
+ convert_footnotes_to_github_style(&mut events);
+ let mut html = String::new();
+ cmark::html::push_html(&mut html, events.into_iter());
+ assert_snapshot!(html);
+ }
+
+ #[test]
+ fn footnote_inside_footnote() {
+ let mut opts = Options::empty();
+ opts.insert(Options::ENABLE_TABLES);
+ opts.insert(Options::ENABLE_FOOTNOTES);
+ opts.insert(Options::ENABLE_STRIKETHROUGH);
+ opts.insert(Options::ENABLE_TASKLISTS);
+ opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
+
+ let content = "This text has a footnote[^1]\n[^1]: But the footnote has another footnote[^2].\n[^2]: That's it.";
+ let mut events: Vec<_> = Parser::new_ext(&content, opts).collect();
+ convert_footnotes_to_github_style(&mut events);
+ let mut html = String::new();
+ cmark::html::push_html(&mut html, events.into_iter());
+ assert_snapshot!(html);
+ }
}
diff --git a/components/markdown/src/snapshots/markdown__markdown__tests__def_before_use.snap b/components/markdown/src/snapshots/markdown__markdown__tests__def_before_use.snap
new file mode 100644
index 0000000000..57e3a922ad
--- /dev/null
+++ b/components/markdown/src/snapshots/markdown__markdown__tests__def_before_use.snap
@@ -0,0 +1,10 @@
+---
+source: components/markdown/src/markdown.rs
+expression: html
+---
+There is footnote definition?
+
diff --git a/components/markdown/src/snapshots/markdown__markdown__tests__footnote_inside_footnote.snap b/components/markdown/src/snapshots/markdown__markdown__tests__footnote_inside_footnote.snap
new file mode 100644
index 0000000000..6b5e28d476
--- /dev/null
+++ b/components/markdown/src/snapshots/markdown__markdown__tests__footnote_inside_footnote.snap
@@ -0,0 +1,13 @@
+---
+source: components/markdown/src/markdown.rs
+expression: html
+---
+This text has a footnote
+
diff --git a/components/markdown/src/snapshots/markdown__markdown__tests__multiple_refs.snap b/components/markdown/src/snapshots/markdown__markdown__tests__multiple_refs.snap
new file mode 100644
index 0000000000..1f7eaff186
--- /dev/null
+++ b/components/markdown/src/snapshots/markdown__markdown__tests__multiple_refs.snap
@@ -0,0 +1,10 @@
+---
+source: components/markdown/src/markdown.rs
+expression: html
+---
+This text has two identical footnotes
+
diff --git a/components/markdown/src/snapshots/markdown__markdown__tests__no_footnotes.snap b/components/markdown/src/snapshots/markdown__markdown__tests__no_footnotes.snap
new file mode 100644
index 0000000000..f9cc7730d8
--- /dev/null
+++ b/components/markdown/src/snapshots/markdown__markdown__tests__no_footnotes.snap
@@ -0,0 +1,6 @@
+---
+source: components/markdown/src/markdown.rs
+expression: html
+---
+Some text without footnotes.
+Only fancy formatting.
diff --git a/components/markdown/src/snapshots/markdown__markdown__tests__reordered_footnotes.snap b/components/markdown/src/snapshots/markdown__markdown__tests__reordered_footnotes.snap
new file mode 100644
index 0000000000..865a344e5c
--- /dev/null
+++ b/components/markdown/src/snapshots/markdown__markdown__tests__reordered_footnotes.snap
@@ -0,0 +1,13 @@
+---
+source: components/markdown/src/markdown.rs
+expression: html
+---
+This text has two footnotes
+
diff --git a/components/markdown/src/snapshots/markdown__markdown__tests__single_footnote.snap b/components/markdown/src/snapshots/markdown__markdown__tests__single_footnote.snap
new file mode 100644
index 0000000000..34a7f2d3e6
--- /dev/null
+++ b/components/markdown/src/snapshots/markdown__markdown__tests__single_footnote.snap
@@ -0,0 +1,10 @@
+---
+source: components/markdown/src/markdown.rs
+expression: html
+---
+This text has a footnote
+
diff --git a/components/markdown/tests/markdown.rs b/components/markdown/tests/markdown.rs
index a12246837b..e8cdcd42c2 100644
--- a/components/markdown/tests/markdown.rs
+++ b/components/markdown/tests/markdown.rs
@@ -355,3 +355,40 @@ and multiple paragraphs.
.body;
insta::assert_snapshot!(body);
}
+
+#[test]
+fn github_style_footnotes() {
+ let mut config = Config::default_for_test();
+ config.markdown.bottom_footnotes = true;
+
+ let markdown = r#"This text has a footnote[^1]
+
+[^1]:But it is meaningless.
+
+This text has two[^3] footnotes[^2].
+
+[^2]: not sorted.
+[^3]: But they are
+
+[^4]:It's before the reference.
+
+There is footnote definition?[^4]
+
+This text has two[^5] identical footnotes[^5]
+[^5]: So one is present.
+[^6]: But another in not.
+
+This text has a footnote[^7]
+
+[^7]: But the footnote has another footnote[^8].
+
+[^8]: That's it.
+
+Footnotes can also be referenced with identifiers[^first].
+
+[^first]: Like this: `[^first]`.
+"#;
+
+ let body = common::render_with_config(&markdown, config).unwrap().body;
+ insta::assert_snapshot!(body);
+}
diff --git a/components/markdown/tests/snapshots/markdown__github_style_footnotes.snap b/components/markdown/tests/snapshots/markdown__github_style_footnotes.snap
new file mode 100644
index 0000000000..de55e63fb7
--- /dev/null
+++ b/components/markdown/tests/snapshots/markdown__github_style_footnotes.snap
@@ -0,0 +1,36 @@
+---
+source: components/markdown/tests/markdown.rs
+expression: body
+---
+This text has a footnote
+This text has two footnotes.
+There is footnote definition?
+This text has two identical footnotes
+This text has a footnote
+Footnotes can also be referenced with identifiers.
+
diff --git a/docs/content/documentation/getting-started/configuration.md b/docs/content/documentation/getting-started/configuration.md
index d8ec482d5d..dce920ce8d 100644
--- a/docs/content/documentation/getting-started/configuration.md
+++ b/docs/content/documentation/getting-started/configuration.md
@@ -135,6 +135,9 @@ smart_punctuation = false
# For example, `![xx](...)` is ok but `![*x*x](...)` isn’t ok
lazy_async_image = false
+# Whether footnotes are rendered in the GitHub-style (at the bottom, with back references) or plain (in the place, where they are defined)
+bottom_footnotes = false
+
# Configuration of the link checker.
[link_checker]
# Skip link checking for external URLs that start with these prefixes