Skip to content

Commit 54d79e7

Browse files
committed
rustdoc: Allow multiple references to a single footnote
Multiple references to a single footnote is not prohibited by rustdoc but causes multiple "sup" elements with the same "id" attribute, which is invalid per the HTML specification. However, such references are helpful on certain cases and actually tested in tests/rustdoc/footnote-reference-in-footnote-def.rs. This commit keeps track of the number of references per footnote and gives unique ID per reference to a footnote 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 f242d6c commit 54d79e7

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)