generate subpage listings automatically
right now very barebones! - doesn't sort pages quite correctly - no search function - still not sure about the UI design aspects includes refactor of tree generation code
This commit is contained in:
parent
9a86b5f98e
commit
fbb9f39353
8 changed files with 508 additions and 187 deletions
|
@ -161,9 +161,9 @@ fn extract_entries(sources: &Sources, dirs: &Dirs, file_id: FileId) -> Vec<Entry
|
||||||
let mut summary = String::new();
|
let mut summary = String::new();
|
||||||
branches_to_html_simple(&mut summary, sources, dirs, file_id, &branch.children);
|
branches_to_html_simple(&mut summary, sources, dirs, file_id, &branch.children);
|
||||||
|
|
||||||
let updated = Ulid::from_string(&branch.attributes.id)
|
let updated = branch
|
||||||
.ok()
|
.attributes
|
||||||
.and_then(|ulid| DateTime::from_timestamp_millis(ulid.timestamp_ms() as i64))
|
.timestamp()
|
||||||
.unwrap_or(DateTime::UNIX_EPOCH); // if you see the Unix epoch... oops
|
.unwrap_or(DateTime::UNIX_EPOCH); // if you see the Unix epoch... oops
|
||||||
|
|
||||||
parsed.link.map(|url| Entry {
|
parsed.link.map(|url| Entry {
|
||||||
|
|
|
@ -6,7 +6,7 @@ use tracing::{info_span, instrument};
|
||||||
use crate::{
|
use crate::{
|
||||||
dirs::Dirs,
|
dirs::Dirs,
|
||||||
generate::BaseTemplateData,
|
generate::BaseTemplateData,
|
||||||
html::{breadcrumbs::breadcrumbs_to_html, tree::branches_to_html},
|
html::{breadcrumbs::breadcrumbs_to_html, tree},
|
||||||
sources::Sources,
|
sources::Sources,
|
||||||
state::FileId,
|
state::FileId,
|
||||||
};
|
};
|
||||||
|
@ -51,16 +51,14 @@ pub fn generate(
|
||||||
.expect("tree should have been added to the treehouse");
|
.expect("tree should have been added to the treehouse");
|
||||||
|
|
||||||
let tree = {
|
let tree = {
|
||||||
let _span = info_span!("generate_tree::branches_to_html").entered();
|
let _span = info_span!("generate_tree::root_to_html").entered();
|
||||||
let mut tree = String::new();
|
let renderer = tree::Renderer {
|
||||||
branches_to_html(
|
sources,
|
||||||
&mut tree,
|
|
||||||
&sources.treehouse,
|
|
||||||
&sources.config,
|
|
||||||
dirs,
|
dirs,
|
||||||
file_id,
|
file_id,
|
||||||
&roots.branches,
|
};
|
||||||
);
|
let mut tree = String::new();
|
||||||
|
renderer.root(&mut tree);
|
||||||
tree
|
tree
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,192 +1,233 @@
|
||||||
use std::{borrow::Cow, fmt::Write};
|
use std::fmt::Write;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use treehouse_format::pull::BranchKind;
|
use treehouse_format::pull::BranchKind;
|
||||||
use ulid::Ulid;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
dirs::Dirs,
|
dirs::Dirs,
|
||||||
html::EscapeAttribute,
|
html::EscapeAttribute,
|
||||||
|
sources::Sources,
|
||||||
state::{FileId, Treehouse},
|
state::{FileId, Treehouse},
|
||||||
tree::{
|
tree::{
|
||||||
attributes::{Content, Stage},
|
attributes::{Content, Stage, Visibility},
|
||||||
mini_template, SemaBranchId,
|
mini_template, SemaBranchId,
|
||||||
},
|
},
|
||||||
|
vfs::{self, VPath, VPathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{djot, EscapeHtml};
|
use super::{djot, EscapeHtml};
|
||||||
|
|
||||||
pub fn branch_to_html(
|
pub struct Renderer<'a> {
|
||||||
s: &mut String,
|
pub sources: &'a Sources,
|
||||||
treehouse: &Treehouse,
|
pub dirs: &'a Dirs,
|
||||||
config: &Config,
|
pub file_id: FileId,
|
||||||
dirs: &Dirs,
|
}
|
||||||
file_id: FileId,
|
|
||||||
branch_id: SemaBranchId,
|
|
||||||
) {
|
|
||||||
let source = treehouse.source(file_id);
|
|
||||||
let branch = treehouse.tree.branch(branch_id);
|
|
||||||
|
|
||||||
if !cfg!(debug_assertions) && branch.attributes.stage == Stage::Draft {
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
return;
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
let has_children = !branch.children.is_empty()
|
fn config(&self) -> &Config {
|
||||||
|| matches!(branch.attributes.content, Content::ResolvedLink(_));
|
&self.sources.config
|
||||||
|
|
||||||
let class = if has_children { "branch" } else { "leaf" };
|
|
||||||
let mut class = String::from(class);
|
|
||||||
if !branch.attributes.classes.branch.is_empty() {
|
|
||||||
class.push(' ');
|
|
||||||
class.push_str(&branch.attributes.classes.branch);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if branch.attributes.stage == Stage::Draft {
|
fn open_branch(&self, s: &mut String, id: &str) {
|
||||||
class.push_str(" draft");
|
write!(s, "<li id=\"{}\"", EscapeAttribute(id)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let component = if let Content::ResolvedLink(_) = branch.attributes.content {
|
fn attr(&self, s: &mut String, key: &'static str, value: &str) {
|
||||||
"b-linked"
|
write!(s, r#" {key}="{}""#, EscapeAttribute(value)).unwrap()
|
||||||
} else {
|
|
||||||
"b"
|
|
||||||
};
|
|
||||||
let component = if !branch.attributes.cast.is_empty() {
|
|
||||||
Cow::Owned(format!("{component} {}", branch.attributes.cast))
|
|
||||||
} else {
|
|
||||||
Cow::Borrowed(component)
|
|
||||||
};
|
|
||||||
|
|
||||||
let linked_branch = if let Content::ResolvedLink(file_id) = &branch.attributes.content {
|
|
||||||
let path = treehouse.tree_path(*file_id).expect(".tree file expected");
|
|
||||||
format!(" th-link=\"{}\"", EscapeHtml(path.as_str()))
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let do_not_persist = if branch.attributes.do_not_persist {
|
|
||||||
" th-do-not-persist"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut data_attributes = String::new();
|
|
||||||
for (key, value) in &branch.attributes.data {
|
|
||||||
write!(
|
|
||||||
data_attributes,
|
|
||||||
" data-{key}=\"{}\"",
|
|
||||||
EscapeAttribute(value)
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn attr_class_begin(&self, s: &mut String, has_children: HasChildren) {
|
||||||
write!(
|
write!(
|
||||||
s,
|
s,
|
||||||
"<li data-cast=\"{component}\" class=\"{class}\" id=\"{}\"{linked_branch}{do_not_persist}{data_attributes}>",
|
r#" class="{}"#,
|
||||||
EscapeAttribute(&branch.html_id)
|
EscapeAttribute(match has_children {
|
||||||
|
HasChildren::Yes => "branch",
|
||||||
|
HasChildren::No => "leaf",
|
||||||
|
})
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
{
|
}
|
||||||
if has_children {
|
|
||||||
s.push_str(match branch.kind {
|
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::Expanded => "<details open>",
|
||||||
BranchKind::Collapsed => "<details>",
|
BranchKind::Collapsed => "<details>",
|
||||||
});
|
});
|
||||||
s.push_str("<summary class=\"branch-container\">");
|
s.push_str("<summary class=\"branch-container\">");
|
||||||
} else {
|
}
|
||||||
|
HasChildren::No => {
|
||||||
s.push_str("<div class=\"branch-container\">");
|
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>");
|
s.push_str("<th-bp></th-bp>");
|
||||||
|
|
||||||
let raw_block_content = &source.input()[branch.content.clone()];
|
|
||||||
let mut final_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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final_markup.push_str(&line[space_count..]);
|
fn branch_content(&self, s: &mut String, markup: &str, linked: Option<&VPath>) {
|
||||||
final_markup.push('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
if branch.attributes.template {
|
|
||||||
final_markup = mini_template::render(config, treehouse, dirs, &final_markup);
|
|
||||||
}
|
|
||||||
s.push_str("<th-bc>");
|
s.push_str("<th-bc>");
|
||||||
|
|
||||||
let events: Vec<_> = jotdown::Parser::new(&final_markup)
|
let events: Vec<_> = jotdown::Parser::new(markup).into_offset_iter().collect();
|
||||||
.into_offset_iter()
|
|
||||||
.collect();
|
|
||||||
// TODO: Report rendering diagnostics.
|
// TODO: Report rendering diagnostics.
|
||||||
let render_diagnostics = djot::Renderer {
|
let render_diagnostics = djot::Renderer {
|
||||||
page_id: treehouse
|
page_id: self
|
||||||
.tree_path(file_id)
|
.treehouse()
|
||||||
|
.tree_path(self.file_id)
|
||||||
.expect(".tree file expected")
|
.expect(".tree file expected")
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
|
||||||
config,
|
config: self.config(),
|
||||||
dirs,
|
dirs: self.dirs,
|
||||||
|
|
||||||
treehouse,
|
treehouse: self.treehouse(),
|
||||||
file_id,
|
file_id: self.file_id,
|
||||||
}
|
}
|
||||||
.render(&events, s);
|
.render(&events, s);
|
||||||
|
|
||||||
let branch = treehouse.tree.branch(branch_id);
|
if let Some(linked) = linked {
|
||||||
if let Content::ResolvedLink(file_id) = &branch.attributes.content {
|
|
||||||
let path = treehouse.tree_path(*file_id).expect(".tree file expected");
|
|
||||||
write!(
|
write!(
|
||||||
s,
|
s,
|
||||||
"<noscript><a class=\"navigate icon-go\" href=\"{}/{}\">Go to linked tree: <code>{}</code></a></noscript>",
|
"<noscript><a class=\"navigate icon-go\" href=\"{}/{}\">Go to linked tree: <code>{}</code></a></noscript>",
|
||||||
EscapeAttribute(&config.site),
|
EscapeAttribute(&self.config().site),
|
||||||
EscapeAttribute(path.as_str()),
|
EscapeAttribute(linked.as_str()),
|
||||||
EscapeHtml(path.as_str()),
|
EscapeHtml(linked.as_str()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
s.push_str("</th-bc>");
|
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>");
|
s.push_str("<th-bb>");
|
||||||
{
|
{
|
||||||
if let Some(date_time) = Ulid::from_string(&branch.attributes.id)
|
if let Some(date_time) = date_time {
|
||||||
.ok()
|
|
||||||
.as_ref()
|
|
||||||
.map(Ulid::timestamp_ms)
|
|
||||||
.and_then(|ms| DateTime::from_timestamp_millis(ms as i64))
|
|
||||||
{
|
|
||||||
write!(s, "<th-bd>{}</th-bd>", date_time.format("%F")).unwrap();
|
write!(s, "<th-bd>{}</th-bd>", date_time.format("%F")).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Content::ResolvedLink(file_id) = &branch.attributes.content {
|
match link_button {
|
||||||
let path = treehouse.tree_path(*file_id).expect(".tree file expected");
|
LinkButton::Tree => {
|
||||||
write!(
|
write!(
|
||||||
s,
|
s,
|
||||||
"<a class=\"icon icon-go\" href=\"{}/{}\" title=\"linked tree\"></a>",
|
"<a class=\"icon icon-go\" href=\"{}\" title=\"linked tree\"></a>",
|
||||||
EscapeAttribute(&config.site),
|
EscapeAttribute(link)
|
||||||
EscapeAttribute(path.as_str()),
|
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
} else {
|
}
|
||||||
|
LinkButton::Branch => {
|
||||||
write!(
|
write!(
|
||||||
s,
|
s,
|
||||||
"<a th-p class=\"icon icon-permalink\" href=\"/b?{}\" title=\"permalink\"></a>",
|
"<a th-p class=\"icon icon-permalink\" href=\"{}\" title=\"permalink\"></a>",
|
||||||
EscapeAttribute(&branch.named_id)
|
EscapeAttribute(link)
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
s.push_str("</th-bb>");
|
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);
|
||||||
|
|
||||||
if has_children {
|
|
||||||
s.push_str("</summary>");
|
|
||||||
{
|
|
||||||
s.push_str("<ul");
|
s.push_str("<ul");
|
||||||
if !branch.attributes.classes.branch_children.is_empty() {
|
if !branch.attributes.classes.branch_children.is_empty() {
|
||||||
write!(
|
write!(
|
||||||
|
@ -199,30 +240,227 @@ pub fn branch_to_html(
|
||||||
s.push('>');
|
s.push('>');
|
||||||
let num_children = branch.children.len();
|
let num_children = branch.children.len();
|
||||||
for i in 0..num_children {
|
for i in 0..num_children {
|
||||||
let child_id = treehouse.tree.branch(branch_id).children[i];
|
let child_id = self.treehouse().tree.branch(branch_id).children[i];
|
||||||
branch_to_html(s, treehouse, config, dirs, file_id, child_id);
|
self.branch(s, child_id);
|
||||||
}
|
}
|
||||||
s.push_str("</ul>");
|
s.push_str("</ul>");
|
||||||
}
|
}
|
||||||
s.push_str("</details>");
|
|
||||||
|
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 {
|
} else {
|
||||||
s.push_str("</div>");
|
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.html_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("</li>");
|
||||||
|
}
|
||||||
|
|
||||||
|
s.push_str("</ul>");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn branches_to_html(
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
s: &mut String,
|
struct ChildPage {
|
||||||
treehouse: &Treehouse,
|
timestamp: Option<DateTime<Utc>>,
|
||||||
config: &Config,
|
title: String,
|
||||||
dirs: &Dirs,
|
icon: String,
|
||||||
file_id: FileId,
|
tree_path: VPathBuf,
|
||||||
branches: &[SemaBranchId],
|
}
|
||||||
) {
|
|
||||||
s.push_str("<ul>");
|
impl Renderer<'_> {
|
||||||
for &child in branches {
|
fn get_child_pages(&self, parent_page: &VPath) -> Vec<ChildPage> {
|
||||||
branch_to_html(s, treehouse, config, dirs, file_id, child);
|
let mut child_pages = vfs::entries(&self.dirs.content, parent_page);
|
||||||
}
|
child_pages.retain(|path| matches!(path.extension(), Some("tree")));
|
||||||
s.push_str("</ul>");
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ pub mod mini_template;
|
||||||
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use attributes::Timestamps;
|
||||||
use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle, Severity};
|
use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle, Severity};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use treehouse_format::{
|
use treehouse_format::{
|
||||||
|
@ -56,7 +57,7 @@ impl SemaRoots {
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
roots: Roots,
|
roots: Roots,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
let mut sema_roots = Self {
|
||||||
attributes: Self::parse_attributes(treehouse, diagnostics, config, file_id, &roots),
|
attributes: Self::parse_attributes(treehouse, diagnostics, config, file_id, &roots),
|
||||||
branches: roots
|
branches: roots
|
||||||
.branches
|
.branches
|
||||||
|
@ -65,8 +66,26 @@ impl SemaRoots {
|
||||||
SemaBranch::from_branch(treehouse, diagnostics, config, file_id, branch)
|
SemaBranch::from_branch(treehouse, diagnostics, config, file_id, branch)
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if sema_roots.attributes.timestamps.is_none() {
|
||||||
|
let mut timestamps = None;
|
||||||
|
for &branch_id in &sema_roots.branches {
|
||||||
|
let branch = treehouse.tree.branch(branch_id);
|
||||||
|
if let Some(timestamp) = branch.attributes.timestamp() {
|
||||||
|
let timestamps = timestamps.get_or_insert(Timestamps {
|
||||||
|
created: timestamp,
|
||||||
|
updated: timestamp,
|
||||||
|
});
|
||||||
|
timestamps.created = timestamps.created.min(timestamp);
|
||||||
|
timestamps.updated = timestamps.updated.max(timestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sema_roots.attributes.timestamps = timestamps;
|
||||||
|
}
|
||||||
|
|
||||||
|
sema_roots
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_attributes(
|
fn parse_attributes(
|
||||||
treehouse: &mut Treehouse,
|
treehouse: &mut Treehouse,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::collections::HashMap;
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
use crate::{state::FileId, vfs::VPathBuf};
|
use crate::{state::FileId, vfs::VPathBuf};
|
||||||
|
|
||||||
|
@ -18,6 +18,11 @@ pub struct RootAttributes {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
|
||||||
|
/// Page icon used in indexes.
|
||||||
|
/// This is an emoji name, such as `page` (default).
|
||||||
|
#[serde(default = "default_icon")]
|
||||||
|
pub icon: String,
|
||||||
|
|
||||||
/// Summary of the generated .html page.
|
/// Summary of the generated .html page.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
@ -36,6 +41,15 @@ pub struct RootAttributes {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub styles: Vec<String>,
|
pub styles: Vec<String>,
|
||||||
|
|
||||||
|
/// Visibility of a page in the parent page's index.
|
||||||
|
#[serde(default)]
|
||||||
|
pub visibility: Visibility,
|
||||||
|
|
||||||
|
/// The page's timestamps. These are automatically populated if a page has at least one branch
|
||||||
|
/// with an ID that includes a timestamp.
|
||||||
|
#[serde(default)]
|
||||||
|
pub timestamps: Option<Timestamps>,
|
||||||
|
|
||||||
/// When specified, this page will have a corresponding Atom feed under `rss/{feed}.xml`.
|
/// When specified, this page will have a corresponding Atom feed under `rss/{feed}.xml`.
|
||||||
///
|
///
|
||||||
/// In feeds, top-level branches are expected to have a single heading containing the post title.
|
/// In feeds, top-level branches are expected to have a single heading containing the post title.
|
||||||
|
@ -55,6 +69,28 @@ pub struct Picture {
|
||||||
pub alt: Option<String>,
|
pub alt: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Visibility of a page.
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
pub enum Visibility {
|
||||||
|
#[default]
|
||||||
|
Public,
|
||||||
|
/// Hidden from the parent page's index.
|
||||||
|
Private,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Timestamps for a page.
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
pub struct Timestamps {
|
||||||
|
/// When the page was created. By default, this is the timestamp of the least recent branch.
|
||||||
|
pub created: DateTime<Utc>,
|
||||||
|
/// When the page was last updated. By default, this is the timestamp of the most recent branch.
|
||||||
|
pub updated: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_icon() -> String {
|
||||||
|
String::from("page")
|
||||||
|
}
|
||||||
|
|
||||||
/// Branch attributes.
|
/// Branch attributes.
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
|
||||||
pub struct Attributes {
|
pub struct Attributes {
|
||||||
|
@ -96,16 +132,24 @@ pub struct Attributes {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub cast: String,
|
pub cast: String,
|
||||||
|
|
||||||
/// List of extra `data` attributes to add to the block.
|
|
||||||
#[serde(default)]
|
|
||||||
pub data: HashMap<String, String>,
|
|
||||||
|
|
||||||
/// In feeds, specifies the list of tags to attach to an entry.
|
/// In feeds, specifies the list of tags to attach to an entry.
|
||||||
/// This only has an effect on top-level branches.
|
/// This only has an effect on top-level branches.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Attributes {
|
||||||
|
/// Parses the timestamp out of the branch's ID.
|
||||||
|
/// Returns `None` if the ID does not contain a timestamp.
|
||||||
|
pub fn timestamp(&self) -> Option<DateTime<Utc>> {
|
||||||
|
Ulid::from_string(&self.id)
|
||||||
|
.ok()
|
||||||
|
.as_ref()
|
||||||
|
.map(Ulid::timestamp_ms)
|
||||||
|
.and_then(|ms| DateTime::from_timestamp_millis(ms as i64))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Controls for block content presentation.
|
/// Controls for block content presentation.
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
|
|
|
@ -225,7 +225,7 @@ impl VPathBuf {
|
||||||
|
|
||||||
let range = self.path.len() - chop_len..;
|
let range = self.path.len() - chop_len..;
|
||||||
self.path.replace_range(range, new_extension);
|
self.path.replace_range(range, new_extension);
|
||||||
} else {
|
} else if !new_extension.is_empty() {
|
||||||
self.path.push('.');
|
self.path.push('.');
|
||||||
self.path.push_str(new_extension);
|
self.path.push_str(new_extension);
|
||||||
}
|
}
|
||||||
|
|
|
@ -326,7 +326,7 @@ th-bd {
|
||||||
|
|
||||||
/* Hide branch dates on very small displays. No clue how to fix this just yet. */
|
/* Hide branch dates on very small displays. No clue how to fix this just yet. */
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
th-bb .branch-date {
|
th-bd {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -397,6 +397,23 @@ th-bd {
|
||||||
opacity: 80%;
|
opacity: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Children containers */
|
||||||
|
|
||||||
|
.tree li.child-pages {
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
padding-top: 0.8rem;
|
||||||
|
padding-bottom: 0.8rem;
|
||||||
|
border-top: 0.1rem solid var(--border-1);
|
||||||
|
|
||||||
|
& > ul {
|
||||||
|
/* Show child page lists without an indent.
|
||||||
|
Visually they belong to the root of the page. */
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* branch-quote class for "air quote branches"; used to separate a subtree from a parent tree
|
/* branch-quote class for "air quote branches"; used to separate a subtree from a parent tree
|
||||||
stylistically such that it's interpretable as a form of block quote. */
|
stylistically such that it's interpretable as a form of block quote. */
|
||||||
ul.branch-quote {
|
ul.branch-quote {
|
||||||
|
|
|
@ -73,9 +73,14 @@ export class Branch {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ulid.isCanonicalUlid(this.namedID)) {
|
|
||||||
// Adjust dates to fit the user's time zone.
|
// Adjust dates to fit the user's time zone.
|
||||||
let timestamp = ulid.getTimestamp(this.namedID);
|
let timestamp = null;
|
||||||
|
if (element.hasAttribute("th-ts")) {
|
||||||
|
timestamp = new Date(parseInt(element.getAttribute("th-ts")));
|
||||||
|
} else if (ulid.isCanonicalUlid(this.namedID)) {
|
||||||
|
timestamp = ulid.getTimestamp(this.namedID);
|
||||||
|
}
|
||||||
|
if (timestamp != null) {
|
||||||
let branchDate = this.buttonBar.getElementsByTagName("th-bd")[0];
|
let branchDate = this.buttonBar.getElementsByTagName("th-bd")[0];
|
||||||
branchDate.textContent = dateToString(timestamp);
|
branchDate.textContent = dateToString(timestamp);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue