rustdoc/html/
toc.rs

1//! Table-of-contents creation.
2use crate::html::escape::Escape;
3
4/// A (recursive) table of contents
5#[derive(Debug, PartialEq)]
6pub(crate) struct Toc {
7    /// The levels are strictly decreasing, i.e.
8    ///
9    /// `entries[0].level >= entries[1].level >= ...`
10    ///
11    /// Normally they are equal, but can differ in cases like A and B,
12    /// both of which end up in the same `Toc` as they have the same
13    /// parent (Main).
14    ///
15    /// ```text
16    /// # Main
17    /// ### A
18    /// ## B
19    /// ```
20    pub(crate) entries: Vec<TocEntry>,
21}
22
23impl Toc {
24    fn count_entries_with_level(&self, level: u32) -> usize {
25        self.entries.iter().filter(|e| e.level == level).count()
26    }
27}
28
29#[derive(Debug, PartialEq)]
30pub(crate) struct TocEntry {
31    pub(crate) level: u32,
32    pub(crate) sec_number: String,
33    // name is a plain text header that works in a `title` tag
34    // html includes `<code>` tags
35    // the tooltip is used so that, when a toc is truncated,
36    // you can mouse over it to see the whole thing
37    pub(crate) name: String,
38    pub(crate) html: String,
39    pub(crate) id: String,
40    pub(crate) children: Toc,
41}
42
43/// Progressive construction of a table of contents.
44#[derive(PartialEq)]
45pub(crate) struct TocBuilder {
46    top_level: Toc,
47    /// The current hierarchy of parent headings, the levels are
48    /// strictly increasing (i.e., `chain[0].level < chain[1].level <
49    /// ...`) with each entry being the most recent occurrence of a
50    /// heading with that level (it doesn't include the most recent
51    /// occurrences of every level, just, if it *is* in `chain` then
52    /// it is the most recent one).
53    ///
54    /// We also have `chain[0].level <= top_level.entries[last]`.
55    chain: Vec<TocEntry>,
56}
57
58impl TocBuilder {
59    pub(crate) fn new() -> TocBuilder {
60        TocBuilder { top_level: Toc { entries: Vec::new() }, chain: Vec::new() }
61    }
62
63    /// Converts into a true `Toc` struct.
64    pub(crate) fn into_toc(mut self) -> Toc {
65        // we know all levels are >= 1.
66        self.fold_until(0);
67        self.top_level
68    }
69
70    /// Collapse the chain until the first heading more important than
71    /// `level` (i.e., lower level)
72    ///
73    /// Example:
74    ///
75    /// ```text
76    /// ## A
77    /// # B
78    /// # C
79    /// ## D
80    /// ## E
81    /// ### F
82    /// #### G
83    /// ### H
84    /// ```
85    ///
86    /// If we are considering H (i.e., level 3), then A and B are in
87    /// self.top_level, D is in C.children, and C, E, F, G are in
88    /// self.chain.
89    ///
90    /// When we attempt to push H, we realize that first G is not the
91    /// parent (level is too high) so it is popped from chain and put
92    /// into F.children, then F isn't the parent (level is equal, aka
93    /// sibling), so it's also popped and put into E.children.
94    ///
95    /// This leaves us looking at E, which does have a smaller level,
96    /// and, by construction, it's the most recent thing with smaller
97    /// level, i.e., it's the immediate parent of H.
98    fn fold_until(&mut self, level: u32) {
99        let mut this = None;
100        loop {
101            match self.chain.pop() {
102                Some(mut next) => {
103                    next.children.entries.extend(this);
104                    if next.level < level {
105                        // this is the parent we want, so return it to
106                        // its rightful place.
107                        self.chain.push(next);
108                        return;
109                    } else {
110                        this = Some(next);
111                    }
112                }
113                None => {
114                    self.top_level.entries.extend(this);
115                    return;
116                }
117            }
118        }
119    }
120
121    /// Push a level `level` heading into the appropriate place in the
122    /// hierarchy, returning a string containing the section number in
123    /// `<num>.<num>.<num>` format.
124    pub(crate) fn push(&mut self, level: u32, name: String, html: String, id: String) -> &str {
125        assert!(level >= 1);
126
127        // collapse all previous sections into their parents until we
128        // get to relevant heading (i.e., the first one with a smaller
129        // level than us)
130        self.fold_until(level);
131
132        let mut sec_number;
133        {
134            let (toc_level, toc) = match self.chain.last() {
135                None => {
136                    sec_number = String::new();
137                    (0, &self.top_level)
138                }
139                Some(entry) => {
140                    sec_number = entry.sec_number.clone();
141                    sec_number.push('.');
142                    (entry.level, &entry.children)
143                }
144            };
145            // fill in any missing zeros, e.g., for
146            // # Foo (1)
147            // ### Bar (1.0.1)
148            for _ in toc_level..level - 1 {
149                sec_number.push_str("0.");
150            }
151            let number = toc.count_entries_with_level(level);
152            sec_number.push_str(&(number + 1).to_string())
153        }
154
155        self.chain.push(TocEntry {
156            level,
157            name,
158            html,
159            sec_number,
160            id,
161            children: Toc { entries: Vec::new() },
162        });
163
164        // get the thing we just pushed, so we can borrow the string
165        // out of it with the right lifetime
166        let just_inserted = self.chain.last_mut().unwrap();
167        &just_inserted.sec_number
168    }
169}
170
171impl Toc {
172    fn print_inner(&self, v: &mut String) {
173        use std::fmt::Write as _;
174
175        v.push_str("<ul>");
176        for entry in &self.entries {
177            // recursively format this table of contents
178            let _ = write!(
179                v,
180                "\n<li><a href=\"#{id}\" title=\"{name}\">{num} {html}</a>",
181                id = entry.id,
182                num = entry.sec_number,
183                name = Escape(&entry.name),
184                html = &entry.html,
185            );
186            entry.children.print_inner(&mut *v);
187            v.push_str("</li>");
188        }
189        v.push_str("</ul>");
190    }
191    pub(crate) fn print(&self) -> String {
192        let mut v = String::new();
193        self.print_inner(&mut v);
194        v
195    }
196}
197
198#[cfg(test)]
199mod tests;