1use serde::{Deserialize, Serialize};
2
3use crate::BookConfig;
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6pub struct Catalog {
7 pub title: String,
8 pub books: Vec<CatalogBook>,
9 pub folders: Vec<FolderNode>,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct CatalogBook {
14 pub id: String,
15 pub title: String,
16 pub source: String,
17 pub href: String,
18 pub folders: Vec<String>,
19 pub tags: Vec<String>,
20 pub order: i32,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24pub struct FolderNode {
25 pub name: String,
26 pub path: String,
27 pub books: Vec<String>,
28 pub children: Vec<FolderNode>,
29}
30
31pub fn build_catalog(config: &crate::BookyardConfig) -> Catalog {
32 let mut books: Vec<_> = config
33 .books
34 .iter()
35 .map(|book| CatalogBook {
36 id: book.id.clone(),
37 title: book.title.clone(),
38 source: book.source.clone(),
39 href: format!("books/{}/index.html", book.id),
40 folders: book.folders.clone(),
41 tags: book.tags.clone(),
42 order: book.order,
43 })
44 .collect();
45 books.sort_by(|a, b| a.order.cmp(&b.order).then_with(|| a.title.cmp(&b.title)));
46 Catalog {
47 title: config.workspace.title.clone(),
48 folders: build_folder_tree(&config.books),
49 books,
50 }
51}
52
53pub fn build_folder_tree(books: &[BookConfig]) -> Vec<FolderNode> {
54 let mut roots: Vec<FolderNode> = Vec::new();
55 for book in books {
56 for folder in &book.folders {
57 insert_folder_path(&mut roots, folder, &book.id);
58 }
59 }
60 sort_nodes(&mut roots);
61 roots
62}
63
64fn insert_folder_path(nodes: &mut Vec<FolderNode>, folder: &str, book_id: &str) {
65 let mut parts = Vec::new();
66 for raw in folder.split(['/', '\\']) {
67 let part = raw.trim();
68 if !part.is_empty() {
69 parts.push(part);
70 }
71 }
72 if parts.is_empty() {
73 return;
74 }
75 insert_parts(nodes, &parts, String::new(), book_id);
76}
77
78fn insert_parts(nodes: &mut Vec<FolderNode>, parts: &[&str], parent: String, book_id: &str) {
79 let name = parts[0].to_string();
80 let path = if parent.is_empty() {
81 name.clone()
82 } else {
83 format!("{parent}/{name}")
84 };
85 let index = match nodes.iter().position(|node| node.name == name) {
86 Some(index) => index,
87 None => {
88 nodes.push(FolderNode {
89 name,
90 path: path.clone(),
91 books: Vec::new(),
92 children: Vec::new(),
93 });
94 nodes.len() - 1
95 }
96 };
97 if parts.len() == 1 {
98 if !nodes[index].books.iter().any(|id| id == book_id) {
99 nodes[index].books.push(book_id.to_owned());
100 }
101 return;
102 }
103 insert_parts(&mut nodes[index].children, &parts[1..], path, book_id);
104}
105
106fn sort_nodes(nodes: &mut [FolderNode]) {
107 nodes.sort_by(|a, b| a.path.cmp(&b.path));
108 for node in nodes {
109 node.books.sort();
110 sort_nodes(&mut node.children);
111 }
112}