rustdoc/html/markdown/
footnotes.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//! Markdown footnote handling.

use std::fmt::Write as _;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Weak};

use pulldown_cmark::{CowStr, 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>>,
    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,
}

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) {
        let new_id = self.footnotes.len() + 1 + self.start_id;
        let key = key.to_owned();
        let FootnoteDef { content, id } =
            self.footnotes.entry(key).or_insert(FootnoteDef { content: Vec::new(), id: new_id });
        // Don't allow changing the ID of existing entrys, but allow changing the contents.
        (content, *id)
    }

    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) = self.get_entry(reference);
        let reference = format!(
            "<sup id=\"fnref{0}\"><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 assocated 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 emmited, 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 } 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
}