1use std::collections::BTreeMap;
4use std::path::Path;
5use std::path::PathBuf;
6use std::path::absolute;
7use std::rc::Rc;
8
9use maud::Markup;
10use maud::html;
11use pathdiff::diff_paths;
12
13use crate::Document;
14use crate::full_page;
15use crate::r#struct::Struct;
16use crate::task::Task;
17use crate::workflow::Workflow;
18
19#[derive(Debug)]
21pub enum PageType {
22 Index(Document),
24 Struct(Struct),
26 Task(Task),
28 Workflow(Workflow),
30}
31
32#[derive(Debug)]
34pub struct HTMLPage {
35 name: String,
37 page_type: PageType,
39}
40
41impl HTMLPage {
42 pub fn new(name: String, page_type: PageType) -> Self {
44 Self { name, page_type }
45 }
46
47 pub fn name(&self) -> &str {
49 &self.name
50 }
51
52 pub fn page_type(&self) -> &PageType {
54 &self.page_type
55 }
56}
57
58#[derive(Debug)]
60struct Node {
61 name: String,
63 path: PathBuf,
65 page: Option<Rc<HTMLPage>>,
67 children: BTreeMap<String, Node>,
69}
70
71impl Node {
72 pub fn new<P: Into<PathBuf>>(name: String, path: P) -> Self {
74 Self {
75 name,
76 path: path.into(),
77 page: None,
78 children: BTreeMap::new(),
79 }
80 }
81
82 pub fn name(&self) -> &str {
84 &self.name
85 }
86
87 pub fn path(&self) -> &PathBuf {
89 &self.path
90 }
91
92 pub fn page(&self) -> Option<Rc<HTMLPage>> {
94 self.page.clone()
95 }
96
97 pub fn depth_first_traversal(&self) -> Vec<&Node> {
99 fn recurse_depth_first<'a>(node: &'a Node, nodes: &mut Vec<&'a Node>) {
100 nodes.push(node);
101
102 for child in node.children.values() {
103 recurse_depth_first(child, nodes);
104 }
105 }
106
107 let mut nodes = Vec::new();
108 recurse_depth_first(self, &mut nodes);
109
110 nodes
111 }
112}
113
114#[derive(Debug)]
116pub struct DocsTree {
117 root: Node,
121 stylesheet: Option<PathBuf>,
123}
124
125impl DocsTree {
126 pub fn new(root: impl AsRef<Path>) -> Self {
128 let abs_path = absolute(root.as_ref()).unwrap();
129 let node = Node::new(
130 abs_path.file_name().unwrap().to_str().unwrap().to_string(),
131 abs_path.clone(),
132 );
133 Self {
134 root: node,
135 stylesheet: None,
136 }
137 }
138
139 pub fn new_with_stylesheet(
141 root: impl AsRef<Path>,
142 stylesheet: impl AsRef<Path>,
143 ) -> anyhow::Result<Self> {
144 let abs_path = absolute(root.as_ref()).unwrap();
145 let in_stylesheet = absolute(stylesheet.as_ref())?;
146 let new_stylesheet = abs_path.join("style.css");
147 std::fs::copy(in_stylesheet, &new_stylesheet)?;
148
149 let node = Node::new(
150 abs_path.file_name().unwrap().to_str().unwrap().to_string(),
151 abs_path.clone(),
152 );
153
154 Ok(Self {
155 root: node,
156 stylesheet: Some(new_stylesheet),
157 })
158 }
159
160 fn root(&self) -> &Node {
162 &self.root
163 }
164
165 fn root_mut(&mut self) -> &mut Node {
167 &mut self.root
168 }
169
170 pub fn stylesheet(&self) -> Option<&PathBuf> {
172 self.stylesheet.as_ref()
173 }
174
175 pub fn stylesheet_relative_to<P: AsRef<Path>>(&self, path: P) -> Option<PathBuf> {
177 if let Some(stylesheet) = self.stylesheet() {
178 let path = path.as_ref();
179 let stylesheet = diff_paths(stylesheet, path).unwrap();
180 Some(stylesheet)
181 } else {
182 None
183 }
184 }
185
186 pub fn add_page<P: Into<PathBuf>>(&mut self, abs_path: P, page: Rc<HTMLPage>) {
188 let root = self.root_mut();
189 let path = abs_path.into();
190 let rel_path = path
191 .strip_prefix(&root.path)
192 .expect("path should be in the docs directory");
193
194 let mut current_node = root;
195
196 let mut components = rel_path.components().peekable();
197 while let Some(component) = components.next() {
198 let cur_name = component.as_os_str().to_str().unwrap();
199 if current_node.children.contains_key(cur_name) {
200 current_node = current_node.children.get_mut(cur_name).unwrap();
201 } else {
202 let new_node = Node::new(cur_name.to_string(), current_node.path().join(component));
203 current_node.children.insert(cur_name.to_string(), new_node);
204 current_node = current_node.children.get_mut(cur_name).unwrap();
205 }
206 if let Some(next_component) = components.peek() {
207 if next_component.as_os_str().to_str().unwrap() == "index.html" {
208 break;
209 }
210 }
211 }
212
213 current_node.page = Some(page);
214 }
215
216 fn get_node<P: AsRef<Path>>(&self, abs_path: P) -> Option<&Node> {
218 let root = self.root();
219 let path = abs_path.as_ref();
220 let rel_path = path.strip_prefix(&root.path).unwrap();
221
222 let mut current_node = root;
223
224 for component in rel_path
225 .components()
226 .map(|c| c.as_os_str().to_str().unwrap())
227 {
228 if current_node.children.contains_key(component) {
229 current_node = current_node.children.get(component).unwrap();
230 } else {
231 return None;
232 }
233 }
234
235 Some(current_node)
236 }
237
238 pub fn get_page<P: AsRef<Path>>(&self, abs_path: P) -> Option<Rc<HTMLPage>> {
240 self.get_node(abs_path).and_then(|node| node.page())
241 }
242
243 pub fn render_sidebar_component<P: AsRef<Path>>(&self, path: P) -> Markup {
252 let root = self.root();
253 let base = path.as_ref().parent().unwrap();
254 let nodes = root.depth_first_traversal();
255
256 html! {
257 div class="top-0 left-0 h-full w-1/6 dark:bg-slate-950 dark:text-white" {
258 h1 class="text-2xl text-center" { "Sidebar" }
259 @for node in nodes {
260 @match node.page() {
261 Some(page) => {
262 @match page.page_type() {
263 PageType::Index(_) => {
264 p { a href=(diff_paths(node.path().join("index.html"), base).unwrap().to_string_lossy()) { (page.name()) } }
265 }
266 _ => {
267 p { a href=(diff_paths(node.path(), base).unwrap().to_string_lossy()) { (page.name()) } }
268 }
269 }
270 }
271 None => {
272 p class="" { (node.name()) }
273 }
274 }
275 }
276 }
277 }
278 }
279
280 pub fn render_all(&self) -> anyhow::Result<()> {
282 let root = self.root();
283
284 for node in root.depth_first_traversal() {
285 if let Some(page) = node.page() {
286 self.write_page(page.as_ref(), node.path())?;
287 }
288 }
289
290 self.write_homepage()?;
291 Ok(())
292 }
293
294 fn write_homepage(&self) -> anyhow::Result<()> {
296 let root = self.root();
297 let index_path = root.path().join("index.html");
298
299 let sidebar = self.render_sidebar_component(&index_path);
300 let content = html! {
301 div class="" {
302 h3 class="" { "Home" }
303 table class="border" {
304 thead class="border" { tr {
305 th class="" { "Page" }
306 }}
307 tbody class="border" {
308 @for node in root.depth_first_traversal() {
309 @if node.page().is_some() {
310 tr class="border" {
311 td class="border" {
312 @match node.page().unwrap().page_type() {
313 PageType::Index(_) => {
314 a href=(diff_paths(node.path().join("index.html"), root.path()).unwrap().to_str().unwrap()) {(node.name()) }
315 }
316 _ => {
317 a href=(diff_paths(node.path(), root.path()).unwrap().to_str().unwrap()) {(node.name()) }
318 }
319 }
320 }
321 }
322 }
323 }
324 }
325 }
326 }
327 };
328
329 let html = full_page(
330 "Home",
331 html! {
332 (sidebar)
333 (content)
334 },
335 self.stylesheet_relative_to(root.path()).as_deref(),
336 );
337 std::fs::write(index_path, html.into_string())?;
338 Ok(())
339 }
340
341 pub fn write_page<P: Into<PathBuf>>(&self, page: &HTMLPage, path: P) -> anyhow::Result<()> {
343 let mut path = path.into();
344
345 let content = match page.page_type() {
346 PageType::Index(doc) => {
347 path = path.join("index.html");
348 doc.render()
349 }
350 PageType::Struct(s) => s.render(),
351 PageType::Task(t) => t.render(),
352 PageType::Workflow(w) => w.render(),
353 };
354
355 let stylesheet =
356 self.stylesheet_relative_to(path.parent().expect("path should have a parent"));
357 let sidebar = self.render_sidebar_component(&path);
358
359 let html = full_page(
360 page.name(),
361 html! {
362 (sidebar)
363 (content)
364 },
365 stylesheet.as_deref(),
366 );
367 std::fs::write(path, html.into_string())?;
368 Ok(())
369 }
370}