wdl_doc/
docs_tree.rs

1//! Implementations for a [`DocsTree`] which represents the docs directory.
2
3use 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/// The type of a page.
20#[derive(Debug)]
21pub enum PageType {
22    /// An index page.
23    Index(Document),
24    /// A struct page.
25    Struct(Struct),
26    /// A task page.
27    Task(Task),
28    /// A workflow page.
29    Workflow(Workflow),
30}
31
32/// An HTML page in the docs directory.
33#[derive(Debug)]
34pub struct HTMLPage {
35    /// The display name of the page.
36    name: String,
37    /// The type of the page.
38    page_type: PageType,
39}
40
41impl HTMLPage {
42    /// Create a new HTML page.
43    pub fn new(name: String, page_type: PageType) -> Self {
44        Self { name, page_type }
45    }
46
47    /// Get the name of the page.
48    pub fn name(&self) -> &str {
49        &self.name
50    }
51
52    /// Get the type of the page.
53    pub fn page_type(&self) -> &PageType {
54        &self.page_type
55    }
56}
57
58/// A node in the docs directory tree.
59#[derive(Debug)]
60struct Node {
61    /// The name of the node.
62    name: String,
63    /// The absolute path to the node.
64    path: PathBuf,
65    /// The page associated with the node.
66    page: Option<Rc<HTMLPage>>,
67    /// The children of the node.
68    children: BTreeMap<String, Node>,
69}
70
71impl Node {
72    /// Create a new node.
73    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    /// Get the name of the node.
83    pub fn name(&self) -> &str {
84        &self.name
85    }
86
87    /// Get the path of the node.
88    pub fn path(&self) -> &PathBuf {
89        &self.path
90    }
91
92    /// Get the page associated with the node.
93    pub fn page(&self) -> Option<Rc<HTMLPage>> {
94        self.page.clone()
95    }
96
97    /// Gather the node and its children in a Depth First Traversal order.
98    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/// A tree representing the docs directory.
115#[derive(Debug)]
116pub struct DocsTree {
117    /// The root of the tree.
118    ///
119    /// `root.path` is the path to the docs directory and is absolute.
120    root: Node,
121    /// The absolute path to the stylesheet, if it exists.
122    stylesheet: Option<PathBuf>,
123}
124
125impl DocsTree {
126    /// Create a new docs tree.
127    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    /// Create a new docs tree with a stylesheet.
140    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    /// Get the root of the tree.
161    fn root(&self) -> &Node {
162        &self.root
163    }
164
165    /// Get the root of the tree as mutable.
166    fn root_mut(&mut self) -> &mut Node {
167        &mut self.root
168    }
169
170    /// Get the absolute path to the stylesheet.
171    pub fn stylesheet(&self) -> Option<&PathBuf> {
172        self.stylesheet.as_ref()
173    }
174
175    /// Get a relative path to the stylesheet.
176    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    /// Add a page to the tree.
187    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    /// Get the Node associated with a path.
217    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    /// Get the page associated with a path.
239    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    /// Render a sidebar component given a path.
244    ///
245    /// The sidebar will contain a table of contents for the docs directory.
246    /// Every node in the tree will be visited in a Depth First Traversal order.
247    /// If the node has a page associated with it, a link to the page will be
248    /// rendered. If the node does not have a page associated with it, the
249    /// name of the node will be rendered. All links will be relative to the
250    /// given path.
251    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    /// Render every page in the tree.
281    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    /// Write the homepage to disk.
295    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    /// Write a page to disk at the designated path.
342    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}