use std::fmt::Write; use chrono::{DateTime, Utc}; use crate::{ config::Config, dirs::Dirs, html::EscapeAttribute, sources::Sources, state::{FileId, Treehouse}, tree::{ attributes::{Content, Stage, Visibility}, mini_template, pull::BranchKind, SemaBranchId, }, vfs::{self, VPath, VPathBuf}, }; use super::{djot, EscapeHtml}; pub struct Renderer<'a> { pub sources: &'a Sources, pub dirs: &'a Dirs, pub file_id: FileId, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum HasChildren { No, Yes, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum LinkButton { Tree, Branch, } struct OpenBranch { has_children: HasChildren, } impl Renderer<'_> { fn treehouse(&self) -> &Treehouse { &self.sources.treehouse } fn config(&self) -> &Config { &self.sources.config } fn open_branch(&self, s: &mut String, id: &str) { write!(s, "
  • "branch", HasChildren::No => "leaf", }) ) .unwrap(); } fn attr_class_push(&self, s: &mut String, class: &str) { write!(s, " {}", EscapeAttribute(class)).unwrap(); } fn attr_class_end(&self, s: &mut String) { s.push('"'); } fn attr_cast_begin(&self, s: &mut String) { s.push_str(r#" data-cast=""#); } fn attr_cast_push(&self, s: &mut String, spell: &str) { if s.as_bytes().last() != Some(&b'"') { s.push(' '); } write!(s, "{}", EscapeAttribute(spell)).unwrap(); } fn attr_cast_end(&self, s: &mut String) { s.push('"'); } fn attr_link(&self, s: &mut String, linked: &VPath) { self.attr(s, "th-link", linked.as_str()); } fn attr_ts(&self, s: &mut String, timestamp: DateTime) { self.attr(s, "th-ts", ×tamp.timestamp_millis().to_string()) } fn attr_do_not_persist(&self, s: &mut String) { s.push_str(" th-do-not-persist"); } fn end_attrs(&self, s: &mut String) { s.push('>'); } fn begin_container( &self, s: &mut String, has_children: HasChildren, branch_kind: BranchKind, ) -> OpenBranch { match has_children { HasChildren::Yes => { s.push_str(match branch_kind { BranchKind::Expanded => "
    ", BranchKind::Collapsed => "
    ", }); s.push_str(""); } HasChildren::No => { s.push_str("
    "); } } OpenBranch { has_children } } fn begin_children(&self, s: &mut String, open: &OpenBranch) -> HasChildren { if open.has_children == HasChildren::Yes { s.push_str("
    "); } open.has_children } fn close_branch(&self, s: &mut String, open: OpenBranch) { match open.has_children { HasChildren::Yes => { s.push_str("
    "); } HasChildren::No => { s.push_str(""); } } s.push_str("
  • "); } fn bullet_point(&self, s: &mut String) { s.push_str(""); } fn branch_content(&self, s: &mut String, markup: &str, linked: Option<&VPath>) { s.push_str(""); let events: Vec<_> = jotdown::Parser::new(markup).into_offset_iter().collect(); // TODO: Report rendering diagnostics. let render_diagnostics = djot::Renderer { page_id: self .treehouse() .tree_path(self.file_id) .expect(".tree file expected") .to_string(), config: self.config(), dirs: self.dirs, treehouse: self.treehouse(), file_id: self.file_id, } .render(&events, s); if let Some(linked) = linked { write!( s, "", EscapeAttribute(&self.config().site), EscapeAttribute(linked.as_str()), EscapeHtml(linked.as_str()), ) .unwrap(); } s.push_str(""); } fn button_bar( &self, s: &mut String, date_time: Option>, link_button: LinkButton, link: &str, ) { s.push_str(""); { if let Some(date_time) = date_time { write!(s, "{}", date_time.format("%F")).unwrap(); } match link_button { LinkButton::Tree => { write!( s, "", EscapeAttribute(link) ) .unwrap(); } LinkButton::Branch => { write!( s, "", EscapeAttribute(link) ) .unwrap(); } } } s.push_str(""); } fn branch_children_empty(&self, s: &mut String) { s.push_str(""); } fn branch_children(&self, s: &mut String, branch_id: SemaBranchId) { let branch = self.treehouse().tree.branch(branch_id); s.push_str("'); let num_children = branch.children.len(); for i in 0..num_children { let child_id = self.treehouse().tree.branch(branch_id).children[i]; self.branch(s, child_id); } s.push_str(""); } fn preprocess_markup(&self, branch_id: SemaBranchId) -> String { let branch = self.treehouse().tree.branch(branch_id); let raw_block_content = &self.treehouse().source(self.file_id).input()[branch.content.clone()]; let mut markup = String::with_capacity(raw_block_content.len()); for line in raw_block_content.lines() { // Bit of a jank way to remove at most branch.indent_level spaces from the front. let mut space_count = 0; for i in 0..branch.indent_level { if line.as_bytes().get(i).copied() == Some(b' ') { space_count += 1; } else { break; } } markup.push_str(&line[space_count..]); markup.push('\n'); } if branch.attributes.template { markup = mini_template::render(self.config(), self.treehouse(), self.dirs, &markup); } markup } pub fn branch(&self, s: &mut String, branch_id: SemaBranchId) { let branch = self.treehouse().tree.branch(branch_id); if !cfg!(debug_assertions) && branch.attributes.stage == Stage::Draft { return; } let has_children = match !branch.children.is_empty() || matches!(branch.attributes.content, Content::ResolvedLink(_)) { true => HasChildren::Yes, false => HasChildren::No, }; let linked_tree = match branch.attributes.content { Content::Inline | Content::Link(_) => None, Content::ResolvedLink(file_id) => self.treehouse().tree_path(file_id), }; self.open_branch(s, &branch.html_id); { // data-cast self.attr_cast_begin(s); self.attr_cast_push( s, match linked_tree { Some(_) => "b-linked", None => "b", }, ); if !branch.attributes.cast.is_empty() { self.attr_cast_push(s, &branch.attributes.cast); } self.attr_cast_end(s); // th-link if let Some(tree_path) = linked_tree { self.attr_link(s, tree_path); } // class self.attr_class_begin(s, has_children); if !branch.attributes.classes.branch.is_empty() { self.attr_class_push(s, &branch.attributes.classes.branch); } if branch.attributes.stage == Stage::Draft { self.attr_class_push(s, "draft"); } self.attr_class_end(s); // th-do-not-persist if branch.attributes.do_not_persist { self.attr_do_not_persist(s); } } self.end_attrs(s); let open = self.begin_container(s, has_children, branch.kind); { self.bullet_point(s); self.branch_content(s, &self.preprocess_markup(branch_id), linked_tree); let date_time = branch.attributes.timestamp(); let link_button = match linked_tree { Some(_) => LinkButton::Tree, None => LinkButton::Branch, }; let link = match linked_tree { Some(tree_path) => format!("{}/{}", self.config().site, tree_path), None => format!("{}/b?{}", self.config().site, &branch.named_id), }; self.button_bar(s, date_time, link_button, &link); if self.begin_children(s, &open) == HasChildren::Yes { self.branch_children(s, branch_id); } } self.close_branch(s, open); } pub fn root(&self, s: &mut String) { let roots = self .treehouse() .roots .get(&self.file_id) .expect("tree should have been added to the treehouse"); s.push_str(""); } } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] struct ChildPage { timestamp: Option>, title: String, icon: String, tree_path: VPathBuf, } impl Renderer<'_> { fn get_child_pages(&self, parent_page: &VPath) -> Vec { let mut child_pages = vfs::entries(&self.dirs.content, parent_page); child_pages.retain(|path| matches!(path.extension(), Some("tree"))); for child_page in &mut child_pages { child_page.set_extension(""); } child_pages.sort(); child_pages.dedup(); let mut child_pages: Vec<_> = child_pages .into_iter() .filter_map(|tree_path| { self.treehouse() .files_by_tree_path .get(&tree_path) .and_then(|file_id| { let roots = &self.treehouse().roots[file_id]; let visible = roots.attributes.visibility == Visibility::Public; visible.then(|| ChildPage { tree_path, title: roots.attributes.title.clone(), icon: roots.attributes.icon.clone(), timestamp: roots.attributes.timestamps.as_ref().map(|t| t.updated), }) }) }) .collect(); child_pages.sort_by(|a, b| b.cmp(a)); child_pages } }