Skip to content

Commit 456821b

Browse files
authored
Rollup merge of #131945 - aDotInTheVoid:footnote-time, r=notriddle
rustdoc: Clean up footnote handling Best reviewed commit by commit. Extracts footnote handling logic into it's own file (first commit) and then makes that file slightly nicer to read/understand. No functional changes, but lays the groundwork for making more changes to footnotes (eg #131901, #131946)
2 parents 63fccf0 + 9435afc commit 456821b

File tree

2 files changed

+118
-79
lines changed

2 files changed

+118
-79
lines changed

src/librustdoc/html/markdown.rs

+5-79
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use std::sync::OnceLock;
3737
use pulldown_cmark::{
3838
BrokenLink, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag, TagEnd, html,
3939
};
40-
use rustc_data_structures::fx::{FxHashMap, FxIndexMap};
40+
use rustc_data_structures::fx::FxHashMap;
4141
use rustc_errors::{Diag, DiagMessage};
4242
use rustc_hir::def_id::LocalDefId;
4343
use rustc_middle::ty::TyCtxt;
@@ -57,6 +57,7 @@ use crate::html::length_limit::HtmlWithLimit;
5757
use crate::html::render::small_url_encode;
5858
use crate::html::toc::{Toc, TocBuilder};
5959

60+
mod footnotes;
6061
#[cfg(test)]
6162
mod tests;
6263

@@ -646,81 +647,6 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for SummaryLine<'a, I> {
646647
}
647648
}
648649

649-
/// Moves all footnote definitions to the end and add back links to the
650-
/// references.
651-
struct Footnotes<'a, I> {
652-
inner: I,
653-
footnotes: FxIndexMap<String, (Vec<Event<'a>>, u16)>,
654-
}
655-
656-
impl<'a, I> Footnotes<'a, I> {
657-
fn new(iter: I) -> Self {
658-
Footnotes { inner: iter, footnotes: FxIndexMap::default() }
659-
}
660-
661-
fn get_entry(&mut self, key: &str) -> &mut (Vec<Event<'a>>, u16) {
662-
let new_id = self.footnotes.len() + 1;
663-
let key = key.to_owned();
664-
self.footnotes.entry(key).or_insert((Vec::new(), new_id as u16))
665-
}
666-
}
667-
668-
impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, I> {
669-
type Item = SpannedEvent<'a>;
670-
671-
fn next(&mut self) -> Option<Self::Item> {
672-
loop {
673-
match self.inner.next() {
674-
Some((Event::FootnoteReference(ref reference), range)) => {
675-
let entry = self.get_entry(reference);
676-
let reference = format!(
677-
"<sup id=\"fnref{0}\"><a href=\"#fn{0}\">{0}</a></sup>",
678-
(*entry).1
679-
);
680-
return Some((Event::Html(reference.into()), range));
681-
}
682-
Some((Event::Start(Tag::FootnoteDefinition(def)), _)) => {
683-
let mut content = Vec::new();
684-
for (event, _) in &mut self.inner {
685-
if let Event::End(TagEnd::FootnoteDefinition) = event {
686-
break;
687-
}
688-
content.push(event);
689-
}
690-
let entry = self.get_entry(&def);
691-
(*entry).0 = content;
692-
}
693-
Some(e) => return Some(e),
694-
None => {
695-
if !self.footnotes.is_empty() {
696-
let mut v: Vec<_> = self.footnotes.drain(..).map(|(_, x)| x).collect();
697-
v.sort_by(|a, b| a.1.cmp(&b.1));
698-
let mut ret = String::from("<div class=\"footnotes\"><hr><ol>");
699-
for (mut content, id) in v {
700-
write!(ret, "<li id=\"fn{id}\">").unwrap();
701-
let mut is_paragraph = false;
702-
if let Some(&Event::End(TagEnd::Paragraph)) = content.last() {
703-
content.pop();
704-
is_paragraph = true;
705-
}
706-
html::push_html(&mut ret, content.into_iter());
707-
write!(ret, "&nbsp;<a href=\"#fnref{id}\">↩</a>").unwrap();
708-
if is_paragraph {
709-
ret.push_str("</p>");
710-
}
711-
ret.push_str("</li>");
712-
}
713-
ret.push_str("</ol></div>");
714-
return Some((Event::Html(ret.into()), 0..0));
715-
} else {
716-
return None;
717-
}
718-
}
719-
}
720-
}
721-
}
722-
}
723-
724650
/// A newtype that represents a relative line number in Markdown.
725651
///
726652
/// In other words, this represents an offset from the first line of Markdown
@@ -1408,7 +1334,7 @@ impl Markdown<'_> {
14081334
let mut s = String::with_capacity(md.len() * 3 / 2);
14091335

14101336
let p = HeadingLinks::new(p, None, ids, heading_offset);
1411-
let p = Footnotes::new(p);
1337+
let p = footnotes::Footnotes::new(p);
14121338
let p = LinkReplacer::new(p.map(|(ev, _)| ev), links);
14131339
let p = TableWrapper::new(p);
14141340
let p = CodeBlocks::new(p, codes, edition, playground);
@@ -1443,7 +1369,7 @@ impl MarkdownWithToc<'_> {
14431369

