| //! Markdown footnote handling. | |
| use std::fmt::Write as _; | |
| use std::sync::atomic::{AtomicUsize, Ordering}; | |
| use std::sync::{Arc, Weak}; | |
| use rustc_data_structures::fx::FxIndexMap; | |
| use rustc_resolve::rustdoc::pulldown_cmark::{CowStr, Event, Tag, TagEnd, html}; | |
| 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>>, | |
| existing_footnotes: Arc<AtomicUsize>, | |
| start_id: usize, | |
| } | |
| /// The definition of a single footnote. | |
| 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> { | |
| pub(super) fn new(iter: I, existing_footnotes: Weak<AtomicUsize>) -> Self { | |
| let existing_footnotes = | |
| existing_footnotes.upgrade().expect("`existing_footnotes` was dropped"); | |
| let start_id = existing_footnotes.load(Ordering::Relaxed); | |
| Footnotes { inner: iter, footnotes: FxIndexMap::default(), existing_footnotes, start_id } | |
| } | |
| 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, 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, 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, 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}{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. | |
| id - self.start_id | |
| ); | |
| Event::Html(reference.into()) | |
| } | |
| fn collect_footnote_def(&mut self) -> Vec<Event<'a>> { | |
| let mut content = Vec::new(); | |
| while let Some((event, _)) = self.inner.next() { | |
| match event { | |
| Event::End(TagEnd::FootnoteDefinition) => break, | |
| Event::FootnoteReference(ref reference) => { | |
| content.push(self.handle_footnote_reference(reference)); | |
| } | |
| event => content.push(event), | |
| } | |
| } | |
| content | |
| } | |
| } | |
| impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, I> { | |
| type Item = SpannedEvent<'a>; | |
| fn next(&mut self) -> Option<Self::Item> { | |
| loop { | |
| let next = self.inner.next(); | |
| match next { | |
| Some((Event::FootnoteReference(ref reference), range)) => { | |
| return Some((self.handle_footnote_reference(reference), range)); | |
| } | |
| Some((Event::Start(Tag::FootnoteDefinition(def)), _)) => { | |
| // 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); | |
| *entry_content = content; | |
| } | |
| Some(e) => return Some(e), | |
| None => { | |
| if !self.footnotes.is_empty() { | |
| // After all the markdown is emitted, emit an <hr> then all the footnotes | |
| // in a list. | |
| let defs: Vec<_> = self.footnotes.drain(..).map(|(_, x)| x).collect(); | |
| self.existing_footnotes.fetch_add(defs.len(), Ordering::Relaxed); | |
| let defs_html = render_footnotes_defs(defs); | |
| return Some((Event::Html(defs_html.into()), 0..0)); | |
| } else { | |
| return None; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| 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, 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() { | |
| content.pop(); | |
| 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>"); | |
| } | |
| ret.push_str("</li>"); | |
| } | |
| ret.push_str("</ol></div>"); | |
| ret | |
| } |