zola/components/rendering/src/table_of_contents.rs
2020-02-02 17:48:43 -08:00

167 lines
4.9 KiB
Rust

use serde_derive::Serialize;
/// Populated while receiving events from the markdown parser
#[derive(Debug, PartialEq, Clone, Serialize)]
pub struct Heading {
pub level: u32,
pub id: String,
pub permalink: String,
pub title: String,
pub children: Vec<Heading>,
}
impl Heading {
pub fn new(level: u32) -> Heading {
Heading {
level,
id: String::new(),
permalink: String::new(),
title: String::new(),
children: Vec::new(),
}
}
}
impl Default for Heading {
fn default() -> Self {
Heading::new(0)
}
}
// Takes a potential (mutable) parent and a heading to try and insert into
// Returns true when it performed the insertion, false otherwise
fn insert_into_parent(potential_parent: Option<&mut Heading>, heading: &Heading) -> bool {
match potential_parent {
None => {
// No potential parent to insert into so it needs to be insert higher
false
}
Some(parent) => {
if heading.level <= parent.level {
// Heading is same level or higher so we don't insert here
return false;
}
if heading.level + 1 == parent.level {
// We have a direct child of the parent
parent.children.push(heading.clone());
return true;
}
// We need to go deeper
if !insert_into_parent(parent.children.iter_mut().last(), heading) {
// No, we need to insert it here
parent.children.push(heading.clone());
}
true
}
}
}
/// Converts the flat temp headings into a nested set of headings
/// representing the hierarchy
pub fn make_table_of_contents(headings: Vec<Heading>) -> Vec<Heading> {
let mut toc = vec![];
for heading in headings {
// First heading or we try to insert the current heading in a previous one
if toc.is_empty() || !insert_into_parent(toc.iter_mut().last(), &heading) {
toc.push(heading);
}
}
toc
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_make_basic_toc() {
let input = vec![Heading::new(1), Heading::new(1), Heading::new(1)];
let toc = make_table_of_contents(input);
assert_eq!(toc.len(), 3);
}
#[test]
fn can_make_more_complex_toc() {
let input = vec![
Heading::new(1),
Heading::new(2),
Heading::new(2),
Heading::new(3),
Heading::new(2),
Heading::new(1),
Heading::new(2),
Heading::new(3),
Heading::new(3),
];
let toc = make_table_of_contents(input);
assert_eq!(toc.len(), 2);
assert_eq!(toc[0].children.len(), 3);
assert_eq!(toc[1].children.len(), 1);
assert_eq!(toc[0].children[1].children.len(), 1);
assert_eq!(toc[1].children[0].children.len(), 2);
}
#[test]
fn can_make_deep_toc() {
let input = vec![
Heading::new(1),
Heading::new(2),
Heading::new(3),
Heading::new(4),
Heading::new(5),
Heading::new(4),
];
let toc = make_table_of_contents(input);
assert_eq!(toc.len(), 1);
assert_eq!(toc[0].level, 1);
assert_eq!(toc[0].children.len(), 1);
assert_eq!(toc[0].children[0].children.len(), 1);
assert_eq!(toc[0].children[0].children[0].children.len(), 2);
assert_eq!(toc[0].children[0].children[0].children[0].children.len(), 1);
}
#[test]
fn can_make_deep_messy_toc() {
let input = vec![
Heading::new(2), // toc[0]
Heading::new(3),
Heading::new(4),
Heading::new(5),
Heading::new(4),
Heading::new(2), // toc[1]
Heading::new(1), // toc[2]
Heading::new(2),
Heading::new(3),
Heading::new(4),
];
let toc = make_table_of_contents(input);
assert_eq!(toc.len(), 3);
assert_eq!(toc[0].children.len(), 1);
assert_eq!(toc[0].children[0].children.len(), 2);
assert_eq!(toc[0].children[0].children[0].children.len(), 1);
assert_eq!(toc[1].children.len(), 0);
assert_eq!(toc[2].children.len(), 1);
assert_eq!(toc[2].children[0].children.len(), 1);
assert_eq!(toc[2].children[0].children[0].children.len(), 1);
}
#[test]
fn can_make_messy_toc() {
let input = vec![
Heading::new(3),
Heading::new(2),
Heading::new(2),
Heading::new(3),
Heading::new(2),
Heading::new(1),
Heading::new(4),
];
let toc = make_table_of_contents(input);
println!("{:#?}", toc);
assert_eq!(toc.len(), 5);
assert_eq!(toc[2].children.len(), 1);
assert_eq!(toc[4].children.len(), 1);
}
}