Skip to content

Commit ed3dba9

Browse files
committed
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 causes 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.
1 parent 7295b08 commit ed3dba9

File tree

3 files changed

+51
-10
lines changed

3 files changed

+51
-10
lines changed

src/librustdoc/html/markdown/footnotes.rs

+27-9
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ struct FootnoteDef<'a> {
2323
content: Vec<Event<'a>>,
2424
/// The number that appears in the footnote reference and list.
2525
id: usize,
26+
/// The number of footnote references.
27+
num_refs: usize,
2628
}
2729

2830
impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Footnotes<'a, I> {
@@ -33,21 +35,28 @@ impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Footnotes<'a, I> {
3335
Footnotes { inner: iter, footnotes: FxIndexMap::default(), existing_footnotes, start_id }
3436
}
3537

36-
fn get_entry(&mut self, key: &str) -> (&mut Vec<Event<'a>>, usize) {
38+
fn get_entry(&mut self, key: &str) -> (&mut Vec<Event<'a>>, usize, &mut usize) {
3739
let new_id = self.footnotes.len() + 1 + self.start_id;
3840
let key = key.to_owned();
39-
let FootnoteDef { content, id } =
40-
self.footnotes.entry(key).or_insert(FootnoteDef { content: Vec::new(), id: new_id });
41+
let FootnoteDef { content, id, num_refs } = self
42+
.footnotes
43+
.entry(key)
44+
.or_insert(FootnoteDef { content: Vec::new(), id: new_id, num_refs: 0 });
4145
// Don't allow changing the ID of existing entrys, but allow changing the contents.
42-
(content, *id)
46+
(content, *id, num_refs)
4347
}
4448

4549
fn handle_footnote_reference(&mut self, reference: &CowStr<'a>) -> Event<'a> {
4650
// When we see a reference (to a footnote we may not know) the definition of,
4751
// reserve a number for it, and emit a link to that number.
48-
let (_, id) = self.get_entry(reference);
52+
let (_, id, num_refs) = self.get_entry(reference);
53+
*num_refs += 1;
54+
let fnref_suffix = {
55+
let num_refs = *num_refs;
56+
if num_refs <= 1 { "".to_owned() } else { format!("-{num_refs}") }
57+
};
4958
let reference = format!(
50-
"<sup id=\"fnref{0}\"><a href=\"#fn{0}\">{1}</a></sup>",
59+
"<sup id=\"fnref{0}{fnref_suffix}\"><a href=\"#fn{0}\">{1}</a></sup>",
5160
id,
5261
// Although the ID count is for the whole page, the footnote reference
5362
// are local to the item so we make this ID "local" when displayed.
@@ -85,7 +94,7 @@ impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, I> {
8594
// When we see a footnote definition, collect the assocated content, and store
8695
// that for rendering later.
8796
let content = self.collect_footnote_def();
88-
let (entry_content, _) = self.get_entry(&def);
97+
let (entry_content, _, _) = self.get_entry(&def);
8998
*entry_content = content;
9099
}
91100
Some(e) => return Some(e),
@@ -113,15 +122,24 @@ fn render_footnotes_defs(mut footnotes: Vec<FootnoteDef<'_>>) -> String {
113122
// browser generated for <li> are right.
114123
footnotes.sort_by_key(|x| x.id);
115124

116-
for FootnoteDef { mut content, id } in footnotes {
125+
for FootnoteDef { mut content, id, num_refs } in footnotes {
117126
write!(ret, "<li id=\"fn{id}\">").unwrap();
118127
let mut is_paragraph = false;
119128
if let Some(&Event::End(TagEnd::Paragraph)) = content.last() {
120129
content.pop();
121130
is_paragraph = true;
122131
}
123132
html::push_html(&mut ret, content.into_iter());
124-
write!(ret, "&nbsp;<a href=\"#fnref{id}\">↩</a>").unwrap();
133+
if num_refs <= 1 {
134+
write!(ret, "&nbsp;<a href=\"#fnref{id}\">↩</a>").unwrap();
135+
} else {
136+
// There are multiple references to single footnote. Make the first
137+
// back link a single "a" element to make touch region larger.
138+
write!(ret, "&nbsp;<a href=\"#fnref{id}\">↩&nbsp;<sup>1</sup></a>").unwrap();
139+
for refid in 2..=num_refs {
140+
write!(ret, "&nbsp;<sup><a href=\"#fnref{id}-{refid}\">{refid}</a></sup>").unwrap();
141+
}
142+
}
125143
if is_paragraph {
126144
ret.push_str("</p>");
127145
}
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// This test ensures that multiple references to a single footnote and
2+
// corresponding back links work as expected.
3+
4+
#![crate_name = "foo"]
5+
6+
//@ has 'foo/index.html'
7+
//@ has - '//*[@class="docblock"]/p/sup[@id="fnref1"]/a[@href="#fn1"]' '1'
8+
//@ has - '//*[@class="docblock"]/p/sup[@id="fnref2"]/a[@href="#fn2"]' '2'
9+
//@ has - '//*[@class="docblock"]/p/sup[@id="fnref2-2"]/a[@href="#fn2"]' '2'
10+
//@ has - '//li[@id="fn1"]/p' 'meow'
11+
//@ has - '//li[@id="fn1"]/p/a[@href="#fnref1"]' '↩'
12+
//@ has - '//li[@id="fn2"]/p' 'uwu'
13+
//@ has - '//li[@id="fn2"]/p/a[@href="#fnref2"]/sup' '1'
14+
//@ has - '//li[@id="fn2"]/p/sup/a[@href="#fnref2-2"]' '2'
15+
16+
//! # Footnote, references and back links
17+
//!
18+
//! Single: [^a].
19+
//!
20+
//! Double: [^b] [^b].
21+
//!
22+
//! [^a]: meow
23+
//! [^b]: uwu

tests/rustdoc/footnote-reference-in-footnote-def.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//@ has - '//li[@id="fn1"]/p/sup[@id="fnref2"]/a[@href="#fn2"]' '2'
1010
//@ has - '//li[@id="fn1"]//a[@href="#fn2"]' '2'
1111
//@ has - '//li[@id="fn2"]/p' 'uwu'
12-
//@ has - '//li[@id="fn2"]/p/sup[@id="fnref1"]/a[@href="#fn1"]' '1'
12+
//@ has - '//li[@id="fn2"]/p/sup[@id="fnref1-2"]/a[@href="#fn1"]' '1'
1313
//@ has - '//li[@id="fn2"]//a[@href="#fn1"]' '1'
1414

1515
//! # footnote-hell

0 commit comments

Comments
 (0)