tree update!
This commit is contained in:
parent
26b6056dbc
commit
5f86f4cee7
10
content/_treehouse/404.tree
Normal file
10
content/_treehouse/404.tree
Normal 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
21
content/_treehouse/b.tree
Normal 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>
|
|
@ -1,3 +1,5 @@
|
|||
%% title = "liquidex's treehouse"
|
||||
|
||||
% id = "treehouse"
|
||||
- # liquidex's treehouse
|
||||
<span class="oops-you-seem-to-have-gotten-stuck">
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(|| {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
86
crates/treehouse/src/cli/serve.rs
Normal file
86
crates/treehouse/src/cli/serve.rs
Normal 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())
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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)?,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
|
|
|
@ -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+');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Reference in a new issue