1use std::collections::HashMap;
18
19use rtb_assets::Assets;
20
21use crate::error::{DocsError, Result};
22use crate::index::{parse_index, scan_index, Index};
23
24pub fn load_docs(assets: &Assets, root: &str) -> Result<(Index, HashMap<String, String>)> {
37 let root = root.trim_end_matches('/');
38
39 let mut pages: HashMap<String, String> = HashMap::new();
40 walk(assets, root, "", &mut pages)?;
41
42 let yaml_key = format!("{root}/_index.yaml");
43 let index = if assets.exists(&yaml_key) {
44 let body = assets.open_text(&yaml_key).map_err(|e| DocsError::Assets(e.to_string()))?;
45 parse_index(&body)?
46 } else if pages.is_empty() {
47 return Err(DocsError::RootMissing(root.to_string()));
48 } else {
49 let scan_input: Vec<(String, String)> =
50 pages.iter().map(|(p, b)| (p.clone(), b.clone())).collect();
51 scan_index(&scan_input)
52 };
53
54 Ok((index, pages))
55}
56
57fn walk(
60 assets: &Assets,
61 root: &str,
62 prefix: &str,
63 out: &mut HashMap<String, String>,
64) -> Result<()> {
65 let dir_key = if prefix.is_empty() { root.to_string() } else { format!("{root}/{prefix}") };
66 for entry in assets.list_dir(&dir_key) {
67 let rel = if prefix.is_empty() { entry.clone() } else { format!("{prefix}/{entry}") };
71 let key = format!("{root}/{rel}");
72 if std::path::Path::new(&entry)
73 .extension()
74 .is_some_and(|ext| ext.eq_ignore_ascii_case("md"))
75 {
76 let body = assets.open_text(&key).map_err(|e| DocsError::Assets(e.to_string()))?;
77 out.insert(rel, body);
78 } else if assets.exists(&key) {
79 } else {
82 walk(assets, root, &rel, out)?;
86 }
87 }
88 Ok(())
89}
90
91#[cfg(test)]
96mod tests {
97 use super::*;
98 use std::collections::HashMap;
99
100 fn assets_with(files: &[(&str, &str)]) -> Assets {
101 let map: HashMap<String, Vec<u8>> =
102 files.iter().map(|(p, b)| ((*p).to_string(), b.as_bytes().to_vec())).collect();
103 Assets::builder().memory("test", map).build()
104 }
105
106 #[test]
107 fn loads_with_index_yaml() {
108 let assets = assets_with(&[
109 (
110 "docs/_index.yaml",
111 "title: T\nsections:\n - title: S\n pages:\n - { path: a.md, title: A }\n",
112 ),
113 ("docs/a.md", "# A\n\nbody"),
114 ]);
115 let (idx, pages) = load_docs(&assets, "docs").expect("load");
116 assert_eq!(idx.title, "T");
117 assert_eq!(pages.get("a.md").map(String::as_str), Some("# A\n\nbody"));
118 }
119
120 #[test]
121 fn falls_back_to_scan_when_no_index_yaml() {
122 let assets = assets_with(&[
123 ("docs/intro.md", "# Intro\n\nhi"),
124 ("docs/guide/setup.md", "# Setup\n\nbody"),
125 ]);
126 let (idx, pages) = load_docs(&assets, "docs").expect("load");
127 assert_eq!(idx.page_count(), 2);
128 assert!(pages.contains_key("intro.md"));
129 assert!(pages.contains_key("guide/setup.md"));
130 }
131
132 #[test]
133 fn empty_tree_is_root_missing() {
134 let assets = assets_with(&[]);
135 let err = load_docs(&assets, "docs").expect_err("empty tree");
136 assert!(matches!(err, DocsError::RootMissing(_)), "got {err:?}");
137 }
138}