14441370
{
14451371
let p = HeadingLinks::new(p, Some(&mut toc), ids, HeadingOffset::H1);
1446-
let p = Footnotes::new(p);
1372+
let p = footnotes::Footnotes::new(p);
14471373
let p = TableWrapper::new(p.map(|(ev, _)| ev));
14481374
let p = CodeBlocks::new(p, codes, edition, playground);
14491375
html::push_html(&mut s, p);
@@ -1476,7 +1402,7 @@ impl MarkdownItemInfo<'_> {
14761402
let mut s = String::with_capacity(md.len() * 3 / 2);
14771403

14781404
let p = HeadingLinks::new(p, None, ids, HeadingOffset::H1);
1479-
let p = Footnotes::new(p);
1405+
let p = footnotes::Footnotes::new(p);
14801406
let p = TableWrapper::new(p.map(|(ev, _)| ev));
14811407
let p = p.filter(|event| {
14821408
!matches!(event, Event::Start(Tag::Paragraph) | Event::End(TagEnd::Paragraph))
+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
//! Markdown footnote handling.
2+
use std::fmt::Write as _;
3+
4+
use pulldown_cmark::{Event, Tag, TagEnd, html};
5+
use rustc_data_structures::fx::FxIndexMap;
6+
7+
use super::SpannedEvent;
8+
9+
/// Moves all footnote definitions to the end and add back links to the
10+
/// references.
11+
pub(super) struct Footnotes<'a, I> {
12+
inner: I,
13+
footnotes: FxIndexMap<String, FootnoteDef<'a>>,
14+
}
15+
16+
/// The definition of a single footnote.
17+
struct FootnoteDef<'a> {
18+
content: Vec<Event<'a>>,
19+
/// The number that appears in the footnote reference and list.
20+
id: u16,
21+
}
22+
23+
impl<'a, I> Footnotes<'a, I> {
24+
pub(super) fn new(iter: I) -> Self {
25+
Footnotes { inner: iter, footnotes: FxIndexMap::default() }
26+
}
27+
28+
fn get_entry(&mut self, key: &str) -> (&mut Vec<Event<'a>>, u16) {
29+
let new_id = self.footnotes.len() + 1;
30+
let key = key.to_owned();
31+
let FootnoteDef { content, id } = self
32+
.footnotes
33+
.entry(key)
34+
.or_insert(FootnoteDef { content: Vec::new(), id: new_id as u16 });
35+
// Don't allow changing the ID of existing entrys, but allow changing the contents.
36+
(content, *id)
37+
}
38+
}
39+
40+
impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, I> {
41+
type Item = SpannedEvent<'a>;
42+
43+
fn next(&mut self) -> Option<Self::Item> {
44+
loop {
45+
match self.inner.next() {
46+
Some((Event::FootnoteReference(ref reference), range)) => {
47+
// When we see a reference (to a footnote we may not know) the definition of,
48+
// reserve a number for it, and emit a link to that number.
49+
let (_, id) = self.get_entry(reference);
50+
let reference =
51+
format!("<sup id=\"fnref{0}\"><a href=\"#fn{0}\">{0}</a></sup>", id);
52+
return Some((Event::Html(reference.into()), range));
53+
}
54+
Some((Event::Start(Tag::FootnoteDefinition(def)), _)) => {
55+
// When we see a footnote definition, collect the assocated content, and store
56+
// that for rendering later.
57+
let content = collect_footnote_def(&mut self.inner);
58+
let (entry_content, _) = self.get_entry(&def);
59+
*entry_content = content;
60+
}
61+
Some(e) => return Some(e),
62+
None => {
63+
if !self.footnotes.is_empty() {
64+
// After all the markdown is emmited, emit an <hr> then all the footnotes
65+
// in a list.
66+
let defs: Vec<_> = self.footnotes.drain(..).map(|(_, x)| x).collect();
67+
let defs_html = render_footnotes_defs(defs);
68+
return Some((Event::Html(defs_html.into()), 0..0));
69+
} else {
70+
return None;
71+
}
72+
}
73+
}
74+
}
75+
}
76+
}
77+
78+
fn collect_footnote_def<'a>(events: impl Iterator<Item = SpannedEvent<'a>>) -> Vec<Event<'a>> {
79+
let mut content = Vec::new();
80+
for (event, _) in events {
81+
if let Event::End(TagEnd::FootnoteDefinition) = event {
82+
break;
83+
}
84+
content.push(event);
85+
}
86+
content
87+
}
88+
89+
fn render_footnotes_defs(mut footnotes: Vec<FootnoteDef<'_>>) -> String {
90+
let mut ret = String::from("<div class=\"footnotes\"><hr><ol>");
91+
92+
// Footnotes must listed in order of id, so the numbers the
93+
// browser generated for <li> are right.
94+
footnotes.sort_by_key(|x| x.id);
95+
96+
for FootnoteDef { mut content, id } in footnotes {
97+
write!(ret, "<li id=\"fn{id}\">").unwrap();
98+
let mut is_paragraph = false;
99+
if let Some(&Event::End(TagEnd::Paragraph)) = content.last() {
100+
content.pop();
101+
is_paragraph = true;
102+
}
103+
html::push_html(&mut ret, content.into_iter());
104+
write!(ret, "&nbsp;<a href=\"#fnref{id}\">↩</a>").unwrap();
105+
if is_paragraph {
106+
ret.push_str("</p>");
107+
}
108+
ret.push_str("</li>");
109+
}
110+
ret.push_str("</ol></div>");
111+
112+
ret
113+
}

0 commit comments

Comments
 (0)