212 lines
5.9 KiB
Rust
212 lines
5.9 KiB
Rust
use front_matter::InsertAnchor;
|
|
use tera::{Context as TeraContext, Tera};
|
|
|
|
#[derive(Debug, PartialEq, Clone, Serialize)]
|
|
pub struct Header {
|
|
#[serde(skip_serializing)]
|
|
pub level: i32,
|
|
pub id: String,
|
|
pub title: String,
|
|
pub permalink: String,
|
|
pub children: Vec<Header>,
|
|
}
|
|
|
|
impl Header {
|
|
pub fn from_temp_header(tmp: &TempHeader, children: Vec<Header>) -> Header {
|
|
Header {
|
|
level: tmp.level,
|
|
id: tmp.id.clone(),
|
|
title: tmp.title.clone(),
|
|
permalink: tmp.permalink.clone(),
|
|
children,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Populated while receiving events from the markdown parser
|
|
#[derive(Debug, PartialEq, Clone)]
|
|
pub struct TempHeader {
|
|
pub level: i32,
|
|
pub id: String,
|
|
pub permalink: String,
|
|
pub title: String,
|
|
pub html: String,
|
|
}
|
|
|
|
impl TempHeader {
|
|
pub fn new(level: i32) -> TempHeader {
|
|
TempHeader {
|
|
level,
|
|
id: String::new(),
|
|
permalink: String::new(),
|
|
title: String::new(),
|
|
html: String::new(),
|
|
}
|
|
}
|
|
|
|
pub fn add_html(&mut self, val: &str) {
|
|
self.html += val;
|
|
}
|
|
|
|
pub fn add_text(&mut self, val: &str) {
|
|
self.html += val;
|
|
self.title += val;
|
|
}
|
|
|
|
/// Transform all the information we have about this header into the HTML string for it
|
|
pub fn to_string(&self, tera: &Tera, insert_anchor: InsertAnchor) -> String {
|
|
let anchor_link = if insert_anchor != InsertAnchor::None {
|
|
let mut c = TeraContext::new();
|
|
c.insert("id", &self.id);
|
|
tera.render("anchor-link.html", &c).unwrap()
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
match insert_anchor {
|
|
InsertAnchor::None => format!(
|
|
"<h{lvl} id=\"{id}\">{t}</h{lvl}>\n",
|
|
lvl = self.level,
|
|
t = self.html,
|
|
id = self.id
|
|
),
|
|
InsertAnchor::Left => format!(
|
|
"<h{lvl} id=\"{id}\">{a}{t}</h{lvl}>\n",
|
|
lvl = self.level,
|
|
a = anchor_link,
|
|
t = self.html,
|
|
id = self.id
|
|
),
|
|
InsertAnchor::Right => format!(
|
|
"<h{lvl} id=\"{id}\">{t}{a}</h{lvl}>\n",
|
|
lvl = self.level,
|
|
a = anchor_link,
|
|
t = self.html,
|
|
id = self.id
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for TempHeader {
|
|
fn default() -> Self {
|
|
TempHeader::new(0)
|
|
}
|
|
}
|
|
|
|
/// Recursively finds children of a header
|
|
fn find_children(
|
|
parent_level: i32,
|
|
start_at: usize,
|
|
temp_headers: &[TempHeader],
|
|
) -> (usize, Vec<Header>) {
|
|
let mut headers = vec![];
|
|
|
|
let mut start_at = start_at;
|
|
// If we have children, we will need to skip some headers since they are already inserted
|
|
let mut to_skip = 0;
|
|
|
|
for h in &temp_headers[start_at..] {
|
|
// stop when we encounter a title at the same level or higher
|
|
// than the parent one. Here a lower integer is considered higher as we are talking about
|
|
// HTML headers: h1, h2, h3, h4, h5 and h6
|
|
if h.level <= parent_level {
|
|
return (start_at, headers);
|
|
}
|
|
|
|
// Do we need to skip some headers?
|
|
if to_skip > 0 {
|
|
to_skip -= 1;
|
|
continue;
|
|
}
|
|
|
|
let (end, children) = find_children(h.level, start_at + 1, temp_headers);
|
|
headers.push(Header::from_temp_header(h, children));
|
|
|
|
// we didn't find any children
|
|
if end == start_at {
|
|
start_at += 1;
|
|
to_skip = 0;
|
|
} else {
|
|
// calculates how many we need to skip. Since the find_children start_at starts at 1,
|
|
// we need to remove 1 to ensure correctness
|
|
to_skip = end - start_at - 1;
|
|
start_at = end;
|
|
}
|
|
|
|
// we don't want to index out of bounds
|
|
if start_at + 1 > temp_headers.len() {
|
|
return (start_at, headers);
|
|
}
|
|
}
|
|
|
|
(start_at, headers)
|
|
}
|
|
|
|
/// Converts the flat temp headers into a nested set of headers
|
|
/// representing the hierarchy
|
|
pub fn make_table_of_contents(temp_headers: &[TempHeader]) -> Vec<Header> {
|
|
let mut toc = vec![];
|
|
let mut start_idx = 0;
|
|
for (i, h) in temp_headers.iter().enumerate() {
|
|
if i < start_idx {
|
|
continue;
|
|
}
|
|
let (end_idx, children) = find_children(h.level, start_idx + 1, temp_headers);
|
|
start_idx = end_idx;
|
|
toc.push(Header::from_temp_header(h, children));
|
|
}
|
|
|
|
toc
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn can_make_basic_toc() {
|
|
let input = vec![TempHeader::new(1), TempHeader::new(1), TempHeader::new(1)];
|
|
let toc = make_table_of_contents(&input);
|
|
assert_eq!(toc.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn can_make_more_complex_toc() {
|
|
let input = vec![
|
|
TempHeader::new(1),
|
|
TempHeader::new(2),
|
|
TempHeader::new(2),
|
|
TempHeader::new(3),
|
|
TempHeader::new(2),
|
|
TempHeader::new(1),
|
|
TempHeader::new(2),
|
|
TempHeader::new(3),
|
|
TempHeader::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_messy_toc() {
|
|
let input = vec![
|
|
TempHeader::new(3),
|
|
TempHeader::new(2),
|
|
TempHeader::new(2),
|
|
TempHeader::new(3),
|
|
TempHeader::new(2),
|
|
TempHeader::new(1),
|
|
TempHeader::new(4),
|
|
];
|
|
let toc = make_table_of_contents(&input);
|
|
assert_eq!(toc.len(), 5);
|
|
assert_eq!(toc[2].children.len(), 1);
|
|
assert_eq!(toc[4].children.len(), 1);
|
|
}
|
|
}
|