diff --git a/Cargo.lock b/Cargo.lock
index 299e8daa53..3fb221bcef 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1139,7 +1139,7 @@ dependencies = [
[[package]]
name = "mdbook"
-version = "0.4.42"
+version = "0.4.43"
dependencies = [
"ammonia",
"anyhow",
diff --git a/Cargo.toml b/Cargo.toml
index 5d593b2700..04063a45a6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,7 +3,7 @@ members = [".", "examples/remove-emphasis/mdbook-remove-emphasis"]
[package]
name = "mdbook"
-version = "0.4.42"
+version = "0.4.43"
authors = [
"Mathieu David ",
"Michael-F-Bryan ",
diff --git a/guide/src/format/configuration/renderers.md b/guide/src/format/configuration/renderers.md
index 5efede6606..84d615d205 100644
--- a/guide/src/format/configuration/renderers.md
+++ b/guide/src/format/configuration/renderers.md
@@ -98,6 +98,7 @@ theme = "my-theme"
default-theme = "light"
preferred-dark-theme = "navy"
smart-punctuation = true
+footnote-backrefs = true
mathjax-support = false
copy-fonts = true
additional-css = ["custom.css", "custom2.css"]
@@ -126,6 +127,7 @@ The following configuration options are available:
See [Smart Punctuation](../markdown.md#smart-punctuation).
Defaults to `false`.
- **curly-quotes:** Deprecated alias for `smart-punctuation`.
+- **footnote-backrefs:** Add backreference links to footnote definitions.
- **mathjax-support:** Adds support for [MathJax](../mathjax.md). Defaults to
`false`.
- **copy-fonts:** (**Deprecated**) If `true` (the default), mdBook uses its built-in fonts which are copied to the output directory.
diff --git a/guide/src/format/markdown.md b/guide/src/format/markdown.md
index f837d54c9c..cbe4d5a0fb 100644
--- a/guide/src/format/markdown.md
+++ b/guide/src/format/markdown.md
@@ -221,6 +221,13 @@ To enable it, see the [`output.html.smart-punctuation`] config option.
[task list extension]: https://github.github.com/gfm/#task-list-items-extension-
[`output.html.smart-punctuation`]: configuration/renderers.md#html-renderer-options
+### Footnote backreference links
+
+Add backreference links to footnote definitions.
+
+This feature is disabled by default.
+To enable it, see the [`output.html.footnote-backrefs`] config option.
+
### Heading attributes
Headings can have a custom HTML ID and classes. This lets you maintain the same ID even if you change the heading's text, it also lets you add multiple classes in the heading.
diff --git a/src/config.rs b/src/config.rs
index b87ad27644..94ca1e5b2f 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -531,6 +531,8 @@ pub struct HtmlConfig {
pub preferred_dark_theme: Option,
/// Supports smart quotes, apostrophes, ellipsis, en-dash, and em-dash.
pub smart_punctuation: bool,
+ /// Add backreference links to footnote definitions.
+ pub footnote_backrefs: bool,
/// Deprecated alias for `smart_punctuation`.
pub curly_quotes: bool,
/// Should mathjax be enabled?
@@ -596,6 +598,7 @@ impl Default for HtmlConfig {
default_theme: None,
preferred_dark_theme: None,
smart_punctuation: false,
+ footnote_backrefs: false,
curly_quotes: false,
mathjax_support: false,
copy_fonts: true,
diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs
index d0149fb523..1ac01094fa 100644
--- a/src/renderer/html_handlebars/hbs_renderer.rs
+++ b/src/renderer/html_handlebars/hbs_renderer.rs
@@ -54,11 +54,16 @@ impl HtmlHandlebars {
.insert("git_repository_edit_url".to_owned(), json!(edit_url));
}
- let content = utils::render_markdown(&ch.content, ctx.html_config.smart_punctuation());
+ let content = utils::render_markdown(
+ &ch.content,
+ ctx.html_config.smart_punctuation(),
+ ctx.html_config.footnote_backrefs,
+ );
let fixed_content = utils::render_markdown_with_path(
&ch.content,
ctx.html_config.smart_punctuation(),
+ ctx.html_config.footnote_backrefs,
Some(path),
);
if !ctx.is_index && ctx.html_config.print.page_break {
@@ -167,8 +172,11 @@ impl HtmlHandlebars {
.to_string()
}
};
- let html_content_404 =
- utils::render_markdown(&content_404, html_config.smart_punctuation());
+ let html_content_404 = utils::render_markdown(
+ &content_404,
+ html_config.smart_punctuation(),
+ html_config.footnote_backrefs,
+ );
let mut data_404 = data.clone();
let base_url = if let Some(site_url) = &html_config.site_url {
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
index a53f79c0e9..b9f2912e75 100644
--- a/src/utils/mod.rs
+++ b/src/utils/mod.rs
@@ -190,8 +190,8 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
}
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
-pub fn render_markdown(text: &str, smart_punctuation: bool) -> String {
- render_markdown_with_path(text, smart_punctuation, None)
+pub fn render_markdown(text: &str, smart_punctuation: bool, footnote_backrefs: bool) -> String {
+ render_markdown_with_path(text, smart_punctuation, footnote_backrefs, None)
}
pub fn new_cmark_parser(text: &str, smart_punctuation: bool) -> Parser<'_> {
@@ -210,11 +210,16 @@ pub fn new_cmark_parser(text: &str, smart_punctuation: bool) -> Parser<'_> {
pub fn render_markdown_with_path(
text: &str,
smart_punctuation: bool,
+ footnote_backrefs: bool,
path: Option<&Path>,
) -> String {
- let mut s = String::with_capacity(text.len() * 3 / 2);
- let p = new_cmark_parser(text, smart_punctuation);
- let events = p
+ if footnote_backrefs {
+ return render_markdown_with_path_with_footnote_backrefs(text, smart_punctuation, path);
+ }
+
+ let mut body = String::with_capacity(text.len() * 3 / 2);
+ let parser = new_cmark_parser(text, smart_punctuation);
+ let events = parser
.map(clean_codeblock_headers)
.map(|event| adjust_links(event, path))
.flat_map(|event| {
@@ -222,8 +227,137 @@ pub fn render_markdown_with_path(
a.into_iter().chain(b)
});
- html::push_html(&mut s, events);
- s
+ html::push_html(&mut body, events);
+ body
+}
+
+pub fn render_markdown_with_path_with_footnote_backrefs(
+ text: &str,
+ smart_punctuation: bool,
+ path: Option<&Path>,
+) -> String {
+ let mut body = String::with_capacity(text.len() * 3 / 2);
+
+ // Based on
+ // https://github.com/pulldown-cmark/pulldown-cmark/blob/master/pulldown-cmark/examples/footnote-rewrite.rs
+
+ // 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();
+
+ let parser = new_cmark_parser(text, smart_punctuation)
+ .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##""##).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),
+ }
+ });
+
+ let events = parser
+ .map(clean_codeblock_headers)
+ .map(|event| adjust_links(event, path))
+ .flat_map(|event| {
+ let (a, b) = wrap_tables(event);
+ a.into_iter().chain(b)
+ });
+
+ html::push_html(&mut body, events);
+
+ // 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.
+ 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!(),
+ });
+
+ body.push_str(""##.len() * usage_count),
+ );
+ for usage in 1..=usage_count {
+ if usage == 1 {
+ end.push_str(&format!(r##" ↩"##));
+ } else {
+ end.push_str(&format!(r##" ↩{usage}"##));
+ }
+ }
+ has_written_backrefs = true;
+ if f == Event::End(TagEnd::FootnoteDefinition) {
+ end.push_str("\n");
+ } else {
+ end.push_str("
\n");
+ }
+ Event::Html(end.into())
+ }
+ Event::End(TagEnd::FootnoteDefinition) => Event::Html("\n".into()),
+ Event::FootnoteReference(_) => unreachable!("converted to HTML earlier"),
+ f => f,
+ })
+ }),
+ );
+
+ // Closing div.footnotes
+ body.push_str("\n");
+ }
+
+ body
}
/// Wraps tables in a `.table-wrapper` class to apply overflow-x rules to.
@@ -310,7 +444,7 @@ mod tests {
#[test]
fn preserves_external_links() {
assert_eq!(
- render_markdown("[example](https://www.rust-lang.org/)", false),
+ render_markdown("[example](https://www.rust-lang.org/)", false, false),
"example
\n"
);
}
@@ -318,17 +452,17 @@ mod tests {
#[test]
fn it_can_adjust_markdown_links() {
assert_eq!(
- render_markdown("[example](example.md)", false),
+ render_markdown("[example](example.md)", false, false),
"example
\n"
);
assert_eq!(
- render_markdown("[example_anchor](example.md#anchor)", false),
+ render_markdown("[example_anchor](example.md#anchor)", false, false),
"example_anchor
\n"
);
// this anchor contains 'md' inside of it
assert_eq!(
- render_markdown("[phantom data](foo.html#phantomdata)", false),
+ render_markdown("[phantom data](foo.html#phantomdata)", false, false),
"phantom data
\n"
);
}
@@ -346,12 +480,12 @@ mod tests {
"#.trim();
- assert_eq!(render_markdown(src, false), out);
+ assert_eq!(render_markdown(src, false, false), out);
}
#[test]
fn it_can_keep_quotes_straight() {
- assert_eq!(render_markdown("'one'", false), "'one'
\n");
+ assert_eq!(render_markdown("'one'", false, false), "'one'
\n");
}
#[test]
@@ -367,7 +501,7 @@ mod tests {
'three'
‘four’
"#;
- assert_eq!(render_markdown(input, true), expected);
+ assert_eq!(render_markdown(input, true, false), expected);
}
#[test]
@@ -389,8 +523,8 @@ more text with spaces
more text with spaces
"#;
- assert_eq!(render_markdown(input, false), expected);
- assert_eq!(render_markdown(input, true), expected);
+ assert_eq!(render_markdown(input, false, false), expected);
+ assert_eq!(render_markdown(input, true, false), expected);
}
#[test]
@@ -402,8 +536,8 @@ more text with spaces
let expected = r#"
"#;
- assert_eq!(render_markdown(input, false), expected);
- assert_eq!(render_markdown(input, true), expected);
+ assert_eq!(render_markdown(input, false, false), expected);
+ assert_eq!(render_markdown(input, true, false), expected);
}
#[test]
@@ -415,8 +549,8 @@ more text with spaces
let expected = r#"
"#;
- assert_eq!(render_markdown(input, false), expected);
- assert_eq!(render_markdown(input, true), expected);
+ assert_eq!(render_markdown(input, false, false), expected);
+ assert_eq!(render_markdown(input, true, false), expected);
}
#[test]
@@ -428,15 +562,15 @@ more text with spaces
let expected = r#"
"#;
- assert_eq!(render_markdown(input, false), expected);
- assert_eq!(render_markdown(input, true), expected);
+ assert_eq!(render_markdown(input, false, false), expected);
+ assert_eq!(render_markdown(input, true, false), expected);
let input = r#"
```rust
```
"#;
- assert_eq!(render_markdown(input, false), expected);
- assert_eq!(render_markdown(input, true), expected);
+ assert_eq!(render_markdown(input, false, false), expected);
+ assert_eq!(render_markdown(input, true, false), expected);
}
}