use serde::{Deserialize, Serialize};
use crate::BookConfig;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Catalog {
pub title: String,
pub books: Vec<CatalogBook>,
pub folders: Vec<FolderNode>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CatalogBook {
pub id: String,
pub title: String,
pub source: String,
pub href: String,
pub folders: Vec<String>,
pub tags: Vec<String>,
pub order: i32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FolderNode {
pub name: String,
pub path: String,
pub books: Vec<String>,
pub children: Vec<FolderNode>,
}
pub fn build_catalog(config: &crate::BookyardConfig) -> Catalog {
let mut books: Vec<_> = config
.books
.iter()
.map(|book| CatalogBook {
id: book.id.clone(),
title: book.title.clone(),
source: book.source.clone(),
href: format!("books/{}/index.html", book.id),
folders: book.folders.clone(),
tags: book.tags.clone(),
order: book.order,
})
.collect();
books.sort_by(|a, b| a.order.cmp(&b.order).then_with(|| a.title.cmp(&b.title)));
Catalog {
title: config.workspace.title.clone(),
folders: build_folder_tree(&config.books),
books,
}
}
pub fn build_folder_tree(books: &[BookConfig]) -> Vec<FolderNode> {
let mut roots: Vec<FolderNode> = Vec::new();
for book in books {
for folder in &book.folders {
insert_folder_path(&mut roots, folder, &book.id);
}
}
sort_nodes(&mut roots);
roots
}
fn insert_folder_path(nodes: &mut Vec<FolderNode>, folder: &str, book_id: &str) {
let mut parts = Vec::new();
for raw in folder.split(['/', '\\']) {
let part = raw.trim();
if !part.is_empty() {
parts.push(part);
}
}
if parts.is_empty() {
return;
}
insert_parts(nodes, &parts, String::new(), book_id);
}
fn insert_parts(nodes: &mut Vec<FolderNode>, parts: &[&str], parent: String, book_id: &str) {
let name = parts[0].to_string();
let path = if parent.is_empty() {
name.clone()
} else {
format!("{parent}/{name}")
};
let index = match nodes.iter().position(|node| node.name == name) {
Some(index) => index,
None => {
nodes.push(FolderNode {
name,
path: path.clone(),
books: Vec::new(),
children: Vec::new(),
});
nodes.len() - 1
}
};
if parts.len() == 1 {
if !nodes[index].books.iter().any(|id| id == book_id) {
nodes[index].books.push(book_id.to_owned());
}
return;
}
insert_parts(&mut nodes[index].children, &parts[1..], path, book_id);
}
fn sort_nodes(nodes: &mut [FolderNode]) {
nodes.sort_by(|a, b| a.path.cmp(&b.path));
for node in nodes {
node.books.sort();
sort_nodes(&mut node.children);
}
}