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. <587a12bb54/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.
This commit is contained in:
@@ -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());
|
||||
if num_refs <= 1 {
|
||||
write!(ret, " <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, " <a href=\"#fnref{id}\">↩ <sup>1</sup></a>").unwrap();
|
||||
for refid in 2..=num_refs {
|
||||
write!(ret, " <sup><a href=\"#fnref{id}-{refid}\">{refid}</a></sup>").unwrap();
|
||||
}
|
||||
}
|
||||
if is_paragraph {
|
||||
ret.push_str("</p>");
|
||||
}
|
||||
|
||||
23
tests/rustdoc/footnote-reference-ids.rs
Normal file
23
tests/rustdoc/footnote-reference-ids.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
// This test ensures that multiple references to a single footnote and
|
||||
// corresponding back links work as expected.
|
||||
|
||||
#![crate_name = "foo"]
|
||||
|
||||
//@ has 'foo/index.html'
|
||||
//@ has - '//*[@class="docblock"]/p/sup[@id="fnref1"]/a[@href="#fn1"]' '1'
|
||||
//@ has - '//*[@class="docblock"]/p/sup[@id="fnref2"]/a[@href="#fn2"]' '2'
|
||||
//@ has - '//*[@class="docblock"]/p/sup[@id="fnref2-2"]/a[@href="#fn2"]' '2'
|
||||
//@ has - '//li[@id="fn1"]/p' 'meow'
|
||||
//@ has - '//li[@id="fn1"]/p/a[@href="#fnref1"]' '↩'
|
||||
//@ has - '//li[@id="fn2"]/p' 'uwu'
|
||||
//@ has - '//li[@id="fn2"]/p/a[@href="#fnref2"]/sup' '1'
|
||||
//@ has - '//li[@id="fn2"]/p/sup/a[@href="#fnref2-2"]' '2'
|
||||
|
||||
//! # Footnote, references and back links
|
||||
//!
|
||||
//! Single: [^a].
|
||||
//!
|
||||
//! Double: [^b] [^b].
|
||||
//!
|
||||
//! [^a]: meow
|
||||
//! [^b]: uwu
|
||||
@@ -9,7 +9,7 @@
|
||||
//@ has - '//li[@id="fn1"]/p/sup[@id="fnref2"]/a[@href="#fn2"]' '2'
|
||||
//@ has - '//li[@id="fn1"]//a[@href="#fn2"]' '2'
|
||||
//@ has - '//li[@id="fn2"]/p' 'uwu'
|
||||
//@ has - '//li[@id="fn2"]/p/sup[@id="fnref1"]/a[@href="#fn1"]' '1'
|
||||
//@ has - '//li[@id="fn2"]/p/sup[@id="fnref1-2"]/a[@href="#fn1"]' '1'
|
||||
//@ has - '//li[@id="fn2"]//a[@href="#fn1"]' '1'
|
||||
|
||||
//! # footnote-hell
|
||||
|
||||
Reference in New Issue
Block a user