From 5f86f4cee77db7d546352b82619f8424d6cfddc4 Mon Sep 17 00:00:00 2001 From: lqdev Date: Thu, 18 Jan 2024 22:46:57 +0100 Subject: [PATCH] tree update! --- content/_treehouse/404.tree | 10 +++ content/_treehouse/b.tree | 21 +++++ content/index.tree | 2 + crates/treehouse-format/src/ast.rs | 8 +- crates/treehouse-format/src/pull.rs | 28 +++++++ crates/treehouse/src/cli/fix.rs | 12 +-- crates/treehouse/src/cli/generate.rs | 74 ++++++++++------- crates/treehouse/src/cli/mod.rs | 25 ++++-- crates/treehouse/src/cli/parse.rs | 4 +- crates/treehouse/src/cli/serve.rs | 86 +++++++++++++++++++ crates/treehouse/src/html/tree.rs | 12 ++- crates/treehouse/src/main.rs | 22 +++-- crates/treehouse/src/state.rs | 59 +++++++------ crates/treehouse/src/tree/attributes.rs | 10 +++ crates/treehouse/src/tree/mod.rs | 70 +++++++++++++--- static/css/tree.css | 106 +++++++++--------------- static/js/tree.js | 40 +++++++-- template/tree.hbs | 21 +++-- 18 files changed, 431 insertions(+), 179 deletions(-) create mode 100644 content/_treehouse/404.tree create mode 100644 content/_treehouse/b.tree create mode 100644 crates/treehouse/src/cli/serve.rs diff --git a/content/_treehouse/404.tree b/content/_treehouse/404.tree new file mode 100644 index 0000000..6736920 --- /dev/null +++ b/content/_treehouse/404.tree @@ -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]? diff --git a/content/_treehouse/b.tree b/content/_treehouse/b.tree new file mode 100644 index 0000000..35b7e4e --- /dev/null +++ b/content/_treehouse/b.tree @@ -0,0 +1,21 @@ +%% title = "GET /b" + +% id = "b" +- # GET /b?branch + + + + % id = "01HMF8KQ990KC8Q08XYSKTV4TQ" + - this endpoint takes you to the branch 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" + - diff --git a/content/index.tree b/content/index.tree index 6a0f1d1..d950d75 100644 --- a/content/index.tree +++ b/content/index.tree @@ -1,3 +1,5 @@ +%% title = "liquidex's treehouse" + % id = "treehouse" - # liquidex's treehouse diff --git a/crates/treehouse-format/src/ast.rs b/crates/treehouse-format/src/ast.rs index 1c8f13c..e8e4915 100644 --- a/crates/treehouse-format/src/ast.rs +++ b/crates/treehouse-format/src/ast.rs @@ -7,11 +7,14 @@ use crate::{ #[derive(Debug, Clone)] pub struct Roots { + pub attributes: Option, pub branches: Vec, } impl Roots { pub fn parse(parser: &mut Parser) -> Result { + let attributes = parser.top_level_attributes()?; + let mut branches = vec![]; while let Some((branch, indent_level)) = Branch::parse_with_indent_level(parser)? { if indent_level != 0 { @@ -19,7 +22,10 @@ impl Roots { } branches.push(branch); } - Ok(Self { branches }) + Ok(Self { + attributes, + branches, + }) } } diff --git a/crates/treehouse-format/src/pull.rs b/crates/treehouse-format/src/pull.rs index a282559..dc4aee5 100644 --- a/crates/treehouse-format/src/pull.rs +++ b/crates/treehouse-format/src/pull.rs @@ -151,6 +151,34 @@ impl<'a> Parser<'a> { Ok(()) } + pub fn top_level_attributes(&mut self) -> Result, 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, ParseError> { if self.current().is_none() { return Ok(None); diff --git a/crates/treehouse/src/cli/fix.rs b/crates/treehouse/src/cli/fix.rs index f95c652..8992c0b 100644 --- a/crates/treehouse/src/cli/fix.rs +++ b/crates/treehouse/src/cli/fix.rs @@ -4,7 +4,7 @@ use anyhow::Context; use treehouse_format::ast::Branch; use walkdir::WalkDir; -use crate::state::{FileId, Treehouse}; +use crate::state::{FileId, Source, Treehouse}; use super::{ parse::{self, parse_toml_with_diagnostics, parse_tree_with_diagnostics}, @@ -106,7 +106,7 @@ pub fn fix_file( file_id: FileId, ) -> Result { 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(); 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 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 fix_args.apply { // Try to write the backup first. If writing that fails, bail out without overwriting // the source file. 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")?; } 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 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 fixed != treehouse.source(file_id) { + if fixed != treehouse.source(file_id).input() { if fix_all_args.apply { println!("fixing: {:?}", entry.path()); std::fs::write(entry.path(), fixed).with_context(|| { diff --git a/crates/treehouse/src/cli/generate.rs b/crates/treehouse/src/cli/generate.rs index 56f7eb4..bdd437a 100644 --- a/crates/treehouse/src/cli/generate.rs +++ b/crates/treehouse/src/cli/generate.rs @@ -5,17 +5,14 @@ use std::{ }; use anyhow::{bail, Context}; -use axum::Router; use codespan_reporting::{ diagnostic::{Diagnostic, Label, LabelStyle, Severity}, files::Files as _, }; use copy_dir::copy_dir; use handlebars::Handlebars; -use log::{debug, info}; +use log::{debug, error, info}; use serde::Serialize; -use tower_http::services::ServeDir; -use tower_livereload::LiveReloadLayer; use walkdir::WalkDir; use crate::{ @@ -26,6 +23,7 @@ use crate::{ navmap::{build_navigation_map, NavigationMap}, tree::branches_to_html, }, + state::Source, tree::SemaRoots, }; @@ -63,7 +61,8 @@ impl Generator { ) -> anyhow::Result { let source = std::fs::read_to_string(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); if let Err(error) = handlebars.register_template_string(name, source) { Self::wrangle_handlebars_error_into_diagnostic( @@ -136,9 +135,17 @@ impl Generator { continue; } }; - let tree_path = tree_path.with_extension("").to_string_lossy().replace('\\', "/"); - let file_id = - treehouse.add_file(utf8_filename.into_owned(), Some(tree_path.clone()), source); + let tree_path = tree_path + .with_extension("") + .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) { let roots = SemaRoots::from_roots(&mut treehouse, file_id, roots); @@ -186,20 +193,30 @@ impl Generator { parsed_tree.file_id, &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)] pub struct TemplateData<'a> { pub config: &'a Config, - pub breadcrumbs: String, - pub tree: String, + pub page: Page, } let template_data = TemplateData { config, - breadcrumbs, - tree, + page: Page { + title: roots.attributes.title.clone(), + breadcrumbs, + tree, + }, }; + treehouse.roots.insert(parsed_tree.tree_path, roots); + let templated_html = match handlebars.render("tree", &template_data) { Ok(html) => html, Err(error) => { @@ -227,7 +244,7 @@ impl Generator { } } -pub fn regenerate(paths: &Paths<'_>) -> anyhow::Result<()> { +pub fn generate(paths: &Paths<'_>) -> anyhow::Result { let start = Instant::now(); info!("loading config"); @@ -268,26 +285,19 @@ pub fn regenerate(paths: &Paths<'_>) -> anyhow::Result<()> { let duration = start.elapsed(); info!("generation done in {duration:?}"); - Ok(()) -} - -pub fn regenerate_or_report_error(paths: &Paths<'_>) { - info!("regenerating site content"); - - match regenerate(paths) { - Ok(_) => (), - Err(error) => eprintln!("error: {error:?}"), + if !treehouse.has_errors() { + Ok(treehouse) + } else { + bail!("generation errors occurred; diagnostics were emitted with detailed descriptions"); } } -pub async fn web_server(port: u16) -> anyhow::Result<()> { - let app = Router::new().nest_service("/", ServeDir::new("target/site")); +pub fn regenerate_or_report_error(paths: &Paths<'_>) -> anyhow::Result { + info!("regenerating site content"); - #[cfg(debug_assertions)] - let app = app.layer(LiveReloadLayer::new()); - - info!("serving on port {port}"); - Ok(axum::Server::bind(&([0, 0, 0, 0], port).into()) - .serve(app.into_make_service()) - .await?) + let result = generate(paths); + if let Err(e) = &result { + error!("{e:?}"); + } + result } diff --git a/crates/treehouse/src/cli/mod.rs b/crates/treehouse/src/cli/mod.rs index c0fd3d4..a723752 100644 --- a/crates/treehouse/src/cli/mod.rs +++ b/crates/treehouse/src/cli/mod.rs @@ -1,6 +1,7 @@ pub mod fix; pub mod generate; mod parse; +pub mod serve; 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`. 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)] -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, -} +pub struct GenerateArgs {} #[derive(Args)] pub struct FixArgs { @@ -57,6 +65,13 @@ pub struct FixAllArgs { 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)] pub struct Paths<'a> { pub target_dir: &'a Path, diff --git a/crates/treehouse/src/cli/parse.rs b/crates/treehouse/src/cli/parse.rs index 1f789cf..dc7f090 100644 --- a/crates/treehouse/src/cli/parse.rs +++ b/crates/treehouse/src/cli/parse.rs @@ -11,7 +11,7 @@ pub fn parse_tree_with_diagnostics( treehouse: &mut Treehouse, file_id: FileId, ) -> Result { - 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| { treehouse.diagnostics.push(Diagnostic { severity: Severity::Error, @@ -34,7 +34,7 @@ pub fn parse_toml_with_diagnostics( file_id: FileId, range: Range, ) -> Result { - 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| { treehouse .diagnostics diff --git a/crates/treehouse/src/cli/serve.rs b/crates/treehouse/src/cli/serve.rs new file mode 100644 index 0000000..b6c2626 --- /dev/null +++ b/crates/treehouse/src/cli/serve.rs @@ -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>) -> Html { + 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(""); + + const PER_PAGE_METADATA_REPLACEMENT_STRING: &str = ""; + 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()) + } +} diff --git a/crates/treehouse/src/html/tree.rs b/crates/treehouse/src/html/tree.rs index 24bc325..caa1406 100644 --- a/crates/treehouse/src/html/tree.rs +++ b/crates/treehouse/src/html/tree.rs @@ -61,7 +61,7 @@ pub fn branch_to_html( s.push_str("
"); } - 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()); for line in raw_block_content.lines() { // 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) .map(|&branch_id| { ( - format!("#{}", treehouse.tree.branch(branch_id).html_id).into(), + format!( + "/b?{}", + treehouse.tree.branch(branch_id).attributes.id + ) + .into(), "".into(), ) }), @@ -144,8 +148,8 @@ pub fn branch_to_html( write!( s, - "", - EscapeAttribute(&branch.html_id) + "", + EscapeAttribute(&branch.attributes.id) ) .unwrap(); } diff --git a/crates/treehouse/src/main.rs b/crates/treehouse/src/main.rs index f73736f..4d58165 100644 --- a/crates/treehouse/src/main.rs +++ b/crates/treehouse/src/main.rs @@ -3,10 +3,11 @@ use std::path::Path; use clap::Parser; use cli::{ fix::{fix_all_cli, fix_file_cli}, - generate::{self, regenerate_or_report_error}, + generate::regenerate_or_report_error, + serve::serve, Command, Paths, ProgramArgs, }; -use log::{error, info}; +use log::{error, info, warn}; mod cli; mod config; @@ -30,14 +31,17 @@ async fn fallible_main() -> anyhow::Result<()> { }; match args.command { - Command::Generate(regenerate_args) => { + Command::Generate(_generate_args) => { info!("regenerating using directories: {paths:#?}"); - - regenerate_or_report_error(&paths); - - if let Some(port) = regenerate_args.serve { - generate::web_server(port).await?; - } + 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"); + } + Command::Serve { + 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)?, diff --git a/crates/treehouse/src/state.rs b/crates/treehouse/src/state.rs index 94bad04..4ec4e27 100644 --- a/crates/treehouse/src/state.rs +++ b/crates/treehouse/src/state.rs @@ -10,7 +10,28 @@ use ulid::Ulid; use crate::tree::{SemaBranchId, SemaRoots, SemaTree}; -pub type Files = SimpleFiles; +#[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 for Source { + fn as_ref(&self) -> &str { + self.input() + } +} + +pub type Files = SimpleFiles; pub type FileId = >::FileId; /// Treehouse compilation context. @@ -22,19 +43,9 @@ pub struct Treehouse { pub branches_by_named_id: HashMap, pub roots: HashMap, - // Bit of a hack because I don't wanna write my own `Files`. - tree_paths: Vec>, - missingno_generator: ulid::Generator, } -#[derive(Debug, Clone)] -pub struct BranchRef { - pub html_id: String, - pub file_id: FileId, - pub kind_span: Range, -} - impl Treehouse { pub fn new() -> Self { Self { @@ -45,25 +56,16 @@ impl Treehouse { branches_by_named_id: HashMap::new(), roots: HashMap::new(), - tree_paths: vec![], - missingno_generator: ulid::Generator::new(), } } - pub fn add_file( - &mut self, - filename: String, - tree_path: Option, - source: String, - ) -> FileId { - let id = self.files.add(filename, source); - self.tree_paths.push(tree_path); - id + pub fn add_file(&mut self, filename: String, source: Source) -> FileId { + self.files.add(filename, source) } /// 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 .get(file_id) .expect("file should have been registered previously") @@ -79,7 +81,10 @@ impl Treehouse { } 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<()> { @@ -98,6 +103,12 @@ impl Treehouse { .generate() .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 { diff --git a/crates/treehouse/src/tree/attributes.rs b/crates/treehouse/src/tree/attributes.rs index 080b385..d8e30c3 100644 --- a/crates/treehouse/src/tree/attributes.rs +++ b/crates/treehouse/src/tree/attributes.rs @@ -1,5 +1,15 @@ 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. #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)] pub struct Attributes { diff --git a/crates/treehouse/src/tree/mod.rs b/crates/treehouse/src/tree/mod.rs index 844e9d7..35569f5 100644 --- a/crates/treehouse/src/tree/mod.rs +++ b/crates/treehouse/src/tree/mod.rs @@ -9,10 +9,12 @@ use treehouse_format::{ }; use crate::{ - state::{toml_error_to_diagnostic, FileId, TomlError, Treehouse}, + state::{toml_error_to_diagnostic, FileId, Source, TomlError, Treehouse}, tree::attributes::{Attributes, Content}, }; +use self::attributes::RootAttributes; + #[derive(Debug, Default, Clone)] pub struct SemaTree { branches: Vec, @@ -35,12 +37,14 @@ impl SemaTree { #[derive(Debug, Clone)] pub struct SemaRoots { + pub attributes: RootAttributes, pub branches: Vec, } impl SemaRoots { pub fn from_roots(treehouse: &mut Treehouse, file_id: FileId, roots: Roots) -> Self { Self { + attributes: Self::parse_attributes(treehouse, file_id, &roots), branches: roots .branches .into_iter() @@ -48,6 +52,44 @@ impl SemaRoots { .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. @@ -132,18 +174,20 @@ impl SemaBranch { let mut successfully_parsed = true; let mut attributes = if let Some(attributes) = &branch.attributes { - toml_edit::de::from_str(&source[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; - Attributes::default() - }) + 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; + Attributes::default() + }, + ) } else { Attributes::default() }; diff --git a/static/css/tree.css b/static/css/tree.css index 8a846a0..a268d6c 100644 --- a/static/css/tree.css +++ b/static/css/tree.css @@ -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 { @@ -22,7 +46,7 @@ background-image: /* breadcrumb */ - url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTYgMTIgNC00LTQtNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNTU0MjNlIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4='); + var(--icon-breadcrumb); background-repeat: no-repeat; background-position: 50% 50%; opacity: 70%; @@ -107,9 +131,7 @@ } .tree details>summary { - background-image: - /* expand */ - url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggY2xpcC1ydWxlPSJldmVub2RkIiBkPSJtNyA1di0zaC0ydjNoLTN2MmgzdjNoMnYtM2gzdi0yeiIgZmlsbD0iIzU1NDIzZSIgZmlsbC1vcGFjaXR5PSIuNSIgZmlsbC1ydWxlPSJldmVub2RkIi8+PC9zdmc+'); + background-image: var(--icon-expand); background-repeat: no-repeat; background-position: var(--tree-icon-position); padding-left: var(--tree-icon-space); @@ -127,9 +149,7 @@ } .tree li>div { - background-image: - /* leaf */ - url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iNiIgY3k9IjYiIGZpbGw9IiM1NTQyM2UiIGZpbGwtb3BhY2l0eT0iLjUiIHI9IjIiLz48L3N2Zz4='); + background-image: var(--icon-leaf); background-repeat: no-repeat; background-position: var(--tree-icon-position); padding-left: var(--tree-icon-space); @@ -139,18 +159,14 @@ } .tree details[open]>summary { - background-image: - /* collapse */ - url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjEyIiB3aWR0aD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTMgNmg2IiBzdHJva2U9IiM1NTQyM2UiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+'); + background-image: var(--icon-collapse); } .tree details:not([open])>summary>.branch-summary>:last-child::after { content: '\00A0'; display: inline-block; - background-image: - /* more */ - url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTQgNiA0IDQgNC00IiBmaWxsPSJub25lIiBzdHJva2U9IiM1NTQyM2UiIHN0cm9rZS1vcGFjaXR5PSIuNSIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+'); + background-image: var(--icon-more); background-repeat: no-repeat; background-position: 50% 50%; @@ -217,15 +233,11 @@ .tree .icon-permalink { - background-image: - /* permalink */ - url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTcuNjU2ODYgMiAxLjQxNDIxIDEuNDE0MjJjLjY4MDUxLjY4MDUxIDEuMDY0NTMgMS41NDUyMSAxLjE1MjEzIDIuNDMzNjIuODg4NC4wODc2IDEuNzUzMS40NzE2NSAyLjQzMzcgMS4xNTIxNmwxLjQxNDIgMS40MTQyMmMxLjU2MjEgMS41NjIwOSAxLjU2MjEgNC4wOTQ3OCAwIDUuNjU2ODhzLTQuMDk0NzkgMS41NjIxLTUuNjU2ODggMGwtMS40MTQyMi0xLjQxNDJjLS42ODA1MS0uNjgwNi0xLjA2NDU2LTEuNTQ1My0xLjE1MjE2LTIuNDMzNy0uODg4NDEtLjA4NzYtMS43NTMxMS0uNDcxNjItMi40MzM2Mi0xLjE1MjEzbC0xLjQxNDIyLTEuNDE0MjFjLTEuNTYyMDk0LTEuNTYyMS0xLjU2MjA5NC00LjA5NDc2IDAtNS42NTY4NiAxLjU2MjEtMS41NjIwOTQgNC4wOTQ3Ni0xLjU2MjA5NCA1LjY1Njg2IDB6bS42MTggNy42ODkwN2MtLjE0NDMuMDg1MjItLjI5MjgxLjE2MDYxLS40NDQ1NS4yMjYxNi4wMjA4My40ODI1Ny4yMTU0Ni45NTg5Ny41ODM5MSAxLjMyNzM3bDEuNDE0MjEgMS40MTQzYy43ODEwNy43ODEgMi4wNDczNy43ODEgMi44Mjg0NyAwIC43ODEtLjc4MTEuNzgxLTIuMDQ3NCAwLTIuODI4NDdsLTEuNDE0My0xLjQxNDIxYy0uMzY4NC0uMzY4NDUtLjg0NDgtLjU2MzA4LTEuMzI3MzctLjU4MzkxLS4wNjU1NS4xNTE3My0uMTQwOTMuMzAwMjQtLjIyNjE2LjQ0NDU0bDEuODQ2NDMgMS44NDY0NS0xLjQxNDIgMS40MTQyem0tLjYxOC00Ljg2MDY0Yy4zNjg0NC4zNjg0NS41NjMwOC44NDQ4OC41ODM5MSAxLjMyNzQyLS4xNTE3NC4wNjU1NC0uMzAwMjQuMTQwOTMtLjQ0NDU0LjIyNjE1bC0xLjkxNzU0LTEuOTE3NTMtMS40MTQyMSAxLjQxNDIxIDEuOTE3NTMgMS45MTc1M2MtLjA4NTIzLjE0NDMxLS4xNjA2MS4yOTI4Mi0uMjI2MTYuNDQ0NTYtLjQ4MjU0LS4wMjA4My0uOTU4OTctLjIxNTQ3LTEuMzI3NDItLjU4MzkxbC0xLjQxNDIxLTEuNDE0MjJjLS43ODEwNS0uNzgxMDUtLjc4MTA1LTIuMDQ3MzcgMC0yLjgyODQyczIuMDQ3MzctLjc4MTA1IDIuODI4NDIgMHoiIGZpbGw9IiM1NTQyM2UiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg=="); + background-image: var(--icon-permalink); } .tree .icon-go { - background-image: - /* go */ - url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTEwLjU4NTggNy0yLjI5Mjg5LTIuMjkyODkgMS40MTQyMS0xLjQxNDIyIDQuNzA3MDggNC43MDcxMS00LjcwNzA4IDQuNzA3MS0xLjQxNDIxLTEuNDE0MiAyLjI5Mjg5LTIuMjkyOWgtNy41ODU4di0yeiIgZmlsbD0iIzU1NDIzZSIvPjwvc3ZnPg=="); + background-image: var(--icon-go); } .tree a.navigate { @@ -241,54 +253,14 @@ opacity: 50%; } -.tree :target>details>summary, -.tree :target>div { - border-bottom: 1px dashed var(--border-2); - margin-bottom: -1px; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} +.tree :target, +.tree .target { -@media (prefers-color-scheme: dark) { - .breadcrumb::before { - background-image: - /* breadcrumb */ - url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0ibTYgMTIgNC00LTQtNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDdjZGJmIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=') - } - - .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+'); + &>details>summary, + &>div { + border-bottom: 1px dashed var(--border-2); + margin-bottom: -1px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; } } diff --git a/static/js/tree.js b/static/js/tree.js index 53ba261..9914758 100644 --- a/static/js/tree.js +++ b/static/js/tree.js @@ -17,6 +17,8 @@ function branchIsOpen(branchID) { } class Branch extends HTMLLIElement { + static branchesByNamedID = new Map(); + constructor() { super(); @@ -31,6 +33,9 @@ class Branch extends HTMLLIElement { this.details.addEventListener("toggle", _ => { 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"); LinkedBranch.byLink.set(this.linkedTree, this); - this.loadingState = "notloaded"; - this.loadingText = document.createElement("p"); { this.loadingText.className = "link-loading"; @@ -109,8 +112,10 @@ function rehash() { // https://www.youtube.com/watch?v=Tv1SYqLllKI if (!rehashing) { rehashing = true; let hash = window.location.hash; - window.location.hash = ""; - window.location.hash = hash; + if (hash.length > 0) { + window.location.hash = ""; + window.location.hash = hash; + } 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() { - let location = window.location.hash.substring(1); - await navigateToBranch(location); + await navigateToBranch(getCurrentlyHighlightedBranch()); } // When you click on a link, and the destination is within a
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 // got expanded by default. async function expandLinkedBranch() { - let hash = window.location.hash; - if (hash.length > 0) { - let linkedBranch = document.getElementById(hash.substring(1)); + let currentlyHighlightedBranch = getCurrentlyHighlightedBranch(); + if (currentlyHighlightedBranch.length > 0) { + let linkedBranch = document.getElementById(currentlyHighlightedBranch); if (linkedBranch.children.length > 0 && linkedBranch.children[0].tagName == "DETAILS") { expandDetailsRecursively(linkedBranch.children[0]); } @@ -206,3 +219,12 @@ async function expandLinkedBranch() { } addEventListener("DOMContentLoaded", expandLinkedBranch); + +async function highlightCurrentBranch() { + let branch = document.getElementById(getCurrentlyHighlightedBranch()); + if (branch != null) { + branch.classList.add("target"); + } +} + +addEventListener("DOMContentLoaded", highlightCurrentBranch); diff --git a/template/tree.hbs b/template/tree.hbs index fd6c60c..3fbfa75 100644 --- a/template/tree.hbs +++ b/template/tree.hbs @@ -5,13 +5,19 @@ - {{ config.user.title }} + {{#if (ne page.title config.user.title)}}{{ page.title }} · {{/if}}{{ config.user.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. + --}} + @@ -34,9 +40,9 @@ - {{#if breadcrumbs}} + {{#if page.breadcrumbs}} {{/if}} @@ -57,7 +63,8 @@ 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 ❤️

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. + way of remembering you closed it, and don't wanna add a database to this website. simplicity + rules!
@@ -70,7 +77,7 @@
- {{{ tree }}} + {{{ page.tree }}}