rustdoc/html/markdown/
footnotes.rs1use 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
12pub(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
21struct FootnoteDef<'a> {
23 content: Vec<Event<'a>>,
24 id: usize,
26 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 (content, *id, num_refs)
47 }
48
49 fn handle_footnote_reference(&mut self, reference: &CowStr<'a>) -> Event<'a> {
50 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 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 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 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.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, " <a href=\"#fnref{id}\">↩</a>").unwrap();
132 } else {
133 write!(ret, " <a href=\"#fnref{id}\">↩ <sup>1</sup></a>").unwrap();
136 for refid in 2..=num_refs {
137 write!(ret, " <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}