navigation maps for navigating across pages
This commit is contained in:
parent
28a39fc883
commit
09ff8a742e
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1231,6 +1231,7 @@ dependencies = [
|
|||
"pulldown-cmark",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"toml_edit",
|
||||
"tower-http",
|
||||
|
|
47
README.md
Normal file
47
README.md
Normal 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.
|
|
@ -1 +1,2 @@
|
|||
% id = "01H8V55MGHGYXWY8F287FMNXNY"
|
||||
- section under construction. sorry! in the meantime, maybe you wanna read [my ramblings about the treehouse][branch:01H89RFHCQCD3E1XS5XAPW86J5]?
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
2
content/secret.tree
Normal file
|
@ -0,0 +1,2 @@
|
|||
% id = "man"
|
||||
- He is behind the tree.
|
|
@ -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"
|
||||
|
||||
|
||||
|
|
|
@ -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,17 +137,35 @@ 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);
|
||||
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();
|
||||
branches_to_html(&mut tree, &mut treehouse, file_id, &roots.branches);
|
||||
// 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) {
|
||||
|
@ -155,12 +183,12 @@ impl Generator {
|
|||
};
|
||||
|
||||
std::fs::create_dir_all(
|
||||
target_path
|
||||
parsed_tree
|
||||
.target_path
|
||||
.parent()
|
||||
.expect("there should be a parent directory to generate files into"),
|
||||
)?;
|
||||
std::fs::write(target_path, templated_html)?;
|
||||
}
|
||||
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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::fmt::{self, Display, Write};
|
||||
|
||||
mod markdown;
|
||||
pub mod navmap;
|
||||
pub mod tree;
|
||||
|
||||
pub struct EscapeAttribute<'a>(&'a str);
|
||||
|
|
82
crates/treehouse/src/html/navmap.rs
Normal file
82
crates/treehouse/src/html/navmap.rs
Normal 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()
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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![],
|
||||
|
||||
|
|
|
@ -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,28 +56,24 @@ 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";
|
||||
|
||||
fetch(`/${this.linkedTree}.html`)
|
||||
.then(response => {
|
||||
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.`;
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(text => {
|
||||
|
||||
let text = await response.text();
|
||||
let parser = new DOMParser();
|
||||
let linkedDocument = parser.parseFromString(text, "text/html");
|
||||
let main = linkedDocument.getElementsByTagName("main")[0];
|
||||
|
@ -80,17 +81,20 @@ class LinkedBranch extends Branch {
|
|||
|
||||
this.loadingText.remove();
|
||||
this.innerUL.innerHTML = ul.innerHTML;
|
||||
|
||||
this.loadingState = "loaded";
|
||||
})
|
||||
.catch(error => {
|
||||
} catch (error) {
|
||||
this.loadingText.innerText = error.toString();
|
||||
this.loadingState = "error";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue