remove treehouse-format crate and collapse everything into src
This commit is contained in:
parent
ca127a9411
commit
b792688776
66 changed files with 145 additions and 112 deletions
467
src/html/tree.rs
Normal file
467
src/html/tree.rs
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
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, "<li id=\"{}\"", EscapeAttribute(id)).unwrap();
|
||||
}
|
||||
|
||||
fn attr(&self, s: &mut String, key: &'static str, value: &str) {
|
||||
write!(s, r#" {key}="{}""#, EscapeAttribute(value)).unwrap()
|
||||
}
|
||||
|
||||
fn attr_class_begin(&self, s: &mut String, has_children: HasChildren) {
|
||||
write!(
|
||||
s,
|
||||
r#" class="{}"#,
|
||||
EscapeAttribute(match has_children {
|
||||
HasChildren::Yes => "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<Utc>) {
|
||||
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 => "<details open>",
|
||||
BranchKind::Collapsed => "<details>",
|
||||
});
|
||||
s.push_str("<summary class=\"branch-container\">");
|
||||
}
|
||||
HasChildren::No => {
|
||||
s.push_str("<div class=\"branch-container\">");
|
||||
}
|
||||
}
|
||||
|
||||
OpenBranch { has_children }
|
||||
}
|
||||
|
||||
fn begin_children(&self, s: &mut String, open: &OpenBranch) -> HasChildren {
|
||||
if open.has_children == HasChildren::Yes {
|
||||
s.push_str("</summary>");
|
||||
}
|
||||
open.has_children
|
||||
}
|
||||
|
||||
fn close_branch(&self, s: &mut String, open: OpenBranch) {
|
||||
match open.has_children {
|
||||
HasChildren::Yes => {
|
||||
s.push_str("</details>");
|
||||
}
|
||||
HasChildren::No => {
|
||||
s.push_str("</div>");
|
||||
}
|
||||
}
|
||||
s.push_str("</li>");
|
||||
}
|
||||
|
||||
fn bullet_point(&self, s: &mut String) {
|
||||
s.push_str("<th-bp></th-bp>");
|
||||
}
|
||||
|
||||
fn branch_content(&self, s: &mut String, markup: &str, linked: Option<&VPath>) {
|
||||
s.push_str("<th-bc>");
|
||||
|
||||
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,
|
||||
"<noscript><a class=\"navigate icon-go\" href=\"{}/{}\">Go to linked tree: <code>{}</code></a></noscript>",
|
||||
EscapeAttribute(&self.config().site),
|
||||
EscapeAttribute(linked.as_str()),
|
||||
EscapeHtml(linked.as_str()),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
s.push_str("</th-bc>");
|
||||
}
|
||||
|
||||
fn button_bar(
|
||||
&self,
|
||||
s: &mut String,
|
||||
date_time: Option<DateTime<Utc>>,
|
||||
link_button: LinkButton,
|
||||
link: &str,
|
||||
) {
|
||||
s.push_str("<th-bb>");
|
||||
{
|
||||
if let Some(date_time) = date_time {
|
||||
write!(s, "<th-bd>{}</th-bd>", date_time.format("%F")).unwrap();
|
||||
}
|
||||
|
||||
match link_button {
|
||||
LinkButton::Tree => {
|
||||
write!(
|
||||
s,
|
||||
"<a class=\"icon icon-go\" href=\"{}\" title=\"linked tree\"></a>",
|
||||
EscapeAttribute(link)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
LinkButton::Branch => {
|
||||
write!(
|
||||
s,
|
||||
"<a th-p class=\"icon icon-permalink\" href=\"{}\" title=\"permalink\"></a>",
|
||||
EscapeAttribute(link)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
s.push_str("</th-bb>");
|
||||
}
|
||||
|
||||
fn branch_children_empty(&self, s: &mut String) {
|
||||
s.push_str("<ul></ul>");
|
||||
}
|
||||
|
||||
fn branch_children(&self, s: &mut String, branch_id: SemaBranchId) {
|
||||
let branch = self.treehouse().tree.branch(branch_id);
|
||||
|
||||
s.push_str("<ul");
|
||||
if !branch.attributes.classes.branch_children.is_empty() {
|
||||
write!(
|
||||
s,
|
||||
" class=\"{}\"",
|
||||
EscapeAttribute(&branch.attributes.classes.branch_children)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
s.push('>');
|
||||
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("</ul>");
|
||||
}
|
||||
|
||||
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("<ul>");
|
||||
for &child in &roots.branches {
|
||||
self.branch(s, child);
|
||||
}
|
||||
|
||||
let path = self.treehouse().path(self.file_id);
|
||||
let children_path = if path == const { VPath::new_const("index.tree") } {
|
||||
VPath::ROOT
|
||||
} else {
|
||||
path
|
||||
};
|
||||
let tree_path = children_path.with_extension("");
|
||||
|
||||
let child_pages = self.get_child_pages(&tree_path);
|
||||
if !child_pages.is_empty() {
|
||||
s.push_str(r#"<li class="child-pages">"#);
|
||||
s.push_str("<ul>");
|
||||
for child_page in &child_pages {
|
||||
self.open_branch(s, &format!("p-{}", child_page.tree_path));
|
||||
{
|
||||
self.attr_cast_begin(s);
|
||||
self.attr_cast_push(s, "b-linked");
|
||||
self.attr_cast_end(s);
|
||||
|
||||
self.attr_link(s, &child_page.tree_path);
|
||||
|
||||
self.attr_class_begin(s, HasChildren::Yes);
|
||||
self.attr_class_end(s);
|
||||
|
||||
if let Some(timestamp) = child_page.timestamp {
|
||||
self.attr_ts(s, timestamp);
|
||||
}
|
||||
}
|
||||
self.end_attrs(s);
|
||||
|
||||
let open = self.begin_container(s, HasChildren::Yes, BranchKind::Collapsed);
|
||||
{
|
||||
self.bullet_point(s);
|
||||
self.branch_content(
|
||||
s,
|
||||
&format!(":{}: {}", child_page.icon, child_page.title),
|
||||
Some(&child_page.tree_path),
|
||||
);
|
||||
self.button_bar(
|
||||
s,
|
||||
child_page.timestamp,
|
||||
LinkButton::Tree,
|
||||
&format!("{}/{}", self.config().site, child_page.tree_path),
|
||||
);
|
||||
self.begin_children(s, &open);
|
||||
self.branch_children_empty(s);
|
||||
}
|
||||
self.close_branch(s, open);
|
||||
}
|
||||
s.push_str("</ul>");
|
||||
s.push_str("</li>");
|
||||
}
|
||||
|
||||
s.push_str("</ul>");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct ChildPage {
|
||||
timestamp: Option<DateTime<Utc>>,
|
||||
title: String,
|
||||
icon: String,
|
||||
tree_path: VPathBuf,
|
||||
}
|
||||
|
||||
impl Renderer<'_> {
|
||||
fn get_child_pages(&self, parent_page: &VPath) -> Vec<ChildPage> {
|
||||
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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue