navigation maps for navigating across pages

This commit is contained in:
liquidex 2023-08-27 14:50:46 +02:00
parent 28a39fc883
commit 09ff8a742e
15 changed files with 382 additions and 71 deletions

1
Cargo.lock generated
View file

@ -1231,6 +1231,7 @@ dependencies = [
"pulldown-cmark",
"rand",
"serde",
"serde_json",
"tokio",
"toml_edit",
"tower-http",

47
README.md Normal file
View file

@ -0,0 +1,47 @@
# liquidex's treehouse
Welcome to the Construct.
If you haven't seen the treehouse yet, you [may wanna](https://liquidex.house). It's pretty darn cool.
Please note that this repository contains spoilers. So if you like exploring by yourself, you may wanna do that first before diving into the source code.
*Spoilers for what?*, you might ask.
You have been warned.
## Building
To build the website:
```sh
cargo run -p treehouse regenerate
```
This will spit out a directory `target/site` containing the static pages. You're free to use any HTTP server you wish, but for development purposes treehouse includes one in the CLI:
```sh
cargo run -p treehouse regenerate --serve
```
This will fire up a server on port 8080. No way to change that, sorry. Edit the source code.
If you're developing, you may wanna use [`cargo-watch`](https://crates.io/crates/cargo-watch):
```sh
cargo watch -- cargo run -p treehouse regenerate --serve
```
The website will reload itself automatically if you change any file in the repository.
## Contributing
If you found a typo, be my guest. Just note that some typos are intentional, please make sure you understand the full context of the sentence.
If you found a bug, by all means please submit a fix. (or at least an issue.)
Since this is my personal website, I don't accept outside contributions for new content. Because then it would no longer be *my* treehouse.
If you wish to create something similar to liquidex's treehouse, you probably want to use more mature software instead of my scrappy, opinionated piece of art. Check out [Logseq](https://logseq.com/) - it has static site generation built in, with a much more approachable UI, cross-device sync, and great customizability through community-made themes and plugins.

View file

@ -1 +1,2 @@
% id = "01H8V55MGHGYXWY8F287FMNXNY"
- section under construction. sorry! in the meantime, maybe you wanna read [my ramblings about the treehouse][branch:01H89RFHCQCD3E1XS5XAPW86J5]?

View file

@ -111,7 +111,7 @@
- but you may not find the experience favorable
% id = "01H89RFHCQ7HTZSP6P2RZR8JHE"
+ but most importantly of all, it is *weird*.
+ but most important of all, it is *weird*.
% id = "01H89RFHCQTRVPZ0AJ0DGJHXKX"
- weird as me
@ -131,11 +131,13 @@
% id = "01H89RFHCQ2GWJPTAKTRGS1QAC"
- weird poems and philosophical talk are over, it's time to focus on the tech.
% id = "01H8V55APDEF8WTQ3KFC7E9HWB"
- call this an overview, Defense of Design, or what have you
% id = "01H89RFHCQF4N9T05B9DVWX67K"
- treehouse is built in the programming language that gives me the most pleasure coding.
% id = "01H8V55APDXH5N1YC2WXDBCDN4"
- no need for you to know more. :shhh:
% id = "01H89RFHCQRA4BBBWDC8K68BB0"
@ -145,60 +147,80 @@
% id = "01H89RFHCQA32MCR4MDCANDNC7"
- but being the altruist I am - don't worry, it _will_ be open source one day.
% id = "01H8V55APDHC0DZNF67DS76P8H"
- in case you're reading this in the far future, and this is still here…
you wouldn't mind [dropping me a line][branch:01H89P3CH8CD28KGX9GVRFK60E] would you?
% id = "01H89RFHCQAQVXP6B2H0T8NNDS"
- personally… the language you build a personal project with almost never matters. it's rather how you execute your ideas.
% id = "01H8V55APDRZHHG69A6BWQM8YE"
+ therefore I find boasting that my project is powered by a `$LANGUAGE` or a `$FRAMEWORK` unnecessary.
% id = "01H8V55APDNJ4W3MY1A204PHJ3"
+ but if you really care that much, it's built with Rust, plain HTML5, plain CSS, and
[Vanilla JS](http://vanilla-js.com/).
% id = "01H8V55APDYHZG2QXQ78HZE97V"
- (yes, I know that website is super old, but I still find it incredibly funny :hueh:)
% id = "01H89RFHCQFWC2FWBAE9PVNC08"
- as I alluded to [here][branch:01H89RFHCQ3EAP0F6PRSEK7S1T], treehouse is built to decay gracefully
as you take away the fancy parts.
% id = "01H8V55APDMPF3WFTNTFSYBQRF"
- you will be able to read it just fine without JavaScript, just that it'll be a little
less pleasant.
% id = "01H8V55APD1RSSRMDZ3CEE9S9C"
- that's because the JS handles stuff like lazy loading of linked trees.
% id = "01H8V55APDAPSR7R9M569GW4S7"
- you may have seen a brief _"Loading…"_ text flash before your eyes as you opened this
_about this_ branch - that's it downloading another tree and pasting it into this document
% id = "01H8V55APDS9BNC2TQ5S5VYC0G"
- when JS is unavailable we fall back to a plain old `<a>` link through a
`<noscript>` tag. if you have uBlock Origin, or any other JS blocker, you can try
that out yourself!
% id = "01H8V55APD503DMXJD9JA2WJGS"
- the JS also lets you select text in these `<details>` elements without them collapsing
as you release the mouse button.
% id = "01H8V55APD34KE6ED6MRTW34H6"
- I have no clue why it is this way by default, but frankly it's like a 5 line
usability fix, so why not.
% id = "01H8V55APDR3VCEANQ6XVJP3X8"
- if you have accessibility concerns about this decision, please let me know.
% id = "01H8V55APDFW5SPAKSQ04K5HRE"
- it also saves your progress as you read. if you refresh the page, you'll notice you end up exactly where you left off!
% id = "01H8V55APDNSXZMZJ2K1KW11KN"
- but, there is one very crucial piece of JavaScript that makes this website tick, and your experience **will be degraded** if you disable it. that feature is linking to branches.
% id = "01H8V55APDBG6AH2W3RNMX724X"
- by default, if you link to an element by its id and it's contained within a `<details>`, the `<details>` will not expand. :ralsei_dead:
% id = "01H8V55APDE0KSKKAS8RBRPYRM"
- therefore there's a bit of JS to make that work, _and_ to tie that together with lazy loading.
% id = "01H8V55APDTZSSYTY17SYZ6DGQ"
- treehouse will not work *as* fine without CSS though - the `<details>` will look extremely
janky, but the content should still be fully readable.
% id = "01H8V55APDWN8TV31K4SXBTTWB"
- myself, I'm writing this content in a bespoke format called `.tree`.
% id = "01H8V55APDGX4EYV8W7ECXZ6FP"
- the structure of `.tree` files is extremely minimal. there are only a few syntactic features to speak of.
% id = "01H8V55APDR95HSV0TX7TK63AF"
- here's a taste of `.tree`:
% id = "01H8V55APD5686J8GTXP118V8E"
- ```
\% id = "root"
\- this is a branch
@ -214,95 +236,141 @@
\- and this branch links to another tree
```
% id = "01H8V55APDB49SPHPPMV2BCMW3"
- the `.tree` format is line-based. that means the `%`, `-`, and `+` tokens are only
interpreted when at the beginning of a line.
% id = "01H8V55APD67W8FHEZK6E2A21K"
- the basic unit of `.tree` is a branch. branches…
% id = "01H8V55APDQ1KS7S313GK590GM"
- …are located at a specific _indent level_
% id = "01H8V55APDHNRFCGH9STD46J0F"
- …may or may not have _attributes_ - that's what the percentage sign `%` begins
% id = "01H8V55APDF9C7Y0QDYF3EBMMS"
- …may or may not be expanded by default (this is the branch's _kind_) - that's what the minus `-` and plus `+` tokens do
% id = "01H8V55APDJ6EYE4PR370JAT4W"
- each branch is constructed in this order: optional attributes, kind, content
% id = "01H8V55APDGT47G5DKZKTQ1S80"
- and ends when another line beginning with `%`, `-`, or `+` is found.
% id = "01H8V55APDQ3VYBBTTMNFTVNV4"
- other than that, `.tree` assumes nothing about what format the branch attributes or content are encoded in.
% id = "01H8V55APDCXPW8JPRYZHY9NXV"
- I chose TOML and Markdown for their ease of use and flexibility, but the parser couldn't care less.
% id = "01H8V55APDWDPSHNZX6QJFM8ZA"
- …actually, that's a lie. see that code example above? Markdown code fences \`\`\` are handled specially to let embed `.tree` source code blocks within `.tree` files. that's all.
% id = "01H8V55APDVQB6AD23Y6PZPPB8"
- you may have noticed in that code example above that almost every branch has an `id` attribute.
% id = "01H8V55APD4H5JT8NTYCCTN77G"
- this is because I want every branch to be individually linkable and quotable.
% id = "01H8V55APDPSZNP9AK7QRK62BG"
- if you ever want to link anything I said in an argument online: hover over a branch, right-click that little _permalink_ icon that's on the right side of the screen, and _Copy Link_.
% id = "01H8V55APDCZ0HY20GGXYE8D1G"
- but manually coming up with ids for branches would be quite the nuisance.
% id = "01H8V55APDXV0KRVEF9PTA72J7"
- that's why with the power of Tools Programming™, I built a tool that fills out all the ids for me.
% id = "01H8V55APD8VG7WE6JX5JBDJEC"
- I chose [ulids](https://github.com/ulid/spec) for generated ids, for a few reasons:
% id = "01H8V55APD2J58ENT1T0JRJ7H6"
- they're guaranteed to be unique, so I can be sure no two branches end up having the same id
% id = "01H8V55APDGV6PKJKZ16CJ1YXY"
- which is precisely what lets me pull the trick where I embed one tree within another, and you just dig into `index.html`.
% id = "01H8V55APD1CYZD8GE3AK87QK2"
- they embed a timestamp, which I can use to display the date/time when a branch was written, because I generally `fix` branches right after writing
% id = "01H8V55APDFXW5A5J85HJQP17C"
- as well as highlight branches which were added since your last visit
% id = "01H8V55APDPZK4C8H5TTHTHH7J"
- not that either of these features are implemented as of writing this (2023-08-20)
% id = "01H8V55APDXB9AXTAGAE9TNQRB"
- they're much more compact than [uuids](https://en.wikipedia.org/wiki/Uuid)
% id = "01H8V55APDZE5AE92GDM2TH2EF"
- which use 32 hexadecimal digits separated unevenly by 4 dashes, giving you 36 characters.
% id = "01H8V55APDS478E3ED5CRVT6RE"
- who even remembers how many digits there are between each dash? I know I don't.
% id = "01H8V55APDXP1D0MHGN2DXBT16"
- ulids on the other hand are 26 digits encoded with [Crockford's base32](https://www.crockford.com/base32.html)
% id = "01H8V55APD79QTC2NFC6X7S3W4"
- which is cool because it's much denser while avoiding ambiguous characters - `0`, `O`, and `o` are all interpreted as `0` (zero).
% id = "01H8V55APDXT064G9PA78JW0CM"
- noticed how fast the treehouse restores your state? there's basically no delay.
% id = "01H8V55APDGS626M5Y6DDAS9ZE"
- this is because it restores your state _as it's loading in_, by using [Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements).
% id = "01H8V55APD73AG65NP2VGX6169"
- despite many people calling that API extremely low-level, I beg to differ. it's actually pretty easy and pleasant to use.
% id = "01H8V55APDDHYNRZXQDEG0A242"
- most importantly it lets me enhance vanilla `<li>` elements with custom behavior executed on load, which I use to restore your reading progress as the page is loading in.
% id = "01H8V55APD4V54M2EKQKQ4GNP8"
- linked branches also use Web Components by the way.
% id = "01H8V55APDKY0HYEJM2C1CJHEG"
- while not strictly a technical topic, I'd like to shout out [Recursive](https://recursive.design) for being an awesome font :ralsei_love:
% id = "01H8V55APD88JCQBQ4SHVCYR97"
- (not to be confused with Font Awesome, which I do not use here. icon designs are my own.)
% id = "01H8V55APD0S00ACN0YY7Y9MSW"
- being a variable font, I can tweak the text's look and feel on many different axes.
% id = "01H8V55APDRG88JRV74MMDTV4J"
- the one that I wanted to shout out in particular is `CASL`, which lets me make it look a lot more happy and playful.
% id = "01H8V55APDMP5FE9ZCMRACH18W"
+ almost like Comic Sans, but without all the stigma.
% id = "01H8V55APDPGMD33J4M3SJ2QKP"
- I actually considered using [Comic Neue](https://comicneue.com/) as the font on this website, but ultimately chose Recursive.
% id = "01H8V55APDXBS7HMDSCEJ1JDPY"
- Comic Neue and not the Real™ Comic Sans MS because it's licensed under the SIL Open Font License, which means I could redistribute it on my website.
% id = "01H8V55APDFT0Y4ECEM92XFTTR"
- not everyone runs Windows or macOS, so just assuming the reader has Comic Sans installed isn't ideal.
% id = "01H8V55APD2BPGQXEK7JARYJ9H"
- in particular the variant on my website is 100% casual (`CASL`), and -2.0 slanted (`slnt`).
% id = "01H8V55APDXZJE3HHH5AQ8ZQHF"
+ that little bit of slant makes it look just a little more like handwriting.
% id = "01H8V55APDGSB2Y742S07DHTAZ"
- I actually thought of handwriting all the text in the treehouse, but ultimately thought it would be too much work. and a technical challenge too.
% id = "01H8V55APDDXH02F91RM19RDHX"
- how to make it scalable (as in, how to keep it smooth when you zoom in?)
% id = "01H8V55APDQ1N44QASCHVDB4D5"
- how to make it scalable (as in, my hand really fucking hurts from writing this much, and I *really* do not wanna go back to the times of writing school essays please?)
% id = "01H8V55APDP5T3EYPVMVN9QVK7"
- how to make it scalable (as in, how to distribute this many images to the user without tanking their internet connection?)
% id = "01H8V55APDB8PC2BF5P9TVYJ35"
- not to mention I'd have to handwrite a lot of text, and *then* either use OCR or type it all out again for accessibility. yeah, no thanks methinks.

View file

@ -82,7 +82,7 @@
- apart from programming I also take interest in other various things
% id = "01H89P3CH864VWM4JXQ0N5C1YV"
- like [video games][page:/games]
- like [video games][branch:games]
% id = "01H89P3CH8WK8F2XKBXFVE8KNX"
- yeah those are pretty great aren't they
@ -97,10 +97,10 @@
- [DELTARUNE](https://deltarune.com)
% id = "01H89P3CH89TQ3SFG2Z40J29HX"
- they're pretty great you should check them out (also check out [my games corner][page:games]? :pleading_face:)
- they're pretty great you should check them out (also check out [my games corner][branch:games]? :pleading_face:)
% id = "01H89P3CH8AJATQ5DJBBFXJ1NH"
- or [music][page:music]
- or [music][branch:music]
% id = "01H89P3CH8XQ59YZD3RFRYQ2BM"
- various genres from electronic through jazz and even rock
@ -117,8 +117,10 @@
% id = "01H89P3CH8C719GX9CTK36FXXN"
+ if you don't know what that means then good for you
% id = "01H8V55G7M7KEKBYX2D2RP9QQE"
+ but if you so insist… [you have been warned](https://www.youtube.com/watch?v=BB7ExLcUuH0)
% id = "01H8V55G7MWC7MQ9YQVDDFZ9QV"
- (this is one of the more crazy ones anyways. he makes some [more normal stuff](https://www.youtube.com/watch?v=IuI6MMUtNdU) too if you care)
% id = "01H89P3CH8XP6ZA1RWYVZWRXN3"
@ -147,6 +149,7 @@
% id = "01H89P3CH80XYPAHXH1QZJ9SMZ"
+ so I type here like I would type on some chat platform. but it's a tree, just like Reddit!
% id = "01H8V55G7MS3NBPFFFKYE8YGFK"
- except on chat I don't capitalize "I" so if that pisses you off you may wanna brace yourself (or just tell me, I'll do my best to adapt)
% id = "01H89P3CH8CD28KGX9GVRFK60E"

View file

@ -1,6 +1,7 @@
% id = "treehouse"
- # liquidex's treehouse
% id = "01H8V556P1PND8DQ73XBTZZJH7"
- welcome! make yourself at home
---
@ -15,8 +16,10 @@
---
% id = "01H8V556P1GRAA3717VH3QJFMV"
- below you can find my Special Interest Corners™
% id = "01H8V556P1KDBC9EYYSA1F44ZJ"
- ie. thoughts on stuff that I wanna jot down somewhere
% id = "programming"
@ -30,3 +33,7 @@
% id = "games"
content.link = "2023-08-20-under-construction"
+ ## games
% id = "3d-printing"
content.link = "2023-08-20-under-construction"
+ ## 3D printing

2
content/secret.tree Normal file
View file

@ -0,0 +1,2 @@
% id = "man"
- He is behind the tree.

View file

@ -24,5 +24,6 @@ tower-livereload = "0.8.0"
walkdir = "2.3.3"
ulid = "1.0.0"
rand = "0.8.5"
serde_json = "1.0.105"

View file

@ -1,6 +1,7 @@
use std::{
ffi::OsStr,
path::{Path, PathBuf},
time::Instant,
};
use anyhow::{bail, Context};
@ -18,7 +19,9 @@ use tower_livereload::LiveReloadLayer;
use walkdir::WalkDir;
use crate::{
cli::parse::parse_tree_with_diagnostics, html::tree::branches_to_html, tree::SemaRoots,
cli::parse::parse_tree_with_diagnostics,
html::{navmap::build_navigation_map, tree::branches_to_html},
tree::SemaRoots,
};
use crate::state::{FileId, Treehouse};
@ -103,6 +106,13 @@ impl Generator {
&dirs.template_dir.join("tree.hbs"),
)?;
struct ParsedTree {
tree_path: String,
file_id: FileId,
target_path: PathBuf,
}
let mut parsed_trees = vec![];
for path in &self.tree_files {
let utf8_filename = path.to_string_lossy();
@ -127,42 +137,60 @@ impl Generator {
continue;
}
};
let file_id = treehouse.add_file(
utf8_filename.into_owned(),
Some(tree_path.with_extension("").to_string_lossy().into_owned()),
source,
);
let tree_path = tree_path.with_extension("").to_string_lossy().into_owned();
let file_id =
treehouse.add_file(utf8_filename.into_owned(), Some(tree_path.clone()), source);
if let Ok(roots) = parse_tree_with_diagnostics(&mut treehouse, file_id) {
let roots = SemaRoots::from_roots(&mut treehouse, file_id, roots);
let mut tree = String::new();
branches_to_html(&mut tree, &mut treehouse, file_id, &roots.branches);
let template_data = TemplateData { tree };
let templated_html = match handlebars.render("tree", &template_data) {
Ok(html) => html,
Err(error) => {
Self::wrangle_handlebars_error_into_diagnostic(
&mut treehouse,
tree_template,
error.line_no,
error.column_no,
error.desc,
)?;
continue;
}
};
std::fs::create_dir_all(
target_path
.parent()
.expect("there should be a parent directory to generate files into"),
)?;
std::fs::write(target_path, templated_html)?;
treehouse.roots.insert(tree_path.clone(), roots);
parsed_trees.push(ParsedTree {
tree_path,
file_id,
target_path,
});
}
}
for parsed_tree in parsed_trees {
let mut tree = String::new();
// Temporarily steal the tree out of the treehouse.
let roots = treehouse
.roots
.remove(&parsed_tree.tree_path)
.expect("tree should have been added to the treehouse");
branches_to_html(
&mut tree,
&mut treehouse,
parsed_tree.file_id,
&roots.branches,
);
treehouse.roots.insert(parsed_tree.tree_path, roots);
let template_data = TemplateData { tree };
let templated_html = match handlebars.render("tree", &template_data) {
Ok(html) => html,
Err(error) => {
Self::wrangle_handlebars_error_into_diagnostic(
&mut treehouse,
tree_template,
error.line_no,
error.column_no,
error.desc,
)?;
continue;
}
};
std::fs::create_dir_all(
parsed_tree
.target_path
.parent()
.expect("there should be a parent directory to generate files into"),
)?;
std::fs::write(parsed_tree.target_path, templated_html)?;
}
Ok(treehouse)
}
}
@ -181,6 +209,8 @@ pub struct TemplateData {
}
pub fn regenerate(dirs: &Dirs<'_>) -> anyhow::Result<()> {
let start = Instant::now();
info!("cleaning target directory");
let _ = std::fs::remove_dir_all(dirs.target_dir);
std::fs::create_dir_all(dirs.target_dir)?;
@ -193,8 +223,18 @@ pub fn regenerate(dirs: &Dirs<'_>) -> anyhow::Result<()> {
generator.add_directory_rec(dirs.content_dir)?;
let treehouse = generator.generate_all_files(dirs)?;
info!("generating navigation map");
let navigation_map = build_navigation_map(&treehouse, "index");
std::fs::write(
dirs.target_dir.join("navmap.js"),
navigation_map.to_javascript(),
)?;
treehouse.report_diagnostics()?;
let duration = start.elapsed();
info!("generation done in {duration:?}");
Ok(())
}

View file

@ -1,6 +1,7 @@
use std::fmt::{self, Display, Write};
mod markdown;
pub mod navmap;
pub mod tree;
pub struct EscapeAttribute<'a>(&'a str);

View file

@ -0,0 +1,82 @@
use std::collections::HashMap;
use serde::Serialize;
use crate::{
state::Treehouse,
tree::{attributes::Content, SemaBranchId},
};
#[derive(Debug, Clone, Default, Serialize)]
pub struct NavigationMap {
/// Tells you which pages need to be opened to get to the key.
pub paths: HashMap<String, Vec<String>>,
}
impl NavigationMap {
pub fn to_javascript(&self) -> String {
format!(
"export const navigationMap = {};",
serde_json::to_string(&self.paths)
.expect("serialization of the navigation map should not fail")
)
}
}
#[derive(Debug, Clone, Default)]
pub struct NavigationMapBuilder {
stack: Vec<String>,
navigation_map: NavigationMap,
}
impl NavigationMapBuilder {
pub fn enter_tree(&mut self, tree: String) {
self.stack.push(tree.clone());
self.navigation_map.paths.insert(tree, self.stack.clone());
}
pub fn exit_tree(&mut self) {
self.stack.pop();
}
pub fn finish(self) -> NavigationMap {
self.navigation_map
}
}
pub fn build_navigation_map(treehouse: &Treehouse, root_tree_path: &str) -> NavigationMap {
let mut builder = NavigationMapBuilder::default();
fn rec_branch(
treehouse: &Treehouse,
builder: &mut NavigationMapBuilder,
branch_id: SemaBranchId,
) {
let branch = treehouse.tree.branch(branch_id);
if let Content::Link(linked) = &branch.attributes.content {
rec_tree(treehouse, builder, linked);
} else {
for &child_id in &branch.children {
rec_branch(treehouse, builder, child_id);
}
}
}
fn rec_tree(treehouse: &Treehouse, builder: &mut NavigationMapBuilder, tree_path: &str) {
if let Some(roots) = treehouse.roots.get(tree_path) {
// Pages can link to each other causing infinite recursion, so we need to handle that
// case by skipping pages that already have been analyzed.
if !builder.navigation_map.paths.contains_key(tree_path) {
builder.enter_tree(tree_path.to_owned());
for &branch_id in &roots.branches {
rec_branch(treehouse, builder, branch_id);
}
builder.exit_tree();
}
}
}
rec_tree(treehouse, &mut builder, root_tree_path);
builder.finish()
}

View file

@ -17,7 +17,6 @@ pub fn branch_to_html(
file_id: FileId,
branch_id: SemaBranchId,
) {
// Reborrow because the closure requires unique access (it adds a new diagnostic.)
let source = treehouse.source(file_id);
let branch = treehouse.tree.branch(branch_id);

View file

@ -8,7 +8,7 @@ use codespan_reporting::{
};
use ulid::Ulid;
use crate::tree::{SemaBranchId, SemaTree};
use crate::tree::{SemaBranchId, SemaRoots, SemaTree};
pub type Files = SimpleFiles<String, String>;
pub type FileId = <Files as codespan_reporting::files::Files<'static>>::FileId;
@ -20,6 +20,7 @@ pub struct Treehouse {
pub tree: SemaTree,
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>>,
@ -42,6 +43,7 @@ impl Treehouse {
tree: SemaTree::default(),
branches_by_named_id: HashMap::new(),
roots: HashMap::new(),
tree_paths: vec![],

View file

@ -1,3 +1,5 @@
import { navigationMap } from "/navmap.js";
const branchStateKey = "treehouse.openBranches";
let branchState = JSON.parse(localStorage.getItem(branchStateKey)) || {};
@ -31,10 +33,13 @@ class Branch extends HTMLLIElement {
customElements.define("th-b", Branch, { extends: "li" });
class LinkedBranch extends Branch {
static byLink = new Map();
constructor() {
super();
this.linkedTree = this.getAttribute("data-th-link");
LinkedBranch.byLink.set(this.linkedTree, this);
this.loadingState = "notloaded";
@ -51,46 +56,45 @@ class LinkedBranch extends Branch {
// correctly, as Branch saves the state in localStorage. Having an expanded-by-default
// linked block can be useful in development.
if (this.details.open) {
this.loadTree();
this.loadTree("constructor");
}
this.details.addEventListener("toggle", _ => {
if (this.details.open) {
this.loadTree();
this.loadTree("toggle");
}
});
}
loadTree() {
if (this.loadingState == "notloaded") {
this.loadingState = "loading";
async loadTreePromise(_initiator) {
try {
let response = await fetch(`/${this.linkedTree}.html`);
if (response.status == 404) {
throw `Hmm, seems like the tree "${this.linkedTree}" does not exist.`;
}
fetch(`/${this.linkedTree}.html`)
.then(response => {
if (response.status == 404) {
throw `Hmm, seems like the tree "${this.linkedTree}" does not exist.`;
}
return response.text();
})
.then(text => {
let parser = new DOMParser();
let linkedDocument = parser.parseFromString(text, "text/html");
let main = linkedDocument.getElementsByTagName("main")[0];
let ul = main.getElementsByTagName("ul")[0];
let text = await response.text();
let parser = new DOMParser();
let linkedDocument = parser.parseFromString(text, "text/html");
let main = linkedDocument.getElementsByTagName("main")[0];
let ul = main.getElementsByTagName("ul")[0];
this.loadingText.remove();
this.innerUL.innerHTML = ul.innerHTML;
this.loadingState = "loaded";
})
.catch(error => {
this.loadingText.innerText = error.toString();
this.loadingState = "error";
});
this.loadingText.remove();
this.innerUL.innerHTML = ul.innerHTML;
} catch (error) {
this.loadingText.innerText = error.toString();
}
}
loadTree() {
if (!this.loading) {
this.loading = this.loadTreePromise();
}
return this.loading;
}
}
customElements.define("th-b-linked", LinkedBranch, { extends: "li" });
function expandDetailsRecursively(element) {
@ -102,13 +106,61 @@ function expandDetailsRecursively(element) {
}
}
// When you click on a link, and the destination is within a <details> that is not expanded,
// expand the <details> recursively.
window.addEventListener("popstate", _ => {
let element = document.getElementById(window.location.hash.substring(1));
if (element !== undefined) {
function navigateToPage(page) {
window.location.pathname = `/${page}.html`
}
async function navigateToBranch(fragment) {
if (fragment.length == 0) {
return;
}
let element = document.getElementById(fragment);
if (element !== null) {
// If the element is already loaded on the page, we're good.
expandDetailsRecursively(element);
window.location.hash = window.location.hash;
} else {
// The element is not loaded, we need to load the tree that has it.
let parts = fragment.split(':');
if (parts.length >= 2) {
let [page, _id] = parts;
let fullPath = navigationMap[page];
if (Array.isArray(fullPath)) {
// TODO: This logic will probably need to be upgraded at some point to support
// navigation maps with roots other than index. Currently though only index is
// generated so that doesn't matter.
let [_root, ...path] = fullPath;
if (path !== undefined) {
let isNotAtIndexHtml = window.location.pathname != "/index.html";
for (let linked of path) {
let branch = LinkedBranch.byLink.get(linked);
if (isNotAtIndexHtml && branch === undefined) {
navigateToPage("index");
return;
}
await branch.loadTree("navigateToBranch");
branch.details.open = true;
}
window.location.hash = window.location.hash;
}
} else {
// In case the navigation map does not contain the given page, we can try
// redirecting the user to a concrete page on the site.
navigateToPage(page);
}
}
}
})
}
async function navigateToCurrentBranch() {
let location = window.location.hash.substring(1);
navigateToBranch(location);
}
// When you click on a link, and the destination is within a <details> that is not expanded,
// expand the <details> recursively.
window.addEventListener("popstate", navigateToCurrentBranch);
addEventListener("DOMContentLoaded", navigateToCurrentBranch);

View file

@ -1,18 +1,19 @@
<!DOCTYPE html>
<html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>{{ config.user.title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ site }}/static/css/main.css">
<link rel="stylesheet" href="{{ site }}/static/css/tree.css">
<link rel="stylesheet" href="{{ site }}/static/font/font.css">
<script type="module" src="{{ site }}/navmap.js"></script>
<script type="module" src="{{ site }}/static/js/tree.js"></script>
<script type="module" src="{{ site }}/static/js/usability.js"></script>
</head>
@ -38,6 +39,10 @@
</div>
</noscript>
<nav>
<a href="{{ site }}/" title="Back to index"></a>
</nav>
<main class="tree">
{{{ tree }}}
</main>