rustdoc/html/toc.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 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
//! Table-of-contents creation.
use crate::html::escape::Escape;
/// A (recursive) table of contents
#[derive(Debug, PartialEq)]
pub(crate) struct Toc {
/// The levels are strictly decreasing, i.e.
///
/// `entries[0].level >= entries[1].level >= ...`
///
/// Normally they are equal, but can differ in cases like A and B,
/// both of which end up in the same `Toc` as they have the same
/// parent (Main).
///
/// ```text
/// # Main
/// ### A
/// ## B
/// ```
pub(crate) entries: Vec<TocEntry>,
}
impl Toc {
fn count_entries_with_level(&self, level: u32) -> usize {
self.entries.iter().filter(|e| e.level == level).count()
}
}
#[derive(Debug, PartialEq)]
pub(crate) struct TocEntry {
pub(crate) level: u32,
pub(crate) sec_number: String,
// name is a plain text header that works in a `title` tag
// html includes `<code>` tags
// the tooltip is used so that, when a toc is truncated,
// you can mouse over it to see the whole thing
pub(crate) name: String,
pub(crate) html: String,
pub(crate) id: String,
pub(crate) children: Toc,
}
/// Progressive construction of a table of contents.
#[derive(PartialEq)]
pub(crate) struct TocBuilder {
top_level: Toc,
/// The current hierarchy of parent headings, the levels are
/// strictly increasing (i.e., `chain[0].level < chain[1].level <
/// ...`) with each entry being the most recent occurrence of a
/// heading with that level (it doesn't include the most recent
/// occurrences of every level, just, if it *is* in `chain` then
/// it is the most recent one).
///
/// We also have `chain[0].level <= top_level.entries[last]`.
chain: Vec<TocEntry>,
}
impl TocBuilder {
pub(crate) fn new() -> TocBuilder {
TocBuilder { top_level: Toc { entries: Vec::new() }, chain: Vec::new() }
}
/// Converts into a true `Toc` struct.
pub(crate) fn into_toc(mut self) -> Toc {
// we know all levels are >= 1.
self.fold_until(0);
self.top_level
}
/// Collapse the chain until the first heading more important than
/// `level` (i.e., lower level)
///
/// Example:
///
/// ```text
/// ## A
/// # B
/// # C
/// ## D
/// ## E
/// ### F
/// #### G
/// ### H
/// ```
///
/// If we are considering H (i.e., level 3), then A and B are in
/// self.top_level, D is in C.children, and C, E, F, G are in
/// self.chain.
///
/// When we attempt to push H, we realize that first G is not the
/// parent (level is too high) so it is popped from chain and put
/// into F.children, then F isn't the parent (level is equal, aka
/// sibling), so it's also popped and put into E.children.
///
/// This leaves us looking at E, which does have a smaller level,
/// and, by construction, it's the most recent thing with smaller
/// level, i.e., it's the immediate parent of H.
fn fold_until(&mut self, level: u32) {
let mut this = None;
loop {
match self.chain.pop() {
Some(mut next) => {
next.children.entries.extend(this);
if next.level < level {
// this is the parent we want, so return it to
// its rightful place.
self.chain.push(next);
return;
} else {
this = Some(next);
}
}
None => {
self.top_level.entries.extend(this);
return;
}
}
}
}
/// Push a level `level` heading into the appropriate place in the
/// hierarchy, returning a string containing the section number in
/// `<num>.<num>.<num>` format.
pub(crate) fn push(&mut self, level: u32, name: String, html: String, id: String) -> &str {
assert!(level >= 1);
// collapse all previous sections into their parents until we
// get to relevant heading (i.e., the first one with a smaller
// level than us)
self.fold_until(level);
let mut sec_number;
{
let (toc_level, toc) = match self.chain.last() {
None => {
sec_number = String::new();
(0, &self.top_level)
}
Some(entry) => {
sec_number = entry.sec_number.clone();
sec_number.push('.');
(entry.level, &entry.children)
}
};
// fill in any missing zeros, e.g., for
// # Foo (1)
// ### Bar (1.0.1)
for _ in toc_level..level - 1 {
sec_number.push_str("0.");
}
let number = toc.count_entries_with_level(level);
sec_number.push_str(&(number + 1).to_string())
}
self.chain.push(TocEntry {
level,
name,
html,
sec_number,
id,
children: Toc { entries: Vec::new() },
});
// get the thing we just pushed, so we can borrow the string
// out of it with the right lifetime
let just_inserted = self.chain.last_mut().unwrap();
&just_inserted.sec_number
}
}
impl Toc {
fn print_inner(&self, v: &mut String) {
use std::fmt::Write as _;
v.push_str("<ul>");
for entry in &self.entries {
// recursively format this table of contents
let _ = write!(
v,
"\n<li><a href=\"#{id}\" title=\"{name}\">{num} {html}</a>",
id = entry.id,
num = entry.sec_number,
name = Escape(&entry.name),
html = &entry.html,
);
entry.children.print_inner(&mut *v);
v.push_str("</li>");
}
v.push_str("</ul>");
}
pub(crate) fn print(&self) -> String {
let mut v = String::new();
self.print_inner(&mut v);
v
}
}
#[cfg(test)]
mod tests;