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"
- # liquidex's treehouse
<span class="oops-you-seem-to-have-gotten-stuck">

View file

@ -7,11 +7,14 @@ use crate::{
#[derive(Debug, Clone)]
pub struct Roots {
pub attributes: Option<Attributes>,
pub branches: Vec<Branch>,
}
impl Roots {
pub fn parse(parser: &mut Parser) -> Result<Self, ParseError> {
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,
})
}
}

View file

@ -151,6 +151,34 @@ impl<'a> Parser<'a> {
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> {
if self.current().is_none() {
return Ok(None);

View file

@ -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<String, parse::ErrorsEmitted> {
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(|| {

View file

@ -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<FileId> {
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,
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<Treehouse> {
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<Treehouse> {
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
}

View file

@ -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<u16>,
}
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,

View file

@ -11,7 +11,7 @@ pub fn parse_tree_with_diagnostics(
treehouse: &mut Treehouse,
file_id: FileId,
) -> 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| {
treehouse.diagnostics.push(Diagnostic {
severity: Severity::Error,
@ -34,7 +34,7 @@ pub fn parse_toml_with_diagnostics(
file_id: FileId,
range: Range<usize>,
) -> 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| {
treehouse
.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>");
}
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,
"<a class=\"icon icon-permalink\" href=\"#{}\" title=\"permalink\"></a>",
EscapeAttribute(&branch.html_id)
"<a class=\"icon icon-permalink\" href=\"/b?{}\" title=\"permalink\"></a>",
EscapeAttribute(&branch.attributes.id)
)
.unwrap();
}

View file

@ -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)?,

View file

@ -10,7 +10,28 @@ use ulid::Ulid;
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;
/// Treehouse compilation context.
@ -22,19 +43,9 @@ pub struct Treehouse {
pub branches_by_named_id: HashMap<String, SemaBranchId>,
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,
}
#[derive(Debug, Clone)]
pub struct BranchRef {
pub html_id: String,
pub file_id: FileId,
pub kind_span: Range<usize>,
}
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<String>,
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 {

View file

@ -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 {

View file

@ -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<SemaBranch>,
@ -35,12 +37,14 @@ impl SemaTree {
#[derive(Debug, Clone)]
pub struct SemaRoots {
pub attributes: RootAttributes,
pub branches: Vec<SemaBranchId>,
}
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,7 +174,8 @@ 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| {
toml_edit::de::from_str(&source.input()[attributes.data.clone()]).unwrap_or_else(
|error| {
treehouse
.diagnostics
.push(toml_error_to_diagnostic(TomlError {
@ -143,7 +186,8 @@ impl SemaBranch {
}));
successfully_parsed = false;
Attributes::default()
})
},
)
} else {
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 {
@ -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 {
.tree :target,
.tree .target {
&>details>summary,
&>div {
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) {
.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+');
}
}

View file

@ -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;
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 <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
// 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);

View file

@ -5,13 +5,19 @@
<head>
<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 property="og:title" 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/tree.css">
@ -34,9 +40,9 @@
</svg>
</a>
{{#if breadcrumbs}}
{{#if page.breadcrumbs}}
<ol class="breadcrumbs">
{{{ breadcrumbs }}}
{{{ page.breadcrumbs }}}
</ol>
{{/if}}
</nav>
@ -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 ❤️</p>
<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>
</noscript>
@ -70,7 +77,7 @@
</div>
<main class="tree">
{{{ tree }}}
{{{ page.tree }}}
</main>
</body>