about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorGuillaume Gomez <guillaume1.gomez@gmail.com>2025-08-14 11:39:32 +0200
committerGitHub <noreply@github.com>2025-08-14 11:39:32 +0200
commitf676dd8729ed30b3fddaa409683bd09e33bca6f1 (patch)
tree5885540baa4032946c8c94f21ea2e127e4ee3f6f /src
parent2c1ac85679678dfe5cce7ea8037735b0349ceaf3 (diff)
parent74aca53f5591f9ad4a5e74c42eb101534c3e7b12 (diff)
downloadrust-f676dd8729ed30b3fddaa409683bd09e33bca6f1.tar.gz
rust-f676dd8729ed30b3fddaa409683bd09e33bca6f1.zip
Rollup merge of #140434 - a4lg:rustdoc-multi-footnote-refs, r=fmease,GuillaumeGomez
rustdoc: Allow multiple references to a single footnote

Multiple references to a single footnote is a part of GitHub Flavored Markdown syntax (although not explicitly documented as well as regular footnotes, it is implemented in GitHub's fork of CommonMark) and not prohibited by rustdoc.

cf. <https://github.com/github/cmark-gfm/blob/587a12bb54d95ac37241377e6ddc93ea0e45439b/test/extensions.txt#L762-L780>

However, using it makes multiple `sup` elements with the same `id` attribute, which is invalid per the HTML specification.

Still, not only this is a valid GitHub Flavored Markdown syntax, this is helpful on certain cases and actually tested (accidentally) in `tests/rustdoc/footnote-reference-in-footnote-def.rs`.

This commit keeps track of the number of references per footnote and gives unique ID to each reference.
It also emits *all* back links from a footnote to its references as "↩" (return symbol) plus a numeric list in superscript.

As a known limitation, it assumes that all references to a footnote are rendered (this is not always true if a dangling footnote has one or more references but considered a reasonable compromise).

Also note that, this commit is designed so that no HTML changes will occur unless multiple references to a single footnote is actually used.
Diffstat (limited to 'src')
-rw-r--r--src/librustdoc/html/markdown/footnotes.rs33
1 files changed, 24 insertions, 9 deletions
diff --git a/src/librustdoc/html/markdown/footnotes.rs b/src/librustdoc/html/markdown/footnotes.rs
index 7ee012c4da2..a81d8dd6035 100644
--- a/src/librustdoc/html/markdown/footnotes.rs
+++ b/src/librustdoc/html/markdown/footnotes.rs
@@ -23,6 +23,8 @@ struct FootnoteDef<'a> {
     content: Vec<Event<'a>>,
     /// The number that appears in the footnote reference and list.
     id: usize,
+    /// The number of footnote references.
+    num_refs: usize,
 }
 
 impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Footnotes<'a, I> {
@@ -33,21 +35,25 @@ impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Footnotes<'a, I> {
         Footnotes { inner: iter, footnotes: FxIndexMap::default(), existing_footnotes, start_id }
     }
 
-    fn get_entry(&mut self, key: &str) -> (&mut Vec<Event<'a>>, usize) {
+    fn get_entry(&mut self, key: &str) -> (&mut Vec<Event<'a>>, usize, &mut usize) {
         let new_id = self.footnotes.len() + 1 + self.start_id;
         let key = key.to_owned();
-        let FootnoteDef { content, id } =
-            self.footnotes.entry(key).or_insert(FootnoteDef { content: Vec::new(), id: new_id });
+        let FootnoteDef { content, id, num_refs } = self
+            .footnotes
+            .entry(key)
+            .or_insert(FootnoteDef { content: Vec::new(), id: new_id, num_refs: 0 });
         // Don't allow changing the ID of existing entries, but allow changing the contents.
-        (content, *id)
+        (content, *id, num_refs)
     }
 
     fn handle_footnote_reference(&mut self, reference: &CowStr<'a>) -> Event<'a> {
         // When we see a reference (to a footnote we may not know) the definition of,
         // reserve a number for it, and emit a link to that number.
-        let (_, id) = self.get_entry(reference);
+        let (_, id, num_refs) = self.get_entry(reference);
+        *num_refs += 1;
+        let fnref_suffix = if *num_refs <= 1 { "".to_owned() } else { format!("-{num_refs}") };
         let reference = format!(
-            "<sup id=\"fnref{0}\"><a href=\"#fn{0}\">{1}</a></sup>",
+            "<sup id=\"fnref{0}{fnref_suffix}\"><a href=\"#fn{0}\">{1}</a></sup>",
             id,
             // Although the ID count is for the whole page, the footnote reference
             // are local to the item so we make this ID "local" when displayed.
@@ -85,7 +91,7 @@ impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, I> {
                     // When we see a footnote definition, collect the associated content, and store
                     // that for rendering later.
                     let content = self.collect_footnote_def();
-                    let (entry_content, _) = self.get_entry(&def);
+                    let (entry_content, _, _) = self.get_entry(&def);
                     *entry_content = content;
                 }
                 Some(e) => return Some(e),
@@ -113,7 +119,7 @@ fn render_footnotes_defs(mut footnotes: Vec<FootnoteDef<'_>>) -> String {
     // browser generated for <li> are right.
     footnotes.sort_by_key(|x| x.id);
 
-    for FootnoteDef { mut content, id } in footnotes {
+    for FootnoteDef { mut content, id, num_refs } in footnotes {
         write!(ret, "<li id=\"fn{id}\">").unwrap();
         let mut is_paragraph = false;
         if let Some(&Event::End(TagEnd::Paragraph)) = content.last() {
@@ -121,7 +127,16 @@ fn render_footnotes_defs(mut footnotes: Vec<FootnoteDef<'_>>) -> String {
             is_paragraph = true;
         }
         html::push_html(&mut ret, content.into_iter());
-        write!(ret, "&nbsp;<a href=\"#fnref{id}\">↩</a>").unwrap();
+        if num_refs <= 1 {
+            write!(ret, "&nbsp;<a href=\"#fnref{id}\">↩</a>").unwrap();
+        } else {
+            // There are multiple references to single footnote. Make the first
+            // back link a single "a" element to make touch region larger.
+            write!(ret, "&nbsp;<a href=\"#fnref{id}\">↩&nbsp;<sup>1</sup></a>").unwrap();
+            for refid in 2..=num_refs {
+                write!(ret, "&nbsp;<sup><a href=\"#fnref{id}-{refid}\">{refid}</a></sup>").unwrap();
+            }
+        }
         if is_paragraph {
             ret.push_str("</p>");
         }