rustdoc/html/markdown/
footnotes.rs

1//! Markdown footnote handling.
2
3use std::fmt::Write as _;
4use std::sync::atomic::{AtomicUsize, Ordering};
5use std::sync::{Arc, Weak};
6
7use pulldown_cmark::{CowStr, Event, Tag, TagEnd, html};
8use rustc_data_structures::fx::FxIndexMap;
9
10use super::SpannedEvent;
11
12/// Moves all footnote definitions to the end and add back links to the
13/// references.
14pub(super) struct Footnotes<'a, I> {
15    inner: I,
16    footnotes: FxIndexMap<String, FootnoteDef<'a>>,
17    existing_footnotes: Arc<AtomicUsize>,
18    start_id: usize,
19}
20
21/// The definition of a single footnote.
22struct FootnoteDef<'a> {
23    content: Vec<Event<'a>>,
24    /// The number that appears in the footnote reference and list.
25    id: usize,
26    /// The number of footnote references.
27    num_refs: usize,
28}
29
30impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Footnotes<'a, I> {
31    pub(super) fn new(iter: I, existing_footnotes: Weak<AtomicUsize>) -> Self {
32        let existing_footnotes =
33            existing_footnotes.upgrade().expect("`existing_footnotes` was dropped");
34        let start_id = existing_footnotes.load(Ordering::Relaxed);
35        Footnotes { inner: iter, footnotes: FxIndexMap::default(), existing_footnotes, start_id }
36    }
37
38    fn get_entry(&mut self, key: &str) -> (&mut Vec<Event<'a>>, usize, &mut usize) {
39        let new_id = self.footnotes.len() + 1 + self.start_id;
40        let key = key.to_owned();
41        let FootnoteDef { content, id, num_refs } = self
42            .footnotes
43            .entry(key)
44            .or_insert(FootnoteDef { content: Vec::new(), id: new_id, num_refs: 0 });
45        // Don't allow changing the ID of existing entries, but allow changing the contents.
46        (content, *id, num_refs)
47    }
48
49    fn handle_footnote_reference(&mut self, reference: &CowStr<'a>) -> Event<'a> {
50        // When we see a reference (to a footnote we may not know) the definition of,
51        // reserve a number for it, and emit a link to that number.
52        let (_, id, num_refs) = self.get_entry(reference);
53        *num_refs += 1;
54        let fnref_suffix = if *num_refs <= 1 { "".to_owned() } else { format!("-{num_refs}") };
55        let reference = format!(
56            "<sup id=\"fnref{0}{fnref_suffix}\"><a href=\"#fn{0}\">{1}</a></sup>",
57            id,
58            // Although the ID count is for the whole page, the footnote reference
59            // are local to the item so we make this ID "local" when displayed.
60            id - self.start_id
61        );
62        Event::Html(reference.into())
63    }
64
65    fn collect_footnote_def(&mut self) -> Vec<Event<'a>> {
66        let mut content = Vec::new();
67        while let Some((event, _)) = self.inner.next() {
68            match event {
69                Event::End(TagEnd::FootnoteDefinition) => break,
70                Event::FootnoteReference(ref reference) => {
71                    content.push(self.handle_footnote_reference(reference));
72                }
73                event => content.push(event),
74            }
75        }
76        content
77    }
78}
79
80impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, I> {
81    type Item = SpannedEvent<'a>;
82
83    fn next(&mut self) -> Option<Self::Item> {
84        loop {
85            let next = self.inner.next();
86            match next {
87                Some((Event::FootnoteReference(ref reference), range)) => {
88                    return Some((self.handle_footnote_reference(reference), range));
89                }
90                Some((Event::Start(Tag::FootnoteDefinition(def)), _)) => {
91                    // When we see a footnote definition, collect the associated content, and store
92                    // that for rendering later.
93                    let content = self.collect_footnote_def();
94                    let (entry_content, _, _) = self.get_entry(&def);
95                    *entry_content = content;
96                }
97                Some(e) => return Some(e),
98                None => {
99                    if !self.footnotes.is_empty() {
100                        // After all the markdown is emitted, emit an <hr> then all the footnotes
101                        // in a list.
102                        let defs: Vec<_> = self.footnotes.drain(..).map(|(_, x)| x).collect();
103                        self.existing_footnotes.fetch_add(defs.len(), Ordering::Relaxed);
104                        let defs_html = render_footnotes_defs(defs);
105                        return Some((Event::Html(defs_html.into()), 0..0));
106                    } else {
107                        return None;
108                    }
109                }
110            }
111        }
112    }
113}
114
115fn render_footnotes_defs(mut footnotes: Vec<FootnoteDef<'_>>) -> String {
116    let mut ret = String::from("<div class=\"footnotes\"><hr><ol>");
117
118    // Footnotes must listed in order of id, so the numbers the
119    // browser generated for <li> are right.
120    footnotes.sort_by_key(|x| x.id);
121
122    for FootnoteDef { mut content, id, num_refs } in footnotes {
123        write!(ret, "<li id=\"fn{id}\">").unwrap();
124        let mut is_paragraph = false;
125        if let Some(&Event::End(TagEnd::Paragraph)) = content.last() {
126            content.pop();
127            is_paragraph = true;
128        }
129        html::push_html(&mut ret, content.into_iter());
130        if num_refs <= 1 {
131            write!(ret, "&nbsp;<a href=\"#fnref{id}\">↩</a>").unwrap();
132        } else {
133            // There are multiple references to single footnote. Make the first
134            // back link a single "a" element to make touch region larger.
135            write!(ret, "&nbsp;<a href=\"#fnref{id}\">↩&nbsp;<sup>1</sup></a>").unwrap();
136            for refid in 2..=num_refs {
137                write!(ret, "&nbsp;<sup><a href=\"#fnref{id}-{refid}\">{refid}</a></sup>").unwrap();
138            }
139        }
140        if is_paragraph {
141            ret.push_str("</p>");
142        }
143        ret.push_str("</li>");
144    }
145    ret.push_str("</ol></div>");
146
147    ret
148}