tree update!

This commit is contained in:
liquidex 2024-01-18 22:46:57 +01:00
parent 26b6056dbc
commit 5f86f4cee7
18 changed files with 431 additions and 179 deletions

View file

@ -0,0 +1,10 @@
%% title = "404"
% id = "404"
- # 404
% id = "01HMF8KQ997F1ZTEGDNAE2S6F1"
- seems like the page you're looking for isn't here.
% id = "01HMF8KQ99XNMEP67NE3QH5698"
- care to go [back to the index][branch:treehouse]?

21
content/_treehouse/b.tree Normal file
View file

@ -0,0 +1,21 @@
%% title = "GET /b"
% id = "b"
- # GET /b?<span class="http-request-parameter">branch</span>
<style>
.http-request-parameter { opacity: 80%; --recursive-wght: 700; }
</style>
% id = "01HMF8KQ990KC8Q08XYSKTV4TQ"
- this endpoint takes you to the <span class="http-request-parameter">branch</span> with the given ID
% id = "01HMF8KQ99VBWQSG1Y8NDTM8QA"
- it also includes proper OpenGraph metadata for the page, unlike the raw .html files.
therefore it's used for permalinks (those on the far right side of the branch →)
% id = "01HMF8KQ99KWR1K9QHKPYY2K15"
+ c'mon, [give it a whirl](/b?the-end-is-never)
% id = "01HMF8KQ99WX9P6D05T5VYBSKK"
- <https://www.youtube.com/watch?v=8Kban1IOQ4M>

View file

@ -1,3 +1,5 @@
%% title = "liquidex's treehouse"
% id = "treehouse" % id = "treehouse"
- # liquidex's treehouse - # liquidex's treehouse
<span class="oops-you-seem-to-have-gotten-stuck"> <span class="oops-you-seem-to-have-gotten-stuck">

View file

