| //! 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; |