use std::collections::BTreeMap;
use std::path::Path;
use std::path::PathBuf;
use std::path::absolute;
use std::rc::Rc;
use maud::Markup;
use maud::html;
use pathdiff::diff_paths;
use crate::Document;
use crate::full_page;
use crate::r#struct::Struct;
use crate::task::Task;
use crate::workflow::Workflow;
#[derive(Debug)]
pub enum PageType {
Index(Document),
Struct(Struct),
Task(Task),
Workflow(Workflow),
}
#[derive(Debug)]
pub struct HTMLPage {
name: String,
page_type: PageType,
}
impl HTMLPage {
pub fn new(name: String, page_type: PageType) -> Self {
Self { name, page_type }
}
pub fn name(&self) -> &str {
&self.name
}
pub fn page_type(&self) -> &PageType {
&self.page_type
}
}
#[derive(Debug)]
struct Node {
name: String,
path: PathBuf,
page: Option<Rc<HTMLPage>>,
children: BTreeMap<String, Node>,
}
impl Node {
pub fn new<P: Into<PathBuf>>(name: String, path: P) -> Self {
Self {
name,
path: path.into(),
page: None,
children: BTreeMap::new(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn path(&self) -> &PathBuf {
&self.path
}
pub fn page(&self) -> Option<Rc<HTMLPage>> {
self.page.clone()
}
pub fn depth_first_traversal(&self) -> Vec<&Node> {
fn recurse_depth_first<'a>(node: &'a Node, nodes: &mut Vec<&'a Node>) {
nodes.push(node);
for child in node.children.values() {
recurse_depth_first(child, nodes);
}
}
let mut nodes = Vec::new();
recurse_depth_first(self, &mut nodes);
nodes
}
}
#[derive(Debug)]
pub struct DocsTree {
root: Node,
stylesheet: Option<PathBuf>,
}
impl DocsTree {
pub fn new(root: impl AsRef<Path>) -> Self {
let abs_path = absolute(root.as_ref()).unwrap();
let node = Node::new(
abs_path.file_name().unwrap().to_str().unwrap().to_string(),
abs_path.clone(),
);
Self {
root: node,
stylesheet: None,
}
}
pub fn new_with_stylesheet(
root: impl AsRef<Path>,
stylesheet: impl AsRef<Path>,
) -> anyhow::Result<Self> {
let abs_path = absolute(root.as_ref()).unwrap();
let in_stylesheet = absolute(stylesheet.as_ref())?;
let new_stylesheet = abs_path.join("style.css");
std::fs::copy(in_stylesheet, &new_stylesheet)?;
let node = Node::new(
abs_path.file_name().unwrap().to_str().unwrap().to_string(),
abs_path.clone(),
);
Ok(Self {
root: node,
stylesheet: Some(new_stylesheet),
})
}
fn root(&self) -> &Node {
&self.root
}
fn root_mut(&mut self) -> &mut Node {
&mut self.root
}
pub fn stylesheet(&self) -> Option<&PathBuf> {
self.stylesheet.as_ref()
}
pub fn stylesheet_relative_to<P: AsRef<Path>>(&self, path: P) -> Option<PathBuf> {
if let Some(stylesheet) = self.stylesheet() {
let path = path.as_ref();
let stylesheet = diff_paths(stylesheet, path).unwrap();
Some(stylesheet)
} else {
None
}
}
pub fn add_page<P: Into<PathBuf>>(&mut self, abs_path: P, page: Rc<HTMLPage>) {
let root = self.root_mut();
let path = abs_path.into();
let rel_path = path
.strip_prefix(&root.path)
.expect("path should be in the docs directory");
let mut current_node = root;
let mut components = rel_path.components().peekable();
while let Some(component) = components.next() {
let cur_name = component.as_os_str().to_str().unwrap();
if current_node.children.contains_key(cur_name) {
current_node = current_node.children.get_mut(cur_name).unwrap();
} else {
let new_node = Node::new(cur_name.to_string(), current_node.path().join(component));
current_node.children.insert(cur_name.to_string(), new_node);
current_node = current_node.children.get_mut(cur_name).unwrap();
}
if let Some(next_component) = components.peek() {
if next_component.as_os_str().to_str().unwrap() == "index.html" {
break;
}
}
}
current_node.page = Some(page);
}
fn get_node<P: AsRef<Path>>(&self, abs_path: P) -> Option<&Node> {
let root = self.root();
let path = abs_path.as_ref();
let rel_path = path.strip_prefix(&root.path).unwrap();
let mut current_node = root;
for component in rel_path
.components()
.map(|c| c.as_os_str().to_str().unwrap())
{
if current_node.children.contains_key(component) {
current_node = current_node.children.get(component).unwrap();
} else {
return None;
}
}
Some(current_node)
}
pub fn get_page<P: AsRef<Path>>(&self, abs_path: P) -> Option<Rc<HTMLPage>> {
self.get_node(abs_path).and_then(|node| node.page())
}
pub fn render_sidebar_component<P: AsRef<Path>>(&self, path: P) -> Markup {
let root = self.root();
let base = path.as_ref().parent().unwrap();
let nodes = root.depth_first_traversal();
html! {
div class="top-0 left-0 h-full w-1/6 dark:bg-slate-950 dark:text-white" {
h1 class="text-2xl text-center" { "Sidebar" }
@for node in nodes {
@match node.page() {
Some(page) => {
@match page.page_type() {
PageType::Index(_) => {
p { a href=(diff_paths(node.path().join("index.html"), base).unwrap().to_string_lossy()) { (page.name()) } }
}
_ => {
p { a href=(diff_paths(node.path(), base).unwrap().to_string_lossy()) { (page.name()) } }
}
}
}
None => {
p class="" { (node.name()) }
}
}
}
}
}
}
pub fn render_all(&self) -> anyhow::Result<()> {
let root = self.root();
for node in root.depth_first_traversal() {
if let Some(page) = node.page() {
self.write_page(page.as_ref(), node.path())?;
}
}
self.write_homepage()?;
Ok(())
}
fn write_homepage(&self) -> anyhow::Result<()> {
let root = self.root();
let index_path = root.path().join("index.html");
let sidebar = self.render_sidebar_component(&index_path);
let content = html! {
div class="" {
h3 class="" { "Home" }
table class="border" {
thead class="border" { tr {
th class="" { "Page" }
}}
tbody class="border" {
@for node in root.depth_first_traversal() {
@if node.page().is_some() {
tr class="border" {
td class="border" {
@match node.page().unwrap().page_type() {
PageType::Index(_) => {
a href=(diff_paths(node.path().join("index.html"), root.path()).unwrap().to_str().unwrap()) {(node.name()) }
}
_ => {
a href=(diff_paths(node.path(), root.path()).unwrap().to_str().unwrap()) {(node.name()) }
}
}
}
}
}
}
}
}
}
};
let html = full_page(
"Home",
html! {
(sidebar)
(content)
},
self.stylesheet_relative_to(root.path()).as_deref(),
);
std::fs::write(index_path, html.into_string())?;
Ok(())
}
pub fn write_page<P: Into<PathBuf>>(&self, page: &HTMLPage, path: P) -> anyhow::Result<()> {
let mut path = path.into();
let content = match page.page_type() {
PageType::Index(doc) => {
path = path.join("index.html");
doc.render()
}
PageType::Struct(s) => s.render(),
PageType::Task(t) => t.render(),
PageType::Workflow(w) => w.render(),
};
let stylesheet =
self.stylesheet_relative_to(path.parent().expect("path should have a parent"));
let sidebar = self.render_sidebar_component(&path);
let html = full_page(
page.name(),
html! {
(sidebar)
(content)
},
stylesheet.as_deref(),
);
std::fs::write(path, html.into_string())?;
Ok(())
}
}