@ -7,11 +7,14 @@ use crate::{
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Roots { pub struct Roots {
pub attributes: Option<Attributes>,
pub branches: Vec<Branch>, pub branches: Vec<Branch>,
} }
impl Roots { impl Roots {
pub fn parse(parser: &mut Parser) -> Result<Self, ParseError> { pub fn parse(parser: &mut Parser) -> Result<Self, ParseError> {
let attributes = parser.top_level_attributes()?;
let mut branches = vec![]; let mut branches = vec![];
while let Some((branch, indent_level)) = Branch::parse_with_indent_level(parser)? { while let Some((branch, indent_level)) = Branch::parse_with_indent_level(parser)? {
if indent_level != 0 { if indent_level != 0 {
@ -19,7 +22,10 @@ impl Roots {
} }
branches.push(branch); branches.push(branch);
} }
Ok(Self { branches }) Ok(Self {
attributes,
branches,
})
} }
} }

View file

@ -151,6 +151,34 @@ impl<'a> Parser<'a> {
Ok(()) Ok(())
} }
pub fn top_level_attributes(&mut self) -> Result<Option<Attributes>, ParseError> {
let start = self.position;
match self.current() {
Some('%') => {
let after_one_percent = self.position;
self.advance();
if self.current() == Some('%') {
self.advance();
let after_two_percent = self.position;
self.eat_indented_lines_until(
0,
|c| c == '-' || c == '+' || c == '%',
AllowCodeBlocks::No,
)?;
let end = self.position;
Ok(Some(Attributes {
percent: start..after_two_percent,
data: after_two_percent..end,
}))
} else {
self.position = after_one_percent;
Ok(None)
}
}
_ => Ok(None),
}
}
pub fn next_branch(&mut self) -> Result<Option<BranchEvent>, ParseError> { pub fn next_branch(&mut self) -> Result<Option<BranchEvent>, ParseError> {
if self.current().is_none() { if self.current().is_none() {
return Ok(None); return Ok(None);

View file

@ -4,7 +4,7 @@ use anyhow::Context;
use treehouse_format::ast::Branch; use treehouse_format::ast::Branch;
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::state::{FileId, Treehouse}; use crate::state::{FileId, Source, Treehouse};
use super::{ use super::{
parse::{self, parse_toml_with_diagnostics, parse_tree_with_diagnostics}, parse::{self, parse_toml_with_diagnostics, parse_tree_with_diagnostics},
@ -106,7 +106,7 @@ pub fn fix_file(
file_id: FileId, file_id: FileId,
) -> Result<String, parse::ErrorsEmitted> { ) -> Result<String, parse::ErrorsEmitted> {
parse_tree_with_diagnostics(treehouse, file_id).map(|roots| { parse_tree_with_diagnostics(treehouse, file_id).map(|roots| {
let mut source = treehouse.source(file_id).to_owned(); let mut source = treehouse.source(file_id).input().to_owned();
let mut state = State::default(); let mut state = State::default();
for branch in &roots.branches { for branch in &roots.branches {
@ -130,14 +130,14 @@ pub fn fix_file_cli(fix_args: FixArgs) -> anyhow::Result<()> {
let file = std::fs::read_to_string(&fix_args.file).context("cannot read file to fix")?; let file = std::fs::read_to_string(&fix_args.file).context("cannot read file to fix")?;
let mut treehouse = Treehouse::new(); let mut treehouse = Treehouse::new();
let file_id = treehouse.add_file(utf8_filename, None, file); let file_id = treehouse.add_file(utf8_filename, Source::Other(file));
if let Ok(fixed) = fix_file(&mut treehouse, file_id) { if let Ok(fixed) = fix_file(&mut treehouse, file_id) {
if fix_args.apply { if fix_args.apply {
// Try to write the backup first. If writing that fails, bail out without overwriting // Try to write the backup first. If writing that fails, bail out without overwriting
// the source file. // the source file.
if let Some(backup_path) = fix_args.backup { if let Some(backup_path) = fix_args.backup {
std::fs::write(backup_path, treehouse.source(file_id)) std::fs::write(backup_path, treehouse.source(file_id).input())
.context("cannot write backup; original file will not be overwritten")?; .context("cannot write backup; original file will not be overwritten")?;
} }
std::fs::write(&fix_args.file, fixed).context("cannot overwrite original file")?; std::fs::write(&fix_args.file, fixed).context("cannot overwrite original file")?;
@ -160,10 +160,10 @@ pub fn fix_all_cli(fix_all_args: FixAllArgs, paths: &Paths<'_>) -> anyhow::Resul
let utf8_filename = entry.path().to_string_lossy(); let utf8_filename = entry.path().to_string_lossy();
let mut treehouse = Treehouse::new(); let mut treehouse = Treehouse::new();
let file_id = treehouse.add_file(utf8_filename.into_owned(), None, file); let file_id = treehouse.add_file(utf8_filename.into_owned(), Source::Other(file));
if let Ok(fixed) = fix_file(&mut treehouse, file_id) { if let Ok(fixed) = fix_file(&mut treehouse, file_id) {
if fixed != treehouse.source(file_id) { if fixed != treehouse.source(file_id).input() {
if fix_all_args.apply { if fix_all_args.apply {
println!("fixing: {:?}", entry.path()); println!("fixing: {:?}", entry.path());
std::fs::write(entry.path(), fixed).with_context(|| { std::fs::write(entry.path(), fixed).with_context(|| {

View file

@ -5,17 +5,14 @@ use std::{
}; };
use anyhow::{bail, Context}; use anyhow::{bail, Context};
use axum::Router;
use codespan_reporting::{ use codespan_reporting::{
diagnostic::{Diagnostic, Label, LabelStyle, Severity}, diagnostic::{Diagnostic, Label, LabelStyle, Severity},
files::Files as _, files::Files as _,
}; };
use copy_dir::copy_dir; use copy_dir::copy_dir;
use handlebars::Handlebars; use handlebars::Handlebars;
use log::{debug, info}; use log::{debug, error, info};
use serde::Serialize; use serde::Serialize;
use tower_http::services::ServeDir;
use tower_livereload::LiveReloadLayer;
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::{ use crate::{
@ -26,6 +23,7 @@ use crate::{
navmap::{build_navigation_map, NavigationMap}, navmap::{build_navigation_map, NavigationMap},
tree::branches_to_html, tree::branches_to_html,
}, },
state::Source,
tree::SemaRoots, tree::SemaRoots,
}; };
@ -63,7 +61,8 @@ impl Generator {
) -> anyhow::Result<FileId> { ) -> anyhow::Result<FileId> {
let source = std::fs::read_to_string(path) let source = std::fs::read_to_string(path)
.with_context(|| format!("cannot read template file {path:?}"))?; .with_context(|| format!("cannot read template file {path:?}"))?;
let file_id = treehouse.add_file(path.to_string_lossy().into_owned(), None, source); let file_id =
treehouse.add_file(path.to_string_lossy().into_owned(), Source::Other(source));
let source = treehouse.source(file_id); let source = treehouse.source(file_id);
if let Err(error) = handlebars.register_template_string(name, source) { if let Err(error) = handlebars.register_template_string(name, source) {
Self::wrangle_handlebars_error_into_diagnostic( Self::wrangle_handlebars_error_into_diagnostic(
@ -136,9 +135,17 @@ impl Generator {
continue; continue;
} }
}; };
let tree_path = tree_path.with_extension("").to_string_lossy().replace('\\', "/"); let tree_path = tree_path
let file_id = .with_extension("")
treehouse.add_file(utf8_filename.into_owned(), Some(tree_path.clone()), source); .to_string_lossy()
.replace('\\', "/");
let file_id = treehouse.add_file(
utf8_filename.into_owned(),
Source::Tree {
input: source,
tree_path: tree_path.clone(),
},
);
if let Ok(roots) = parse_tree_with_diagnostics(&mut treehouse, file_id) { if let Ok(roots) = parse_tree_with_diagnostics(&mut treehouse, file_id) {
let roots = SemaRoots::from_roots(&mut treehouse, file_id, roots); let roots = SemaRoots::from_roots(&mut treehouse, file_id, roots);
@ -186,20 +193,30 @@ impl Generator {
parsed_tree.file_id, parsed_tree.file_id,
&roots.branches, &roots.branches,
); );
treehouse.roots.insert(parsed_tree.tree_path, roots);
#[derive(Serialize)]
pub struct Page {
pub title: String,
pub breadcrumbs: String,
pub tree: String,
}
#[derive(Serialize)] #[derive(Serialize)]
pub struct TemplateData<'a> { pub struct TemplateData<'a> {
pub config: &'a Config, pub config: &'a Config,
pub breadcrumbs: String, pub page: Page,
pub tree: String,
} }
let template_data = TemplateData { let template_data = TemplateData {
config, config,
breadcrumbs, page: Page {
tree, title: roots.attributes.title.clone(),
breadcrumbs,
tree,
},
}; };
treehouse.roots.insert(parsed_tree.tree_path, roots);
let templated_html = match handlebars.render("tree", &template_data) { let templated_html = match handlebars.render("tree", &template_data) {
Ok(html) => html, Ok(html) => html,
Err(error) => { Err(error) => {
@ -227,7 +244,7 @@ impl Generator {
} }
} }
pub fn regenerate(paths: &Paths<'_>) -> anyhow::Result<()> { pub fn generate(paths: &Paths<'_>) -> anyhow::Result<Treehouse> {
let start = Instant::now(); let start = Instant::now();
info!("loading config"); info!("loading config");
@ -268,26 +285,19 @@ pub fn regenerate(paths: &Paths<'_>) -> anyhow::Result<()> {
let duration = start.elapsed(); let duration = start.elapsed();
info!("generation done in {duration:?}"); info!("generation done in {duration:?}");
Ok(()) if !treehouse.has_errors() {
} Ok(treehouse)
} else {
pub fn regenerate_or_report_error(paths: &Paths<'_>) { bail!("generation errors occurred; diagnostics were emitted with detailed descriptions");
info!("regenerating site content");
match regenerate(paths) {
Ok(_) => (),
Err(error) => eprintln!("error: {error:?}"),
} }
} }
pub async fn web_server(port: u16) -> anyhow::Result<()> { pub fn regenerate_or_report_error(paths: &Paths<'_>) -> anyhow::Result<Treehouse> {
let app = Router::new().nest_service("/", ServeDir::new("target/site")); info!("regenerating site content");
#[cfg(debug_assertions)] let result = generate(paths);
let app = app.layer(LiveReloadLayer::new()); if let Err(e) = &result {
error!("{e:?}");
info!("serving on port {port}"); }
Ok(axum::Server::bind(&([0, 0, 0, 0], port).into()) result
.serve(app.into_make_service())
.await?)
} }

View file

@ -1,6 +1,7 @@
pub mod fix; pub mod fix;
pub mod generate; pub mod generate;
mod parse; mod parse;
pub mod serve;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -24,14 +25,21 @@ pub enum Command {
/// ///
/// By default only prints which files would be changed. To apply the changes, use `--apply`. /// By default only prints which files would be changed. To apply the changes, use `--apply`.
FixAll(#[clap(flatten)] FixAllArgs), FixAll(#[clap(flatten)] FixAllArgs),
/// `generate` and start a treehouse server.
///
/// The server uses the generated files and provides extra functionality on top, handling
Serve {
#[clap(flatten)]
generate: GenerateArgs,
#[clap(flatten)]
serve: ServeArgs,
},
} }
#[derive(Args)] #[derive(Args)]
pub struct GenerateArgs { pub struct GenerateArgs {}
/// Start a web server serving the static files on the given port. Useful with `cargo watch`.
#[clap(short, long)]
pub serve: Option<u16>,
}
#[derive(Args)] #[derive(Args)]
pub struct FixArgs { pub struct FixArgs {
@ -57,6 +65,13 @@ pub struct FixAllArgs {
pub apply: bool, pub apply: bool,
} }
#[derive(Args)]
pub struct ServeArgs {
/// The port under which to serve the treehouse.
#[clap(short, long, default_value_t = 8080)]
pub port: u16,
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct Paths<'a> { pub struct Paths<'a> {
pub target_dir: &'a Path, pub target_dir: &'a Path,

View file

@ -11,7 +11,7 @@ pub fn parse_tree_with_diagnostics(
treehouse: &mut Treehouse, treehouse: &mut Treehouse,
file_id: FileId, file_id: FileId,
) -> Result<Roots, ErrorsEmitted> { ) -> Result<Roots, ErrorsEmitted> {
let input = treehouse.source(file_id); let input = &treehouse.source(file_id).input();
Roots::parse(&mut treehouse_format::pull::Parser { input, position: 0 }).map_err(|error| { Roots::parse(&mut treehouse_format::pull::Parser { input, position: 0 }).map_err(|error| {
treehouse.diagnostics.push(Diagnostic { treehouse.diagnostics.push(Diagnostic {
severity: Severity::Error, severity: Severity::Error,
@ -34,7 +34,7 @@ pub fn parse_toml_with_diagnostics(
file_id: FileId, file_id: FileId,
range: Range<usize>, range: Range<usize>,
) -> Result<toml_edit::Document, ErrorsEmitted> { ) -> Result<toml_edit::Document, ErrorsEmitted> {
let input = &treehouse.source(file_id)[range.clone()]; let input = &treehouse.source(file_id).input()[range.clone()];
toml_edit::Document::from_str(input).map_err(|error| { toml_edit::Document::from_str(input).map_err(|error| {
treehouse treehouse
.diagnostics .diagnostics

View file

@ -0,0 +1,86 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::Context;
use axum::{
extract::{RawQuery, State},
response::Html,
routing::get,
Router,
};
use log::{error, info};
use pulldown_cmark::escape::escape_html;
use tower_http::services::ServeDir;
use crate::state::{Source, Treehouse};
use super::Paths;
struct SystemPages {
four_oh_four: String,
b_docs: String,
}
struct Server {
treehouse: Treehouse,
target_dir: PathBuf,
system_pages: SystemPages,
}
pub async fn serve(treehouse: Treehouse, paths: &Paths<'_>, port: u16) -> anyhow::Result<()> {
let app = Router::new()
.nest_service("/", ServeDir::new(paths.target_dir))
.route("/b", get(branch))
.with_state(Arc::new(Server {
treehouse,
target_dir: paths.target_dir.to_owned(),
system_pages: SystemPages {
four_oh_four: std::fs::read_to_string(paths.target_dir.join("_treehouse/404.html"))
.context("cannot read 404 page")?,
b_docs: std::fs::read_to_string(paths.target_dir.join("_treehouse/b.html"))
.context("cannot read /b documentation page")?,
},
}));
#[cfg(debug_assertions)]
let app = app.layer(tower_livereload::LiveReloadLayer::new());
info!("serving on port {port}");
Ok(axum::Server::bind(&([0, 0, 0, 0], port).into())
.serve(app.into_make_service())
.await?)
}
async fn branch(RawQuery(named_id): RawQuery, State(state): State<Arc<Server>>) -> Html<String> {
if let Some(named_id) = named_id {
if let Some(&branch_id) = state.treehouse.branches_by_named_id.get(&named_id) {
let branch = state.treehouse.tree.branch(branch_id);
if let Source::Tree { input, tree_path } = state.treehouse.source(branch.file_id) {
let file_path = state.target_dir.join(format!("{tree_path}.html"));
match std::fs::read_to_string(&file_path) {
Ok(content) => {
let branch_markdown_content = input[branch.content.clone()].trim();
let mut per_page_metadata =
String::from("<meta property=\"og:description\" content=\"");
escape_html(&mut per_page_metadata, branch_markdown_content).unwrap();
per_page_metadata.push_str("\">");
const PER_PAGE_METADATA_REPLACEMENT_STRING: &str = "<!-- treehouse-ca37057a-cff5-45b3-8415-3b02dbf6c799-per-branch-metadata -->";
return Html(content.replacen(
PER_PAGE_METADATA_REPLACEMENT_STRING,
&per_page_metadata,
// Replace one under the assumption that it appears in all pages.
1,
));
}
Err(e) => {
error!("error while reading file {file_path:?}: {e:?}");
}
}
}
}
Html(state.system_pages.four_oh_four.clone())
} else {
Html(state.system_pages.b_docs.clone())
}
}

View file

@ -61,7 +61,7 @@ pub fn branch_to_html(
s.push_str("<div>"); s.push_str("<div>");
} }
let raw_block_content = &source[branch.content.clone()]; let raw_block_content = &source.input()[branch.content.clone()];
let mut unindented_block_content = String::with_capacity(raw_block_content.len()); let mut unindented_block_content = String::with_capacity(raw_block_content.len());
for line in raw_block_content.lines() { for line in raw_block_content.lines() {
// Bit of a jank way to remove at most branch.indent_level spaces from the front. // Bit of a jank way to remove at most branch.indent_level spaces from the front.
@ -93,7 +93,11 @@ pub fn branch_to_html(
.get(linked) .get(linked)
.map(|&branch_id| { .map(|&branch_id| {
( (
format!("#{}", treehouse.tree.branch(branch_id).html_id).into(), format!(
"/b?{}",
treehouse.tree.branch(branch_id).attributes.id
)
.into(),
"".into(), "".into(),
) )
}), }),
@ -144,8 +148,8 @@ pub fn branch_to_html(
write!( write!(
s, s,
"<a class=\"icon icon-permalink\" href=\"#{}\" title=\"permalink\"></a>", "<a class=\"icon icon-permalink\" href=\"/b?{}\" title=\"permalink\"></a>",
EscapeAttribute(&branch.html_id) EscapeAttribute(&branch.attributes.id)
) )
.unwrap(); .unwrap();
} }

View file

@ -3,10 +3,11 @@ use std::path::Path;
use clap::Parser; use clap::Parser;
use cli::{ use cli::{
fix::{fix_all_cli, fix_file_cli}, fix::{fix_all_cli, fix_file_cli},
generate::{self, regenerate_or_report_error}, generate::regenerate_or_report_error,
serve::serve,
Command, Paths, ProgramArgs, Command, Paths, ProgramArgs,
}; };
use log::{error, info}; use log::{error, info, warn};
mod cli; mod cli;
mod config; mod config;
@ -30,14 +31,17 @@ async fn fallible_main() -> anyhow::Result<()> {
}; };
match args.command { match args.command {
Command::Generate(regenerate_args) => { Command::Generate(_generate_args) => {
info!("regenerating using directories: {paths:#?}"); info!("regenerating using directories: {paths:#?}");
regenerate_or_report_error(&paths)?;
regenerate_or_report_error(&paths); warn!("`generate` is for debugging only and the files cannot be fully served using a static file server; use `treehouse serve` if you wish to start a treehouse server");
}
if let Some(port) = regenerate_args.serve { Command::Serve {
generate::web_server(port).await?; generate: _,
} serve: serve_args,
} => {
let treehouse = regenerate_or_report_error(&paths)?;
serve(treehouse, &paths, serve_args.port).await?;
} }
Command::Fix(fix_args) => fix_file_cli(fix_args)?, Command::Fix(fix_args) => fix_file_cli(fix_args)?,

View file

@ -10,7 +10,28 @@ use ulid::Ulid;
use crate::tree::{SemaBranchId, SemaRoots, SemaTree}; use crate::tree::{SemaBranchId, SemaRoots, SemaTree};
pub type Files = SimpleFiles<String, String>; #[derive(Debug, Clone)]
pub enum Source {
Tree { input: String, tree_path: String },
Other(String),
}
impl Source {
pub fn input(&self) -> &str {
match &self {
Source::Tree { input, .. } => input,
Source::Other(source) => source,
}
}
}
impl AsRef<str> for Source {
fn as_ref(&self) -> &str {
self.input()
}
}
pub type Files = SimpleFiles<String, Source>;
pub type FileId = <Files as codespan_reporting::files::Files<'static>>::FileId; pub type FileId = <Files as codespan_reporting::files::Files<'static>>::FileId;
/// Treehouse compilation context. /// Treehouse compilation context.
@ -22,19 +43,9 @@ pub struct Treehouse {
pub branches_by_named_id: HashMap<String, SemaBranchId>, pub branches_by_named_id: HashMap<String, SemaBranchId>,
pub roots: HashMap<String, SemaRoots>, pub roots: HashMap<String, SemaRoots>,
// Bit of a hack because I don't wanna write my own `Files`.
tree_paths: Vec<Option<String>>,
missingno_generator: ulid::Generator, missingno_generator: ulid::Generator,
} }
#[derive(Debug, Clone)]
pub struct BranchRef {
pub html_id: String,
pub file_id: FileId,
pub kind_span: Range<usize>,
}
impl Treehouse { impl Treehouse {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@ -45,25 +56,16 @@ impl Treehouse {
branches_by_named_id: HashMap::new(), branches_by_named_id: HashMap::new(),
roots: HashMap::new(), roots: HashMap::new(),
tree_paths: vec![],
missingno_generator: ulid::Generator::new(), missingno_generator: ulid::Generator::new(),
} }
} }
pub fn add_file( pub fn add_file(&mut self, filename: String, source: Source) -> FileId {
&mut self, self.files.add(filename, source)
filename: String,
tree_path: Option<String>,
source: String,
) -> FileId {
let id = self.files.add(filename, source);
self.tree_paths.push(tree_path);
id
} }
/// Get the source code of a file, assuming it was previously registered. /// Get the source code of a file, assuming it was previously registered.
pub fn source(&self, file_id: FileId) -> &str { pub fn source(&self, file_id: FileId) -> &Source {
self.files self.files
.get(file_id) .get(file_id)
.expect("file should have been registered previously") .expect("file should have been registered previously")
@ -79,7 +81,10 @@ impl Treehouse {
} }
pub fn tree_path(&self, file_id: FileId) -> Option<&str> { pub fn tree_path(&self, file_id: FileId) -> Option<&str> {
self.tree_paths[file_id].as_deref() match self.source(file_id) {
Source::Tree { tree_path, .. } => Some(tree_path),
Source::Other(_) => None,
}
} }
pub fn report_diagnostics(&self) -> anyhow::Result<()> { pub fn report_diagnostics(&self) -> anyhow::Result<()> {
@ -98,6 +103,12 @@ impl Treehouse {
.generate() .generate()
.expect("just how much disk space do you have?") .expect("just how much disk space do you have?")
} }
pub fn has_errors(&self) -> bool {
self.diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == Severity::Error)
}
} }
pub struct TomlError { pub struct TomlError {

View file

@ -1,5 +1,15 @@
use serde::Deserialize; use serde::Deserialize;
/// Top-level `%%` root attributes.
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
pub struct RootAttributes {
/// Title of the generated .html page.
///
/// The page's tree path is used if empty.
#[serde(default)]
pub title: String,
}
/// Branch attributes. /// Branch attributes.
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
pub struct Attributes { pub struct Attributes {

View file

@ -9,10 +9,12 @@ use treehouse_format::{
}; };
use crate::{ use crate::{
state::{toml_error_to_diagnostic, FileId, TomlError, Treehouse}, state::{toml_error_to_diagnostic, FileId, Source, TomlError, Treehouse},
tree::attributes::{Attributes, Content}, tree::attributes::{Attributes, Content},
}; };
use self::attributes::RootAttributes;
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct SemaTree { pub struct SemaTree {
branches: Vec<SemaBranch>, branches: Vec<SemaBranch>,
@ -35,12 +37,14 @@ impl SemaTree {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SemaRoots { pub struct SemaRoots {
pub attributes: RootAttributes,
pub branches: Vec<SemaBranchId>, pub branches: Vec<SemaBranchId>,
} }
impl SemaRoots { impl SemaRoots {
pub fn from_roots(treehouse: &mut Treehouse, file_id: FileId, roots: Roots) -> Self { pub fn from_roots(treehouse: &mut Treehouse, file_id: FileId, roots: Roots) -> Self {
Self { Self {
attributes: Self::parse_attributes(treehouse, file_id, &roots),
branches: roots branches: roots
.branches .branches
.into_iter() .into_iter()
@ -48,6 +52,44 @@ impl SemaRoots {
.collect(), .collect(),
} }
} }
fn parse_attributes(
treehouse: &mut Treehouse,
file_id: FileId,
roots: &Roots,
) -> RootAttributes {
let source = treehouse.source(file_id);
let mut successfully_parsed = true;
let mut attributes = if let Some(attributes) = &roots.attributes {
toml_edit::de::from_str(&source.input()[attributes.data.clone()]).unwrap_or_else(
|error| {
treehouse
.diagnostics
.push(toml_error_to_diagnostic(TomlError {
message: error.message().to_owned(),
span: error.span(),
file_id,
input_range: attributes.data.clone(),
}));
successfully_parsed = false;
RootAttributes::default()
},
)
} else {
RootAttributes::default()
};
let successfully_parsed = successfully_parsed;
if successfully_parsed && attributes.title.is_empty() {
attributes.title = match treehouse.source(file_id) {
Source::Tree { tree_path, .. } => tree_path.clone(),
_ => panic!("parse_attributes called for a non-.tree file"),
}
}
attributes
}
} }
/// Analyzed branch. /// Analyzed branch.
@ -132,18 +174,20 @@ impl SemaBranch {
let mut successfully_parsed = true; let mut successfully_parsed = true;
let mut attributes = if let Some(attributes) = &branch.attributes { let mut attributes = if let Some(attributes) = &branch.attributes {
toml_edit::de::from_str(&source[attributes.data.clone()]).unwrap_or_else(|error| { toml_edit::de::from_str(&source.input()[attributes.data.clone()]).unwrap_or_else(
treehouse |error| {
.diagnostics treehouse
.push(toml_error_to_diagnostic(TomlError { .diagnostics
message: error.message().to_owned(), .push(toml_error_to_diagnostic(TomlError {
span: error.span(), message: error.message().to_owned(),
file_id, span: error.span(),
input_range: attributes.data.clone(), file_id,
})); input_range: attributes.data.clone(),
successfully_parsed = false; }));
Attributes::default() successfully_parsed = false;
}) Attributes::default()
},
)
} else { } else {
Attributes::default() Attributes::default()
}; };

View file

@ -1,3 +1,27 @@
/*** Icons ***/
:root {
--icon-breadcrumb: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTYgMTIgNC00LTQtNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNTU0MjNlIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=');
--icon-expand: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggY2xpcC1ydWxlPSJldmVub2RkIiBkPSJtNyA1di0zaC0ydjNoLTN2MmgzdjNoMnYtM2gzdi0yeiIgZmlsbD0iIzU1NDIzZSIgZmlsbC1vcGFjaXR5PSIuNSIgZmlsbC1ydWxlPSJldmVub2RkIi8+PC9zdmc+');
--icon-leaf: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iNiIgY3k9IjYiIGZpbGw9IiM1NTQyM2UiIGZpbGwtb3BhY2l0eT0iLjUiIHI9IjIiLz48L3N2Zz4=');
--icon-collapse: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTMgNmg2IiBzdHJva2U9IiM1NTQyM2UiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
--icon-more: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTQgNiA0IDQgNC00IiBmaWxsPSJub25lIiBzdHJva2U9IiM1NTQyM2UiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
--icon-permalink: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTcuNjU2ODYgMiAxLjQxNDIxIDEuNDE0MjJjLjY4MDUxLjY4MDUxIDEuMDY0NTMgMS41NDUyMSAxLjE1MjEzIDIuNDMzNjIuODg4NC4wODc2IDEuNzUzMS40NzE2NSAyLjQzMzcgMS4xNTIxNmwxLjQxNDIgMS40MTQyMmMxLjU2MjEgMS41NjIwOSAxLjU2MjEgNC4wOTQ3OCAwIDUuNjU2ODhzLTQuMDk0NzkgMS41NjIxLTUuNjU2ODggMGwtMS40MTQyMi0xLjQxNDJjLS42ODA1MS0uNjgwNi0xLjA2NDU2LTEuNTQ1My0xLjE1MjE2LTIuNDMzNy0uODg4NDEtLjA4NzYtMS43NTMxMS0uNDcxNjItMi40MzM2Mi0xLjE1MjEzbC0xLjQxNDIyLTEuNDE0MjFjLTEuNTYyMDk0LTEuNTYyMS0xLjU2MjA5NC00LjA5NDc2IDAtNS42NTY4NiAxLjU2MjEtMS41NjIwOTQgNC4wOTQ3Ni0xLjU2MjA5NCA1LjY1Njg2IDB6bS42MTggNy42ODkwN2MtLjE0NDMuMDg1MjItLjI5MjgxLjE2MDYxLS40NDQ1NS4yMjYxNi4wMjA4My40ODI1Ny4yMTU0Ni45NTg5Ny41ODM5MSAxLjMyNzM3bDEuNDE0MjEgMS40MTQzYy43ODEwNy43ODEgMi4wNDczNy43ODEgMi44Mjg0NyAwIC43ODEtLjc4MTEuNzgxLTIuMDQ3NCAwLTIuODI4NDdsLTEuNDE0My0xLjQxNDIxYy0uMzY4NC0uMzY4NDUtLjg0NDgtLjU2MzA4LTEuMzI3MzctLjU4MzkxLS4wNjU1NS4xNTE3My0uMTQwOTMuMzAwMjQtLjIyNjE2LjQ0NDU0bDEuODQ2NDMgMS44NDY0NS0xLjQxNDIgMS40MTQyem0tLjYxOC00Ljg2MDY0Yy4zNjg0NC4zNjg0NS41NjMwOC44NDQ4OC41ODM5MSAxLjMyNzQyLS4xNTE3NC4wNjU1NC0uMzAwMjQuMTQwOTMtLjQ0NDU0LjIyNjE1bC0xLjkxNzU0LTEuOTE3NTMtMS40MTQyMSAxLjQxNDIxIDEuOTE3NTMgMS45MTc1M2MtLjA4NTIzLjE0NDMxLS4xNjA2MS4yOTI4Mi0uMjI2MTYuNDQ0NTYtLjQ4MjU0LS4wMjA4My0uOTU4OTctLjIxNTQ3LTEuMzI3NDItLjU4MzkxbC0xLjQxNDIxLTEuNDE0MjJjLS43ODEwNS0uNzgxMDUtLjc4MTA1LTIuMDQ3MzcgMC0yLjgyODQyczIuMDQ3MzctLjc4MTA1IDIuODI4NDIgMHoiIGZpbGw9IiM1NTQyM2UiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg==");
--icon-go: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTEwLjU4NTggNy0yLjI5Mjg5LTIuMjkyODkgMS40MTQyMS0xLjQxNDIyIDQuNzA3MDggNC43MDcxMS00LjcwNzA4IDQuNzA3MS0xLjQxNDIxLTEuNDE0MiAyLjI5Mjg5LTIuMjkyOWgtNy41ODU4di0yeiIgZmlsbD0iIzU1NDIzZSIvPjwvc3ZnPg==");
}
@media (prefers-color-scheme: dark) {
:root {
--icon-breadcrumb: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTYgMTIgNC00LTQtNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDdjZGJmIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=');
--icon-expand: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggY2xpcC1ydWxlPSJldmVub2RkIiBkPSJtNyA1di0zaC0ydjNoLTN2MmgzdjNoMnYtM2gzdi0yeiIgZmlsbD0iI2Q3Y2RiZiIgZmlsbC1vcGFjaXR5PSIuNSIgZmlsbC1ydWxlPSJldmVub2RkIi8+PC9zdmc+');
--icon-leaf: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iNiIgY3k9IjYiIGZpbGw9IiNkN2NkYmYiIGZpbGwtb3BhY2l0eT0iLjUiIHI9IjIiLz48L3N2Zz4=');
--icon-collapse: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTMgNmg2IiBzdHJva2U9IiNkN2NkYmYiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
--icon-permalink: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTcuNjU2ODYgMiAxLjQxNDIxIDEuNDE0MjJjLjY4MDUxLjY4MDUxIDEuMDY0NTMgMS41NDUyMSAxLjE1MjEzIDIuNDMzNjIuODg4NC4wODc2IDEuNzUzMS40NzE2NSAyLjQzMzcgMS4xNTIxNmwxLjQxNDIgMS40MTQyMmMxLjU2MjEgMS41NjIwOSAxLjU2MjEgNC4wOTQ3OCAwIDUuNjU2ODhzLTQuMDk0NzkgMS41NjIxLTUuNjU2ODggMGwtMS40MTQyMi0xLjQxNDJjLS42ODA1MS0uNjgwNi0xLjA2NDU2LTEuNTQ1My0xLjE1MjE2LTIuNDMzNy0uODg4NDEtLjA4NzYtMS43NTMxMS0uNDcxNjItMi40MzM2Mi0xLjE1MjEzbC0xLjQxNDIyLTEuNDE0MjFjLTEuNTYyMDk0LTEuNTYyMS0xLjU2MjA5NC00LjA5NDc2IDAtNS42NTY4NiAxLjU2MjEtMS41NjIwOTQgNC4wOTQ3Ni0xLjU2MjA5NCA1LjY1Njg2IDB6bS42MTggNy42ODkwN2MtLjE0NDMuMDg1MjItLjI5MjgxLjE2MDYxLS40NDQ1NS4yMjYxNi4wMjA4My40ODI1Ny4yMTU0Ni45NTg5Ny41ODM5MSAxLjMyNzM3bDEuNDE0MjEgMS40MTQzYy43ODEwNy43ODEgMi4wNDczNy43ODEgMi44Mjg0NyAwIC43ODEtLjc4MTEuNzgxLTIuMDQ3NCAwLTIuODI4NDdsLTEuNDE0My0xLjQxNDIxYy0uMzY4NC0uMzY4NDUtLjg0NDgtLjU2MzA4LTEuMzI3MzctLjU4MzkxLS4wNjU1NS4xNTE3My0uMTQwOTMuMzAwMjQtLjIyNjE2LjQ0NDU0bDEuODQ2NDMgMS44NDY0NS0xLjQxNDIgMS40MTQyem0tLjYxOC00Ljg2MDY0Yy4zNjg0NC4zNjg0NS41NjMwOC44NDQ4OC41ODM5MSAxLjMyNzQyLS4xNTE3NC4wNjU1NC0uMzAwMjQuMTQwOTMtLjQ0NDU0LjIyNjE1bC0xLjkxNzU0LTEuOTE3NTMtMS40MTQyMSAxLjQxNDIxIDEuOTE3NTMgMS45MTc1M2MtLjA4NTIzLjE0NDMxLS4xNjA2MS4yOTI4Mi0uMjI2MTYuNDQ0NTYtLjQ4MjU0LS4wMjA4My0uOTU4OTctLjIxNTQ3LTEuMzI3NDItLjU4MzkxbC0xLjQxNDIxLTEuNDE0MjJjLS43ODEwNS0uNzgxMDUtLjc4MTA1LTIuMDQ3MzcgMC0yLjgyODQyczIuMDQ3MzctLjc4MTA1IDIuODI4NDIgMHoiIGZpbGw9IiNkN2NkYmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg==");
--icon-go: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTEwLjU4NTggNy0yLjI5Mjg5LTIuMjkyODkgMS40MTQyMS0xLjQxNDIyIDQuNzA3MDggNC43MDcxMS00LjcwNzA4IDQuNzA3MS0xLjQxNDIxLTEuNDE0MiAyLjI5Mjg5LTIuMjkyOWgtNy41ODU4di0yeiIgZmlsbD0iI2Q3Y2RiZiIvPjwvc3ZnPg==");
--icon-more: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTQgNiA0IDQgNC00IiBmaWxsPSJub25lIiBzdHJva2U9IiNkN2NkYmYiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
}
}
/*** Breadcrumbs ***/ /*** Breadcrumbs ***/
.breadcrumbs { .breadcrumbs {
@ -22,7 +46,7 @@
background-image: background-image:
/* breadcrumb */ /* breadcrumb */
url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTYgMTIgNC00LTQtNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNTU0MjNlIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4='); var(--icon-breadcrumb);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 50% 50%; background-position: 50% 50%;
opacity: 70%; opacity: 70%;
@ -107,9 +131,7 @@
} }
.tree details>summary { .tree details>summary {
background-image: background-image: var(--icon-expand);
/* expand */
url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggY2xpcC1ydWxlPSJldmVub2RkIiBkPSJtNyA1di0zaC0ydjNoLTN2MmgzdjNoMnYtM2gzdi0yeiIgZmlsbD0iIzU1NDIzZSIgZmlsbC1vcGFjaXR5PSIuNSIgZmlsbC1ydWxlPSJldmVub2RkIi8+PC9zdmc+');
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: var(--tree-icon-position); background-position: var(--tree-icon-position);
padding-left: var(--tree-icon-space); padding-left: var(--tree-icon-space);
@ -127,9 +149,7 @@
} }
.tree li>div { .tree li>div {
background-image: background-image: var(--icon-leaf);
/* leaf */
url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iNiIgY3k9IjYiIGZpbGw9IiM1NTQyM2UiIGZpbGwtb3BhY2l0eT0iLjUiIHI9IjIiLz48L3N2Zz4=');
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: var(--tree-icon-position); background-position: var(--tree-icon-position);
padding-left: var(--tree-icon-space); padding-left: var(--tree-icon-space);
@ -139,18 +159,14 @@
} }
.tree details[open]>summary { .tree details[open]>summary {
background-image: background-image: var(--icon-collapse);
/* collapse */
url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTMgNmg2IiBzdHJva2U9IiM1NTQyM2UiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
} }
.tree details:not([open])>summary>.branch-summary>:last-child::after { .tree details:not([open])>summary>.branch-summary>:last-child::after {
content: '\00A0'; content: '\00A0';
display: inline-block; display: inline-block;
background-image: background-image: var(--icon-more);
/* more */
url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTQgNiA0IDQgNC00IiBmaWxsPSJub25lIiBzdHJva2U9IiM1NTQyM2UiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 50% 50%; background-position: 50% 50%;
@ -217,15 +233,11 @@
.tree .icon-permalink { .tree .icon-permalink {
background-image: background-image: var(--icon-permalink);
/* permalink */
url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTcuNjU2ODYgMiAxLjQxNDIxIDEuNDE0MjJjLjY4MDUxLjY4MDUxIDEuMDY0NTMgMS41NDUyMSAxLjE1MjEzIDIuNDMzNjIuODg4NC4wODc2IDEuNzUzMS40NzE2NSAyLjQzMzcgMS4xNTIxNmwxLjQxNDIgMS40MTQyMmMxLjU2MjEgMS41NjIwOSAxLjU2MjEgNC4wOTQ3OCAwIDUuNjU2ODhzLTQuMDk0NzkgMS41NjIxLTUuNjU2ODggMGwtMS40MTQyMi0xLjQxNDJjLS42ODA1MS0uNjgwNi0xLjA2NDU2LTEuNTQ1My0xLjE1MjE2LTIuNDMzNy0uODg4NDEtLjA4NzYtMS43NTMxMS0uNDcxNjItMi40MzM2Mi0xLjE1MjEzbC0xLjQxNDIyLTEuNDE0MjFjLTEuNTYyMDk0LTEuNTYyMS0xLjU2MjA5NC00LjA5NDc2IDAtNS42NTY4NiAxLjU2MjEtMS41NjIwOTQgNC4wOTQ3Ni0xLjU2MjA5NCA1LjY1Njg2IDB6bS42MTggNy42ODkwN2MtLjE0NDMuMDg1MjItLjI5MjgxLjE2MDYxLS40NDQ1NS4yMjYxNi4wMjA4My40ODI1Ny4yMTU0Ni45NTg5Ny41ODM5MSAxLjMyNzM3bDEuNDE0MjEgMS40MTQzYy43ODEwNy43ODEgMi4wNDczNy43ODEgMi44Mjg0NyAwIC43ODEtLjc4MTEuNzgxLTIuMDQ3NCAwLTIuODI4NDdsLTEuNDE0My0xLjQxNDIxYy0uMzY4NC0uMzY4NDUtLjg0NDgtLjU2MzA4LTEuMzI3MzctLjU4MzkxLS4wNjU1NS4xNTE3My0uMTQwOTMuMzAwMjQtLjIyNjE2LjQ0NDU0bDEuODQ2NDMgMS44NDY0NS0xLjQxNDIgMS40MTQyem0tLjYxOC00Ljg2MDY0Yy4zNjg0NC4zNjg0NS41NjMwOC44NDQ4OC41ODM5MSAxLjMyNzQyLS4xNTE3NC4wNjU1NC0uMzAwMjQuMTQwOTMtLjQ0NDU0LjIyNjE1bC0xLjkxNzU0LTEuOTE3NTMtMS40MTQyMSAxLjQxNDIxIDEuOTE3NTMgMS45MTc1M2MtLjA4NTIzLjE0NDMxLS4xNjA2MS4yOTI4Mi0uMjI2MTYuNDQ0NTYtLjQ4MjU0LS4wMjA4My0uOTU4OTctLjIxNTQ3LTEuMzI3NDItLjU4MzkxbC0xLjQxNDIxLTEuNDE0MjJjLS43ODEwNS0uNzgxMDUtLjc4MTA1LTIuMDQ3MzcgMC0yLjgyODQyczIuMDQ3MzctLjc4MTA1IDIuODI4NDIgMHoiIGZpbGw9IiM1NTQyM2UiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg==");
} }
.tree .icon-go { .tree .icon-go {
background-image: background-image: var(--icon-go);
/* go */
url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTEwLjU4NTggNy0yLjI5Mjg5LTIuMjkyODkgMS40MTQyMS0xLjQxNDIyIDQuNzA3MDggNC43MDcxMS00LjcwNzA4IDQuNzA3MS0xLjQxNDIxLTEuNDE0MiAyLjI5Mjg5LTIuMjkyOWgtNy41ODU4di0yeiIgZmlsbD0iIzU1NDIzZSIvPjwvc3ZnPg==");
} }
.tree a.navigate { .tree a.navigate {
@ -241,54 +253,14 @@
opacity: 50%; opacity: 50%;
} }
.tree :target>details>summary, .tree :target,
.tree :target>div { .tree .target {
border-bottom: 1px dashed var(--border-2);
margin-bottom: -1px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
@media (prefers-color-scheme: dark) { &>details>summary,
.breadcrumb::before { &>div {
background-image: border-bottom: 1px dashed var(--border-2);
/* breadcrumb */ margin-bottom: -1px;
url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTYgMTIgNC00LTQtNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDdjZGJmIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=') border-bottom-left-radius: 0;
} border-bottom-right-radius: 0;
.tree details>summary {
background-image:
/* expand */
url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggY2xpcC1ydWxlPSJldmVub2RkIiBkPSJtNyA1di0zaC0ydjNoLTN2MmgzdjNoMnYtM2gzdi0yeiIgZmlsbD0iI2Q3Y2RiZiIgZmlsbC1vcGFjaXR5PSIuNSIgZmlsbC1ydWxlPSJldmVub2RkIi8+PC9zdmc+');
}
.tree li>div {
background-image:
/* leaf */
url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iNiIgY3k9IjYiIGZpbGw9IiNkN2NkYmYiIGZpbGwtb3BhY2l0eT0iLjUiIHI9IjIiLz48L3N2Zz4=');
}
.tree details[open]>summary {
background-image:
/* collapse */
url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTMgNmg2IiBzdHJva2U9IiNkN2NkYmYiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
}
.tree .icon-permalink {
background-image:
/* permalink */
url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTcuNjU2ODYgMiAxLjQxNDIxIDEuNDE0MjJjLjY4MDUxLjY4MDUxIDEuMDY0NTMgMS41NDUyMSAxLjE1MjEzIDIuNDMzNjIuODg4NC4wODc2IDEuNzUzMS40NzE2NSAyLjQzMzcgMS4xNTIxNmwxLjQxNDIgMS40MTQyMmMxLjU2MjEgMS41NjIwOSAxLjU2MjEgNC4wOTQ3OCAwIDUuNjU2ODhzLTQuMDk0NzkgMS41NjIxLTUuNjU2ODggMGwtMS40MTQyMi0xLjQxNDJjLS42ODA1MS0uNjgwNi0xLjA2NDU2LTEuNTQ1My0xLjE1MjE2LTIuNDMzNy0uODg4NDEtLjA4NzYtMS43NTMxMS0uNDcxNjItMi40MzM2Mi0xLjE1MjEzbC0xLjQxNDIyLTEuNDE0MjFjLTEuNTYyMDk0LTEuNTYyMS0xLjU2MjA5NC00LjA5NDc2IDAtNS42NTY4NiAxLjU2MjEtMS41NjIwOTQgNC4wOTQ3Ni0xLjU2MjA5NCA1LjY1Njg2IDB6bS42MTggNy42ODkwN2MtLjE0NDMuMDg1MjItLjI5MjgxLjE2MDYxLS40NDQ1NS4yMjYxNi4wMjA4My40ODI1Ny4yMTU0Ni45NTg5Ny41ODM5MSAxLjMyNzM3bDEuNDE0MjEgMS40MTQzYy43ODEwNy43ODEgMi4wNDczNy43ODEgMi44Mjg0NyAwIC43ODEtLjc4MTEuNzgxLTIuMDQ3NCAwLTIuODI4NDdsLTEuNDE0My0xLjQxNDIxYy0uMzY4NC0uMzY4NDUtLjg0NDgtLjU2MzA4LTEuMzI3MzctLjU4MzkxLS4wNjU1NS4xNTE3My0uMTQwOTMuMzAwMjQtLjIyNjE2LjQ0NDU0bDEuODQ2NDMgMS44NDY0NS0xLjQxNDIgMS40MTQyem0tLjYxOC00Ljg2MDY0Yy4zNjg0NC4zNjg0NS41NjMwOC44NDQ4OC41ODM5MSAxLjMyNzQyLS4xNTE3NC4wNjU1NC0uMzAwMjQuMTQwOTMtLjQ0NDU0LjIyNjE1bC0xLjkxNzU0LTEuOTE3NTMtMS40MTQyMSAxLjQxNDIxIDEuOTE3NTMgMS45MTc1M2MtLjA4NTIzLjE0NDMxLS4xNjA2MS4yOTI4Mi0uMjI2MTYuNDQ0NTYtLjQ4MjU0LS4wMjA4My0uOTU4OTctLjIxNTQ3LTEuMzI3NDItLjU4MzkxbC0xLjQxNDIxLTEuNDE0MjJjLS43ODEwNS0uNzgxMDUtLjc4MTA1LTIuMDQ3MzcgMC0yLjgyODQyczIuMDQ3MzctLjc4MTA1IDIuODI4NDIgMHoiIGZpbGw9IiNkN2NkYmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg==");
}
.tree .icon-go {
background-image:
/* go */
url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTEwLjU4NTggNy0yLjI5Mjg5LTIuMjkyODkgMS40MTQyMS0xLjQxNDIyIDQuNzA3MDggNC43MDcxMS00LjcwNzA4IDQuNzA3MS0xLjQxNDIxLTEuNDE0MiAyLjI5Mjg5LTIuMjkyOWgtNy41ODU4di0yeiIgZmlsbD0iI2Q3Y2RiZiIvPjwvc3ZnPg==");
}
.tree details:not([open])>summary>.branch-summary>:last-child::after {
background-image:
/* more */
url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTQgNiA0IDQgNC00IiBmaWxsPSJub25lIiBzdHJva2U9IiNkN2NkYmYiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+');
} }
} }

View file

@ -17,6 +17,8 @@ function branchIsOpen(branchID) {
} }
class Branch extends HTMLLIElement { class Branch extends HTMLLIElement {
static branchesByNamedID = new Map();
constructor() { constructor() {
super(); super();
@ -31,6 +33,9 @@ class Branch extends HTMLLIElement {
this.details.addEventListener("toggle", _ => { this.details.addEventListener("toggle", _ => {
saveBranchIsOpen(this.id, this.details.open); saveBranchIsOpen(this.id, this.details.open);
}); });
Branch.branchesByNamedID.set(this.id.split(':')[1], this);
console.log(Branch.branchesByNamedID)
} }
} }
@ -47,8 +52,6 @@ class LinkedBranch extends Branch {
this.linkedTree = this.getAttribute("data-th-link"); this.linkedTree = this.getAttribute("data-th-link");
LinkedBranch.byLink.set(this.linkedTree, this); LinkedBranch.byLink.set(this.linkedTree, this);
this.loadingState = "notloaded";
this.loadingText = document.createElement("p"); this.loadingText = document.createElement("p");
{ {
this.loadingText.className = "link-loading"; this.loadingText.className = "link-loading";
@ -109,8 +112,10 @@ function rehash() { // https://www.youtube.com/watch?v=Tv1SYqLllKI
if (!rehashing) { if (!rehashing) {
rehashing = true; rehashing = true;
let hash = window.location.hash; let hash = window.location.hash;
window.location.hash = ""; if (hash.length > 0) {
window.location.hash = hash; window.location.hash = "";
window.location.hash = hash;
}
rehashing = false; rehashing = false;
} }
} }
@ -183,9 +188,17 @@ async function navigateToBranch(fragment) {
} }
} }
function getCurrentlyHighlightedBranch() {
if (window.location.pathname == "/b" && window.location.search.length > 0) {
let shortID = window.location.search.substring(1);
return Branch.branchesByNamedID.get(shortID).id;
} else {
return window.location.hash.substring(1);
}
}
async function navigateToCurrentBranch() { async function navigateToCurrentBranch() {
let location = window.location.hash.substring(1); await navigateToBranch(getCurrentlyHighlightedBranch());
await navigateToBranch(location);
} }
// When you click on a link, and the destination is within a <details> that is not expanded, // When you click on a link, and the destination is within a <details> that is not expanded,
@ -196,9 +209,9 @@ addEventListener("DOMContentLoaded", navigateToCurrentBranch);
// When you enter the website through a link someone sent you, it would be nice if the linked branch // When you enter the website through a link someone sent you, it would be nice if the linked branch
// got expanded by default. // got expanded by default.
async function expandLinkedBranch() { async function expandLinkedBranch() {
let hash = window.location.hash; let currentlyHighlightedBranch = getCurrentlyHighlightedBranch();
if (hash.length > 0) { if (currentlyHighlightedBranch.length > 0) {
let linkedBranch = document.getElementById(hash.substring(1)); let linkedBranch = document.getElementById(currentlyHighlightedBranch);
if (linkedBranch.children.length > 0 && linkedBranch.children[0].tagName == "DETAILS") { if (linkedBranch.children.length > 0 && linkedBranch.children[0].tagName == "DETAILS") {
expandDetailsRecursively(linkedBranch.children[0]); expandDetailsRecursively(linkedBranch.children[0]);
} }
@ -206,3 +219,12 @@ async function expandLinkedBranch() {
} }
addEventListener("DOMContentLoaded", expandLinkedBranch); addEventListener("DOMContentLoaded", expandLinkedBranch);
async function highlightCurrentBranch() {
let branch = document.getElementById(getCurrentlyHighlightedBranch());
if (branch != null) {
branch.classList.add("target");
}
}
addEventListener("DOMContentLoaded", highlightCurrentBranch);

View file

@ -5,13 +5,19 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ config.user.title }}</title> <title>{{#if (ne page.title config.user.title)}}{{ page.title }} · {{/if}}{{ config.user.title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta property="og:title" content="{{ config.user.title }}">
<meta property="og:site_name" content="{{ config.user.title }}"> <meta property="og:site_name" content="{{ config.user.title }}">
<meta property="og:description" content="{{ config.user.description }}"> <meta property="og:title" content="{{ page.title }}">
{{!--
This is a bit of a hack to quickly insert metadata into generated pages without going through Handlebars, which
would involve registering, parsing, and generating a page from a template.
Yes it would be more flexible that way, but it doesn't need to be.
It just needs to be a string replacement.
--}}
<!-- treehouse-ca37057a-cff5-45b3-8415-3b02dbf6c799-per-branch-metadata -->
<link rel="stylesheet" href="{{ config.site }}/static/css/main.css"> <link rel="stylesheet" href="{{ config.site }}/static/css/main.css">
<link rel="stylesheet" href="{{ config.site }}/static/css/tree.css"> <link rel="stylesheet" href="{{ config.site }}/static/css/tree.css">
@ -34,9 +40,9 @@
</svg> </svg>
</a> </a>
{{#if breadcrumbs}} {{#if page.breadcrumbs}}
<ol class="breadcrumbs"> <ol class="breadcrumbs">
{{{ breadcrumbs }}} {{{ page.breadcrumbs }}}
</ol> </ol>
{{/if}} {{/if}}
</nav> </nav>
@ -57,7 +63,8 @@
if you don't believe me, you're free to inspect the source yourself! all the scripts are written if you don't believe me, you're free to inspect the source yourself! all the scripts are written
lovingly in vanilla JS (not minified!) by yours truly ❤️</p> lovingly in vanilla JS (not minified!) by yours truly ❤️</p>
<small>and if this box is annoying, feel free to block it with uBlock Origin or something. I have no <small>and if this box is annoying, feel free to block it with uBlock Origin or something. I have no
way of remembering you closed it, and don't wanna host this site on a dynamic server.</small> way of remembering you closed it, and don't wanna add a database to this website. simplicity
rules!</small>
</div> </div>
</noscript> </noscript>
@ -70,7 +77,7 @@
</div> </div>
<main class="tree"> <main class="tree">
{{{ tree }}} {{{ page.tree }}}
</main> </main>
</body> </body>