use std::collections::BTreeMap;
use crate::model::{Doc, TreeNode};
#[derive(Default)]
struct Builder {
dirs: BTreeMap<String, Builder>,
docs: BTreeMap<String, (String, String)>, note: Option<String>,
}
fn insert(node: &mut Builder, parts: &[&str], slug: &str, title: &str, depth: usize) {
match parts {
[leaf] if *leaf == "index" && depth > 0 => {
node.note = Some(slug.to_string());
}
[leaf] => {
node.docs
.insert(leaf.to_string(), (slug.to_string(), title.to_string()));
}
[head, rest @ ..] => {
insert(
node.dirs.entry(head.to_string()).or_default(),
rest,
slug,
title,
depth + 1,
);
}
[] => {}
}
}
fn to_nodes(builder: Builder) -> Vec<TreeNode> {
let mut out = Vec::new();
for (name, child) in builder.dirs {
let slug = child.note.clone();
out.push(TreeNode::Dir {
name,
slug,
children: to_nodes(child),
});
}
for (name, (slug, title)) in builder.docs {
out.push(TreeNode::Doc { name, slug, title });
}
out
}
pub fn build_tree(docs: &[Doc]) -> Vec<TreeNode> {
let mut root = Builder::default();
for doc in docs {
let parts: Vec<&str> = doc.slug.split('/').collect();
insert(&mut root, &parts, &doc.slug, &doc.title, 0);
}
to_nodes(root)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Doc, TreeNode};
fn doc(slug: &str, title: &str) -> Doc {
Doc {
rel_path: format!("{slug}.md"),
slug: slug.into(),
title: title.into(),
description: None,
body_html: String::new(),
has_math: false,
has_mermaid: false,
components_used: Default::default(),
headings: Vec::new(),
}
}
#[test]
fn groups_docs_under_directories() {
let docs = vec![doc("index", "Home"), doc("guide/intro", "Intro")];
let tree = build_tree(&docs);
assert_eq!(tree.len(), 2);
match &tree[0] {
TreeNode::Dir { name, children, .. } => {
assert_eq!(name, "guide");
assert_eq!(children.len(), 1);
assert!(
matches!(&children[0], TreeNode::Doc { slug, .. } if slug == "guide/intro")
);
}
other => panic!("expected dir, got {other:?}"),
}
assert!(matches!(&tree[1], TreeNode::Doc { slug, .. } if slug == "index"));
}
#[test]
fn dirs_come_before_docs_even_when_doc_sorts_first() {
let docs = vec![doc("aaa", "A"), doc("zzz_dir/page", "Page")];
let tree = build_tree(&docs);
assert_eq!(tree.len(), 2);
assert!(matches!(&tree[0], TreeNode::Dir { name, .. } if name == "zzz_dir"));
assert!(matches!(&tree[1], TreeNode::Doc { slug, .. } if slug == "aaa"));
}
#[test]
fn multiple_dirs_and_docs_each_sorted_within_group() {
let docs = vec![
doc("m_doc", "M"),
doc("b_dir/x", "X"),
doc("a_doc", "A"),
doc("a_dir/y", "Y"),
];
let tree = build_tree(&docs);
assert!(matches!(&tree[0], TreeNode::Dir { name, .. } if name == "a_dir"));
assert!(matches!(&tree[1], TreeNode::Dir { name, .. } if name == "b_dir"));
assert!(matches!(&tree[2], TreeNode::Doc { name, .. } if name == "a_doc"));
assert!(matches!(&tree[3], TreeNode::Doc { name, .. } if name == "m_doc"));
}
#[test]
fn groups_nested_directories() {
let docs = vec![doc("a/b/c", "Deep")];
let tree = build_tree(&docs);
let a = match &tree[0] {
TreeNode::Dir { name, children, .. } if name == "a" => children,
other => panic!("expected dir a, got {other:?}"),
};
let b = match &a[0] {
TreeNode::Dir { name, children, .. } if name == "b" => children,
other => panic!("expected dir b, got {other:?}"),
};
assert!(matches!(&b[0], TreeNode::Doc { slug, .. } if slug == "a/b/c"));
}
#[test]
fn folder_index_becomes_folder_note_not_child() {
let docs = vec![doc("guide/index", "Guide"), doc("guide/intro", "Intro")];
let tree = build_tree(&docs);
assert_eq!(tree.len(), 1);
match &tree[0] {
TreeNode::Dir {
name,
slug,
children,
} => {
assert_eq!(name, "guide");
assert_eq!(slug.as_deref(), Some("guide/index"));
assert_eq!(children.len(), 1);
assert!(
matches!(&children[0], TreeNode::Doc { slug, .. } if slug == "guide/intro")
);
}
other => panic!("expected dir, got {other:?}"),
}
}
#[test]
fn root_index_stays_an_ordinary_doc() {
let docs = vec![doc("index", "Home"), doc("guide/intro", "Intro")];
let tree = build_tree(&docs);
assert!(tree
.iter()
.any(|n| matches!(n, TreeNode::Doc { slug, .. } if slug == "index")));
}
}