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)
This commit is contained in:
Stuart Cook 2024-10-20 14:06:05 +11:00 committed by GitHub
commit 456821be88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 118 additions and 79 deletions

View File

@ -37,7 +37,7 @@ use std::sync::OnceLock;
use pulldown_cmark::{
BrokenLink, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag, TagEnd, html,
};
use rustc_data_structures::fx::{FxHashMap, FxIndexMap};
use rustc_data_structures::fx::FxHashMap;
use rustc_errors::{Diag, DiagMessage};
use rustc_hir::def_id::LocalDefId;
use rustc_middle::ty::TyCtxt;
@ -57,6 +57,7 @@ use crate::html::length_limit::HtmlWithLimit;
use crate::html::render::small_url_encode;
use crate::html::toc::{Toc, TocBuilder};
mod footnotes;
#[cfg(test)]
mod tests;
@ -646,81 +647,6 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for SummaryLine<'a, I> {
}
}
/// Moves all footnote definitions to the end and add back links to the
/// references.
struct Footnotes<'a, I> {
inner: I,
footnotes: FxIndexMap<String, (Vec<Event<'a>>, u16)>,
}
impl<'a, I> Footnotes<'a, I> {
fn new(iter: I) -> Self {
Footnotes { inner: iter, footnotes: FxIndexMap::default() }
}
fn get_entry(&mut self, key: &str) -> &mut (Vec<Event<'a>>, u16) {
let new_id = self.footnotes.len() + 1;
let key = key.to_owned();
self.footnotes.entry(key).or_insert((Vec::new(), new_id as u16))
}
}
impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, I> {
type Item = SpannedEvent<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.inner.next() {
Some((Event::FootnoteReference(ref reference), range)) => {
let entry = self.get_entry(reference);
let reference = format!(
"<sup id=\"fnref{0}\"><a href=\"#fn{0}\">{0}</a></sup>",
(*entry).1
);
return Some((Event::Html(reference.into()), range));
}
Some((Event::Start(Tag::FootnoteDefinition(def)), _)) => {
let mut content = Vec::new();
for (event, _) in &mut self.inner {
if let Event::End(TagEnd::FootnoteDefinition) = event {
break;
}
content.push(event);
}
let entry = self.get_entry(&def);
(*entry).0 = content;
}
Some(e) => return Some(e),
None => {
if !self.footnotes.is_empty() {
let mut v: Vec<_> = self.footnotes.drain(..).map(|(_, x)| x).collect();
v.sort_by(|a, b| a.1.cmp(&b.1));
let mut ret = String::from("<div class=\"footnotes\"><hr><ol>");
for (mut content, id) in v {
write!(ret, "<li id=\"fn{id}\">").unwrap();
let mut is_paragraph = false;
if let Some(&Event::End(TagEnd::Paragraph)) = content.last() {
content.pop();
is_paragraph = true;
}
html::push_html(&mut ret, content.into_iter());
write!(ret, "&nbsp;<a href=\"#fnref{id}\">↩</a>").unwrap();
if is_paragraph {
ret.push_str("</p>");
}
ret.push_str("</li>");
}
ret.push_str("</ol></div>");
return Some((Event::Html(ret.into()), 0..0));
} else {
return None;
}
}
}
}
}
}
/// A newtype that represents a relative line number in Markdown.
///
/// In other words, this represents an offset from the first line of Markdown
@ -1408,7 +1334,7 @@ impl Markdown<'_> {
let mut s = String::with_capacity(md.len() * 3 / 2);
let p = HeadingLinks::new(p, None, ids, heading_offset);
let p = Footnotes::new(p);
let p = footnotes::Footnotes::new(p);
let p = LinkReplacer::new(p.map(|(ev, _)| ev), links);
let p = TableWrapper::new(p);
let p = CodeBlocks::new(p, codes, edition, playground);
@ -1443,7 +1369,7 @@ impl MarkdownWithToc<'_> {
{
let p = HeadingLinks::new(p, Some(&mut toc), ids, HeadingOffset::H1);
let p = Footnotes::new(p);
let p = footnotes::Footnotes::new(p);
let p = TableWrapper::new(p.map(|(ev, _)| ev));
let p = CodeBlocks::new(p, codes, edition, playground);
html::push_html(&mut s, p);
@ -1476,7 +1402,7 @@ impl MarkdownItemInfo<'_> {
let mut s = String::with_capacity(md.len() * 3 / 2);
let p = HeadingLinks::new(p, None, ids, HeadingOffset::H1);
let p = Footnotes::new(p);
let p = footnotes::Footnotes::new(p);
let p = TableWrapper::new(p.map(|(ev, _)| ev));
let p = p.filter(|event| {
!matches!(event, Event::Start(Tag::Paragraph) | Event::End(TagEnd::Paragraph))

View File

@ -0,0 +1,113 @@
//! Markdown footnote handling.
use std::fmt::Write as _;
use pulldown_cmark::{Event, Tag, TagEnd, html};
use rustc_data_structures::fx::FxIndexMap;
use super::SpannedEvent;
/// Moves all footnote definitions to the end and add back links to the
/// references.
pub(super) struct Footnotes<'a, I> {
inner: I,
footnotes: FxIndexMap<String, FootnoteDef<'a>>,
}
/// The definition of a single footnote.
struct FootnoteDef<'a> {
content: Vec<Event<'a>>,
/// The number that appears in the footnote reference and list.
id: u16,
}
impl<'a, I> Footnotes<'a, I> {
pub(super) fn new(iter: I) -> Self {
Footnotes { inner: iter, footnotes: FxIndexMap::default() }
}
fn get_entry(&mut self, key: &str) -> (&mut Vec<Event<'a>>, u16) {
let new_id = self.footnotes.len() + 1;
let key = key.to_owned();
let FootnoteDef { content, id } = self
.footnotes
.entry(key)
.or_insert(FootnoteDef { content: Vec::new(), id: new_id as u16 });
// Don't allow changing the ID of existing entrys, but allow changing the contents.
(content, *id)
}
}
impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, I> {
type Item = SpannedEvent<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.inner.next() {
Some((Event::FootnoteReference(ref reference), range)) => {
// 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 reference =
format!("<sup id=\"fnref{0}\"><a href=\"#fn{0}\">{0}</a></sup>", id);
return Some((Event::Html(reference.into()), range));
}
Some((Event::Start(Tag::FootnoteDefinition(def)), _)) => {
// When we see a footnote definition, collect the assocated content, and store
// that for rendering later.
let content = collect_footnote_def(&mut self.inner);
let (entry_content, _) = self.get_entry(&def);
*entry_content = content;
}
Some(e) => return Some(e),
None => {
if !self.footnotes.is_empty() {
// After all the markdown is emmited, emit an <hr> then all the footnotes
// in a list.
let defs: Vec<_> = self.footnotes.drain(..).map(|(_, x)| x).collect();
let defs_html = render_footnotes_defs(defs);
return Some((Event::Html(defs_html.into()), 0..0));
} else {
return None;
}
}
}
}
}
}
fn collect_footnote_def<'a>(events: impl Iterator<Item = SpannedEvent<'a>>) -> Vec<Event<'a>> {
let mut content = Vec::new();
for (event, _) in events {
if let Event::End(TagEnd::FootnoteDefinition) = event {
break;
}
content.push(event);
}
content
}
fn render_footnotes_defs(mut footnotes: Vec<FootnoteDef<'_>>) -> String {
let mut ret = String::from("<div class=\"footnotes\"><hr><ol>");
// Footnotes must listed in order of id, so the numbers the
// browser generated for <li> are right.
footnotes.sort_by_key(|x| x.id);
for FootnoteDef { mut content, id } in footnotes {
write!(ret, "<li id=\"fn{id}\">").unwrap();
let mut is_paragraph = false;
if let Some(&Event::End(TagEnd::Paragraph)) = content.last() {
content.pop();
is_paragraph = true;
}
html::push_html(&mut ret, content.into_iter());
write!(ret, "&nbsp;<a href=\"#fnref{id}\">↩</a>").unwrap();
if is_paragraph {
ret.push_str("</p>");
}
ret.push_str("</li>");
}
ret.push_str("</ol></div>");
ret
}