diff options
| author | Guillaume Gomez <guillaume1.gomez@gmail.com> | 2025-08-14 11:39:32 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-08-14 11:39:32 +0200 |
| commit | f676dd8729ed30b3fddaa409683bd09e33bca6f1 (patch) | |
| tree | 5885540baa4032946c8c94f21ea2e127e4ee3f6f | |
| parent | 2c1ac85679678dfe5cce7ea8037735b0349ceaf3 (diff) | |
| parent | 74aca53f5591f9ad4a5e74c42eb101534c3e7b12 (diff) | |
| download | rust-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.
| -rw-r--r-- | src/librustdoc/html/markdown/footnotes.rs | 33 | ||||
| -rw-r--r-- | tests/rustdoc/footnote-reference-ids.rs | 23 | ||||
| -rw-r--r-- | tests/rustdoc/footnote-reference-in-footnote-def.rs | 2 |
3 files changed, 48 insertions, 10 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, " <a href=\"#fnref{id}\">↩</a>").unwrap(); + 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>"); } diff --git a/tests/rustdoc/footnote-reference-ids.rs b/tests/rustdoc/footnote-reference-ids.rs new file mode 100644 index 00000000000..ffa04e1d767 --- /dev/null +++ b/tests/rustdoc/footnote-reference-ids.rs @@ -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 diff --git a/tests/rustdoc/footnote-reference-in-footnote-def.rs b/tests/rustdoc/footnote-reference-in-footnote-def.rs index db3f9a59ef8..504d0bdb8f7 100644 --- a/tests/rustdoc/footnote-reference-in-footnote-def.rs +++ b/tests/rustdoc/footnote-reference-in-footnote-def.rs @@ -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 |
