Skip to main content

bookyard_core/
catalog.rs

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}