diff --git a/Cargo.lock b/Cargo.lock index 5f8e6a8..e89722b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -724,7 +724,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1409,7 +1409,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879952a81a83930934cbf1786752d6dedc3b1f29e8f8fb2ad1d0a36f377cf442" dependencies = [ "memchr", - "thiserror", + "thiserror 1.0.69", "ucd-trie", ] @@ -1599,7 +1599,7 @@ dependencies = [ "rand_chacha", "simd_helpers", "system-deps", - "thiserror", + "thiserror 1.0.69", "v_frame", "wasm-bindgen", ] @@ -1931,7 +1931,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -1945,6 +1954,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -2176,24 +2196,17 @@ dependencies = [ "regex", "serde", "serde_json", + "thiserror 2.0.12", "tokio", "toml_edit 0.19.15", "tracing", "tracing-chrome", "tracing-subscriber", - "treehouse-format", "ulid", "webp", "xmlparser", ] -[[package]] -name = "treehouse-format" -version = "0.1.0" -dependencies = [ - "thiserror", -] - [[package]] name = "typenum" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index 9c407e9..97f0b1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,37 @@ -[workspace] -members = ["crates/*"] -resolver = "2" +[package] +name = "treehouse" +version = "0.1.0" +edition = "2021" -[workspace.dependencies] +[dependencies] +anyhow = "1.0.75" +axum = { version = "0.7.9", features = ["macros"] } +axum-macros = "0.4.2" +base64 = "0.21.7" +blake3 = "1.5.3" +chrono = { version = "0.4.35", features = ["serde"] } +clap = { version = "4.3.22", features = ["derive"] } +codespan-reporting = "0.11.1" +dashmap = "6.1.0" +git2 = { version = "0.19.0", default-features = false, features = ["vendored-libgit2"] } +handlebars = "4.3.7" +image = "0.25.5" +indexmap = { version = "2.2.6", features = ["serde"] } +jotdown = { version = "0.4.1", default-features = false } +rand = "0.8.5" +rayon = "1.10.0" +regex = "1.10.3" +serde = { version = "1.0.183", features = ["derive"] } +serde_json = "1.0.105" +thiserror = "2.0.12" +tokio = { version = "1.32.0", features = ["full"] } +toml_edit = { version = "0.19.14", features = ["serde"] } tracing = "0.1.40" - -treehouse-format = { path = "crates/treehouse-format" } +tracing-chrome = "0.7.2" +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +ulid = "1.0.0" +webp = "0.3.0" +xmlparser = "0.13.6" [profile.dev] package.webp.opt-level = 3 diff --git a/Justfile b/Justfile index 7cc2557..e9915ae 100644 --- a/Justfile +++ b/Justfile @@ -1,7 +1,7 @@ port := "8080" serve: - cargo watch -- cargo run -- serve --port {{port}} + RUST_BACKTRACE=1 cargo watch -- cargo run -- serve --port {{port}} fix: cargo run -- fix-all --apply diff --git a/content/about.tree b/content/about/v2.tree similarity index 100% rename from content/about.tree rename to content/about/v2.tree diff --git a/content/index.dj b/content/index.dj new file mode 100644 index 0000000..2fdb873 --- /dev/null +++ b/content/index.dj @@ -0,0 +1,37 @@ +title = "riki's house" +include_feed = { name = "new", title = "Blog" } + ++++ + +My name's *riki moe*, or *リキ萌*! + +I'm a _he/him_-type cat [furry][page:philosophy/furry] doing various things with computers. + +I work on game optimization tools at [[CD PROJEKT RED](https://cdprojektred.com) :rarog:]{.nowrap} but that's just the tip of the iceberg! +After hours, I program [websites](/), [compilers][def:stitchkit/repo], [audio][def:dawd3/repo], and other fun things. + +Such as [*video games.*][page:games] +Like, I probably wouldn't be in the industry if I didn't like them.\ +My personal favourites are [:nap: [DELTARUNE](https://deltarune.com)]{.nowrap}, [:bean: [Animal Well](https://www.animalwell.net/)]{.nowrap}, [:fox: [TUNIC](https://tunicgame.com/)]{.nowrap}, [:hueh: [A Hat in Time](https://hatintime.com)]{.nowrap}, and [:propane: [Noita](https://noitagame.com/)]{.nowrap}. +But also many many more, because I'm really indecisive. + +Or [*music.*][page:music] +I [listen to a lot of it.][def:social/bandcamp] +And I mean, [_a lot_.][def:social/listenbrainz] +A metric fuck tonne.\ +I'm a huge fan of electronic genres, but also jazz and (alternative) rock from time to time.\ +I listen to [_Songs About My Cats_](https://venetiansnares.bandcamp.com/album/songs-about-my-cats) while coding.\ +My favourite artists are [C418](https://c418.bandcamp.com/album/excursions), [The Flashbulb](https://theflashbulb.bandcamp.com/album/kirlian-selections), [Aphex Twin](https://aphextwin.bandcamp.com/album/drukqs), [Squarepusher](https://squarepusher.bandcamp.com/album/ultravisitor), and [False Noise](https://upscalehq.bandcamp.com/album/floral-strobe). + +I kinda also [make music sometimes](https://daknus.bandcamp.com) when I feel like it. + +I also drew a bit of dawing (...do a bit of drawing), as evidenced by the floofee on this page. + +If all that sounds like an interesting bunch of words... + +- feel free to email me: `hi` at this domain!! +- or add me on Discord---the nickname's *rikimoe*. + +I like to think I'm pretty amicable in person but I'm uh, also really socially awkward...!\ +~Please excuse any social awkwardness that may ensue from you contacting me.\ +Or me contacting you.~ diff --git a/content/index.tree b/content/index.tree index 7f437f2..5e7245f 100644 --- a/content/index.tree +++ b/content/index.tree @@ -73,10 +73,6 @@ visibility = "Private" - I'd like to make some new friends! if you wanna meet me, email `hi` at this domain. -% id = "about" - content.link = "about" -+ ## [``{=html}][page:kuroneko]{.secret}me - % id = "programming" content.link = "programming" + ## ``{=html}programming diff --git a/content/requiem.dj b/content/requiem.dj new file mode 100644 index 0000000..0a32dfc --- /dev/null +++ b/content/requiem.dj @@ -0,0 +1,130 @@ +title = "Requiem for a Fractal Forest" + ++++ + + +It's been almost 2 years since the treehouse's inception, and there've been a lot of changes in the project throughout. +It all started on my holiday in August 2023, when I was bored without a computer. +Reading [Lobsters](https://lobste.rs), I stumbled upon someone's website, and it made me think a lot about how I could make a website I would love as an outlet for writing. +A website that would truly be a reflection of myself, my own values, and my own quirkiness. + +I opened the A5 notepad I had on me during that trip, and started sketching. + +A website made out of bullet points. +A tree of paragraphs, nesting forever and ever, where in every collapsed branch you could find something more. +And it would grow into an ever-more-deeply-nesting fractal forest, a rabbit hole to suck you in for hours. +Imagine [Vsauce](https://www.youtube.com/@Vsauce), but on a blog, and if all the tangents were optional. + +I was really enjoying [Logseq](https://logseq.com/) at the time, and it was my primary source of inspiration for the treehouse. +It just felt like a natural way to organise thoughts, so I wanted to create something like it, but without the lagginess and chugginess coming with a bloated frontend app written in Clojure. + +And that, my friends, is how the treehouse was born. + + +## The implementation + +When I came back home, I jumped straight to my code editor. +First a data format. Then an HTML generator. And then some CSS to style it. + +The first version of the treehouse was up and running. + +It wasn't much, but it worked. +It proved the concept, so I started fleshing it out. + +- I added some JavaScript to make the navigation more friendly. +- Fixed some UX details of the `
` element that bugged me. +- I made it so that branches could expand into lazily-loaded trees, so that you wouldn't have to navigate away from the main page. +- I made it possible to permalink to branches of the tree, so that you could link parts of it to your friends. +- I wrote a script that would generate unique IDs for branches for me automatically, so that I could just run `treehouse fix` before a commit, and everything would be linkable. +- I added dates, so that you could keep track of when something was updated. +- [And so many more things.][page:treehouse/changelog] + +And of course, alongside all those technicalities, I was writing. + +The treehouse is single-handedly what taught me to write regularly about my thoughts, observations, insights, and pet peeves. +And through that, I taught myself to _Write_. \ +Text. Essays. Prose. + +The treehouse has been on my mind as my main project ever since I started it. + +It just resonated with me so strongly.\ +I cherished it like a beloved friend. + +Everything I did revolved around the treehouse. + +Most side projects I did were features, improvements, and weird experiments. +Like that one time I wanted to add a sort-of-visual-novel-type-of-thing to the website, so I started banging out a whole [Twine](https://twinery.org/)-like story graph editor. + +It was all incredibly fun. + + +## The limits + +But throughout this whole process, I was constantly running into problems with the tree format. +You see, the UX just plain _sucked_. + +By that I mean, the basic UX of reading pages was pretty bad. +It felt more like reading a braindump than a polished post, even when I spent hours on structuring, proofreading, and everything. + +The nesting was distracting. +The more of it was there, the worse it would get. +I pretty soon learned you gotta dial it back down to the absolute minimum. 1--3 levels is enough. +Root for introduction and headings, 2 for heading content, and 3 for tangents. + +But it never felt _right_. +As you expanded branches on the main page, they would drift rightwards. +The indentation would quickly get out of hand, eating away all your precious screen space. +I added indent guides to help combat it, but they didn't help. +It was like browsing an overly nested folder. +Probably because it was _precisely that_. + +The UI around it just felt like a huge distraction. +Hovering over a branch always caused something to fade into your view to signal it could be interacted with, and I feel like it resulted in a lack of sense of stability to the pages. +It was detracting you from the actual content of the website. + +It was a fun quirk, but not much more than that. + +I was also reaching the limits of the structure. +A tree can only have one parent, but what if a post fits more than one category? +Where do I put it then? + +And what if I wanna edit posts on the web, or add a section for short tweet-like braindumps, or something? + +It was all incredibly limiting. + + +## The breakpoint + +The moment I decided it was time to let go, was the moment I needed to do a layout change that'd be impossible with a tree. + +I wanted to add my fursona to the right side of the screen. + +Whatever I did, it would eat away too much space, and result in a feeling of imbalance. +I had to eat away all the padding from the left side, but then the text felt too cramped. +It was much too close to the edge of the screen. + +The only real solution I could see was to limit the page to a smaller width, but that didn't work with the amount of UI elements tree branches had. + +So I abandoned them! + +Welcome to the new treehouse. +Or, rather, just house, because there's no longer a tree. +You can call it a _meoooow~_house if you insist. :ahyes: + +--- + +But! + +Even if it's not a tree in structure, it still has to support the tree format for backwards compatibility. +I wouldn't wanna have to rewrite all those pages I've accumulated throughout the past two years. + +But it's the end of an era. +I'll slowly be rewriting _some parts_ of the site to this document-oriented format, just like this page. +But maybe that isn't such a bad thing? + +There are lots of ways you can make a really fun blog post. +[This one comes to mind immediately](https://modem.io/blog/blog-monetization/), and honestly---would you be able to make something that amazing with a tree structure? + +Because I wouldn't. + +So... welcome to the new treehouse :3 diff --git a/content/treehouse/new.tree b/content/treehouse/new.tree index 371207f..5232f89 100644 --- a/content/treehouse/new.tree +++ b/content/treehouse/new.tree @@ -26,6 +26,13 @@ if you've been wondering what I've been up to, you've come to the right place. if you want to read any of the posts, follow the links. it's like that by design. +% tags = ["design", "treehouse"] + id = "01K02XZTW3VYKX0Q5NZ17NRVTF" +- ### [Requiem for a Fractal Forest][page:requiem.dj] + + % id = "01K02XZTW3W08E195CRHJ4XATD" + - A retrospect on the treehouse's form factor, and why I'm getting rid of it. (at least partially) + % tags = ["programming"] id = "01JX0GYB1D4W3A6FRPBG738N4F" - ### [on changing the Firefox New Tab, and software freedom][page:programming/new-tab] diff --git a/crates/treehouse-format/Cargo.toml b/crates/treehouse-format/Cargo.toml deleted file mode 100644 index 7db6b26..0000000 --- a/crates/treehouse-format/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "treehouse-format" -version = "0.1.0" -edition = "2021" - -[dependencies] -thiserror = "1.0.47" diff --git a/crates/treehouse-format/src/lib.rs b/crates/treehouse-format/src/lib.rs deleted file mode 100644 index 6afdf89..0000000 --- a/crates/treehouse-format/src/lib.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::ops::Range; - -pub mod ast; -pub mod pull; - -#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)] -pub enum ParseErrorKind { - #[error("branch kind (`+` or `-`) expected")] - BranchKindExpected, - - #[error("root branches must not be indented")] - RootIndentLevel, - - #[error("at least {expected} spaces of indentation were expected, but got {got}")] - InconsistentIndentation { got: usize, expected: usize }, - - #[error("unterminated code block")] - UnterminatedCodeBlock, -} - -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] -#[error("{range:?}: {kind}")] -pub struct ParseError { - pub kind: ParseErrorKind, - pub range: Range, -} - -impl ParseErrorKind { - pub fn at(self, range: Range) -> ParseError { - ParseError { kind: self, range } - } -} diff --git a/crates/treehouse/Cargo.toml b/crates/treehouse/Cargo.toml deleted file mode 100644 index 2198fdf..0000000 --- a/crates/treehouse/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "treehouse" -version = "0.1.0" -edition = "2021" - -[dependencies] - -treehouse-format = { workspace = true } - -anyhow = "1.0.75" -axum = { version = "0.7.9", features = ["macros"] } -axum-macros = "0.4.2" -base64 = "0.21.7" -blake3 = "1.5.3" -chrono = { version = "0.4.35", features = ["serde"] } -clap = { version = "4.3.22", features = ["derive"] } -codespan-reporting = "0.11.1" -dashmap = "6.1.0" -git2 = { version = "0.19.0", default-features = false, features = ["vendored-libgit2"] } -handlebars = "4.3.7" -image = "0.25.5" -indexmap = { version = "2.2.6", features = ["serde"] } -jotdown = { version = "0.4.1", default-features = false } -rand = "0.8.5" -rayon = "1.10.0" -regex = "1.10.3" -serde = { version = "1.0.183", features = ["derive"] } -serde_json = "1.0.105" -tokio = { version = "1.32.0", features = ["full"] } -toml_edit = { version = "0.19.14", features = ["serde"] } -tracing.workspace = true -tracing-chrome = "0.7.2" -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } -ulid = "1.0.0" -webp = "0.3.0" -xmlparser = "0.13.6" diff --git a/crates/treehouse/src/generate.rs b/crates/treehouse/src/generate.rs deleted file mode 100644 index ec43d98..0000000 --- a/crates/treehouse/src/generate.rs +++ /dev/null @@ -1,238 +0,0 @@ -mod atom; -mod dir_helper; -mod include_static_helper; -mod simple_template; -mod tree; - -use std::{collections::HashMap, fmt, ops::ControlFlow, sync::Arc}; - -use atom::FeedDir; -use dir_helper::DirHelper; -use handlebars::{handlebars_helper, Handlebars}; -use include_static_helper::IncludeStaticHelper; -use serde::Serialize; -use tracing::{error, info_span, instrument}; - -use crate::{ - config::Config, - dirs::Dirs, - fun::seasons::Season, - sources::Sources, - vfs::{ - self, AnchoredAtExt, Cd, Content, ContentCache, Dir, DynDir, Entries, HtmlCanonicalize, - MemDir, Overlay, ToDynDir, VPath, VPathBuf, - }, -}; - -#[derive(Serialize)] -struct BaseTemplateData<'a> { - config: &'a Config, - import_map: String, - season: Option, - dev: bool, - feeds: Vec, -} - -impl<'a> BaseTemplateData<'a> { - fn new(sources: &'a Sources) -> Self { - Self { - config: &sources.config, - import_map: serde_json::to_string_pretty(&sources.import_map) - .expect("import map should be serializable to JSON"), - season: Season::current(), - dev: cfg!(debug_assertions), - feeds: sources.treehouse.feeds_by_name.keys().cloned().collect(), - } - } -} - -fn create_handlebars(site: &str, static_: DynDir) -> Handlebars<'static> { - let mut handlebars = Handlebars::new(); - - handlebars_helper!(cat: |a: String, b: String| a + &b); - - handlebars.register_helper("cat", Box::new(cat)); - handlebars.register_helper("asset", Box::new(DirHelper::new(site, static_.clone()))); - handlebars.register_helper( - "include_static", - Box::new(IncludeStaticHelper::new(static_)), - ); - - handlebars -} - -#[instrument(skip(handlebars))] -fn load_templates(handlebars: &mut Handlebars, dir: &dyn Dir) { - vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| { - if path.extension() == Some("hbs") { - if let Some(content) = vfs::query::(dir, path).and_then(|c| c.string().ok()) { - let _span = info_span!("register_template", ?path).entered(); - if let Err(err) = handlebars.register_template_string(path.as_str(), content) { - error!("in template: {err}"); - } - } - } - ControlFlow::Continue(()) - }); -} - -struct TreehouseDir { - dirs: Arc, - sources: Arc, - handlebars: Arc>, - dir_index: DirIndex, -} - -impl TreehouseDir { - fn new( - dirs: Arc, - sources: Arc, - handlebars: Arc>, - dir_index: DirIndex, - ) -> Self { - Self { - dirs, - sources, - handlebars, - dir_index, - } - } - - #[instrument("TreehouseDir::dir", skip(self))] - fn dir(&self, path: &VPath) -> Vec { - // NOTE: This does not include simple templates, because that's not really needed right now. - - let mut index = &self.dir_index; - for component in path.segments() { - if let Some(child) = index.children.get(component) { - index = child; - } else { - // There cannot possibly be any entries under an invalid path. - // Bail early. - return vec![]; - } - } - - index - .children - .values() - .map(|child| child.full_path.clone()) - .collect() - } - - #[instrument("TreehouseDir::content", skip(self))] - fn content(&self, path: &VPath) -> Option { - let path = if path.is_root() { - VPath::new_const("index") - } else { - path - }; - - self.sources - .treehouse - .files_by_tree_path - .get(path) - .map(|&file_id| { - Content::new( - "text/html", - tree::generate_or_error(&self.sources, &self.dirs, &self.handlebars, file_id) - .into(), - ) - }) - .or_else(|| { - if path.file_name().is_some_and(|s| !s.starts_with('_')) { - let template_name = path.with_extension("hbs"); - if self.handlebars.has_template(template_name.as_str()) { - return Some(Content::new( - "text/html", - simple_template::generate_or_error( - &self.sources, - &self.handlebars, - template_name.as_str(), - ) - .into(), - )); - } - } - None - }) - } -} - -impl Dir for TreehouseDir { - fn query(&self, path: &VPath, query: &mut vfs::Query) { - query.provide(|| Entries(self.dir(path))); - query.try_provide(|| self.content(path)); - } -} - -impl fmt::Debug for TreehouseDir { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("TreehouseDir") - } -} - -/// Acceleration structure for `dir` operations on [`TreehouseDir`]s. -#[derive(Debug, Default)] -struct DirIndex { - full_path: VPathBuf, - children: HashMap, -} - -impl DirIndex { - #[instrument(name = "DirIndex::new", skip(paths))] - pub fn new<'a>(paths: impl Iterator) -> Self { - let mut root = DirIndex::default(); - - for path in paths { - let mut parent = &mut root; - let mut full_path = VPath::ROOT.to_owned(); - for segment in path.segments() { - full_path.push(segment); - let child = parent - .children - .entry(segment.to_owned()) - .or_insert_with(|| DirIndex { - full_path: full_path.clone(), - children: HashMap::new(), - }); - parent = child; - } - } - - root - } -} - -pub fn target(dirs: Arc, sources: Arc) -> DynDir { - let mut handlebars = create_handlebars(&sources.config.site, dirs.static_.clone()); - load_templates(&mut handlebars, &dirs.template); - let handlebars = Arc::new(handlebars); - - let mut root = MemDir::new(); - root.add( - VPath::new("feed"), - ContentCache::new(FeedDir::new( - dirs.clone(), - sources.clone(), - handlebars.clone(), - )) - .to_dyn(), - ); - root.add(VPath::new("static"), dirs.static_.clone()); - root.add( - VPath::new("robots.txt"), - Cd::new(dirs.static_.clone(), VPathBuf::new("robots.txt")).to_dyn(), - ); - - let dir_index = DirIndex::new(sources.treehouse.files_by_tree_path.keys().map(|x| &**x)); - let tree_view = TreehouseDir::new(dirs, sources, handlebars, dir_index); - - let tree_view = ContentCache::new(tree_view); - tree_view.warm_up(); - let tree_view = HtmlCanonicalize::new(tree_view); - - Overlay::new(tree_view.to_dyn(), root.to_dyn()) - .anchored_at(VPath::ROOT.to_owned()) - .to_dyn() -} diff --git a/crates/treehouse/src/generate/simple_template.rs b/crates/treehouse/src/generate/simple_template.rs deleted file mode 100644 index 6ed88e6..0000000 --- a/crates/treehouse/src/generate/simple_template.rs +++ /dev/null @@ -1,30 +0,0 @@ -use anyhow::Context; -use handlebars::Handlebars; -use tracing::instrument; - -use crate::sources::Sources; - -use super::BaseTemplateData; - -#[instrument(name = "simple_template::generate", skip(sources, handlebars))] -pub fn generate( - sources: &Sources, - handlebars: &Handlebars, - template_name: &str, -) -> anyhow::Result { - let base_template_data = BaseTemplateData::new(sources); - handlebars - .render(template_name, &base_template_data) - .context("failed to render template") -} - -pub fn generate_or_error( - sources: &Sources, - handlebars: &Handlebars, - template_name: &str, -) -> String { - match generate(sources, handlebars, template_name) { - Ok(html) => html, - Err(error) => format!("error: {error:?}"), - } -} diff --git a/crates/treehouse/src/generate/tree.rs b/crates/treehouse/src/generate/tree.rs deleted file mode 100644 index 9fc77aa..0000000 --- a/crates/treehouse/src/generate/tree.rs +++ /dev/null @@ -1,111 +0,0 @@ -use anyhow::{ensure, Context}; -use handlebars::Handlebars; -use serde::Serialize; -use tracing::{info_span, instrument}; - -use crate::{ - dirs::Dirs, - generate::BaseTemplateData, - html::{breadcrumbs::breadcrumbs_to_html, tree}, - sources::Sources, - state::FileId, -}; - -#[derive(Serialize)] -struct Page { - title: String, - thumbnail: Option, - scripts: Vec, - styles: Vec, - breadcrumbs: String, - tree_path: Option, - tree: String, -} - -#[derive(Serialize)] -struct Thumbnail { - url: String, - alt: Option, -} - -#[derive(Serialize)] -struct PageTemplateData<'a> { - #[serde(flatten)] - base: &'a BaseTemplateData<'a>, - page: Page, -} - -#[instrument(skip(sources, dirs, handlebars))] -pub fn generate( - sources: &Sources, - dirs: &Dirs, - handlebars: &Handlebars, - file_id: FileId, -) -> anyhow::Result { - let breadcrumbs = breadcrumbs_to_html(&sources.config, &sources.navigation_map, file_id); - - let roots = sources - .treehouse - .roots - .get(&file_id) - .expect("tree should have been added to the treehouse"); - - let tree = { - let _span = info_span!("generate_tree::root_to_html").entered(); - let renderer = tree::Renderer { - sources, - dirs, - file_id, - }; - let mut tree = String::new(); - renderer.root(&mut tree); - tree - }; - - let template_data = PageTemplateData { - base: &BaseTemplateData::new(sources), - page: Page { - title: roots.attributes.title.clone(), - thumbnail: roots - .attributes - .thumbnail - .as_ref() - .map(|thumbnail| Thumbnail { - url: sources.config.pic_url(&*dirs.pic, &thumbnail.id), - alt: thumbnail.alt.clone(), - }), - scripts: roots.attributes.scripts.clone(), - styles: roots.attributes.styles.clone(), - breadcrumbs, - tree_path: sources.treehouse.tree_path(file_id).map(|s| s.to_string()), - tree, - }, - }; - let template_name = roots - .attributes - .template - .clone() - .unwrap_or_else(|| "_tree.hbs".into()); - - ensure!( - handlebars.has_template(&template_name), - "template {template_name} does not exist" - ); - - let _span = info_span!("handlebars::render").entered(); - handlebars - .render(&template_name, &template_data) - .context("template rendering failed") -} - -pub fn generate_or_error( - sources: &Sources, - dirs: &Dirs, - handlebars: &Handlebars, - file_id: FileId, -) -> String { - match generate(sources, dirs, handlebars, file_id) { - Ok(html) => html, - Err(error) => format!("error: {error:?}"), - } -} diff --git a/crates/treehouse/src/cli.rs b/src/cli.rs similarity index 100% rename from crates/treehouse/src/cli.rs rename to src/cli.rs diff --git a/crates/treehouse/src/cli/fix.rs b/src/cli/fix.rs similarity index 99% rename from crates/treehouse/src/cli/fix.rs rename to src/cli/fix.rs index b5e1342..6e64cc0 100644 --- a/crates/treehouse/src/cli/fix.rs +++ b/src/cli/fix.rs @@ -3,11 +3,11 @@ use std::ops::{ControlFlow, Range}; use anyhow::{anyhow, Context}; use codespan_reporting::diagnostic::Diagnostic; use tracing::{error, info}; -use treehouse_format::ast::Branch; use crate::{ parse::{self, parse_toml_with_diagnostics, parse_tree_with_diagnostics}, state::{report_diagnostics, FileId, Source, Treehouse}, + tree::ast::Branch, vfs::{self, Content, Dir, Edit, EditPath, VPath}, }; diff --git a/crates/treehouse/src/cli/serve.rs b/src/cli/serve.rs similarity index 100% rename from crates/treehouse/src/cli/serve.rs rename to src/cli/serve.rs diff --git a/crates/treehouse/src/cli/serve/live_reload.rs b/src/cli/serve/live_reload.rs similarity index 100% rename from crates/treehouse/src/cli/serve/live_reload.rs rename to src/cli/serve/live_reload.rs diff --git a/crates/treehouse/src/cli/serve/picture_upload.rs b/src/cli/serve/picture_upload.rs similarity index 100% rename from crates/treehouse/src/cli/serve/picture_upload.rs rename to src/cli/serve/picture_upload.rs diff --git a/crates/treehouse/src/cli/wc.rs b/src/cli/wc.rs similarity index 97% rename from crates/treehouse/src/cli/wc.rs rename to src/cli/wc.rs index b210225..7028dc3 100644 --- a/crates/treehouse/src/cli/wc.rs +++ b/src/cli/wc.rs @@ -1,10 +1,9 @@ use std::ops::ControlFlow; -use treehouse_format::ast::{Branch, Roots}; - use crate::{ parse::parse_tree_with_diagnostics, state::{report_diagnostics, Source, Treehouse}, + tree::ast::{Branch, Roots}, vfs::{self, Content, Dir, VPath}, }; diff --git a/crates/treehouse/src/config.rs b/src/config.rs similarity index 97% rename from crates/treehouse/src/config.rs rename to src/config.rs index 48fe361..c55cba2 100644 --- a/crates/treehouse/src/config.rs +++ b/src/config.rs @@ -152,6 +152,8 @@ impl Config { } pub fn page_url(&self, page: &str) -> String { + // We don't want .dj appearing in URLs, though it exists as a disambiguator in [page:] links. + let page = page.strip_suffix(".dj").unwrap_or(page); format!("{}/{}", self.site, page) } diff --git a/crates/treehouse/src/dirs.rs b/src/dirs.rs similarity index 100% rename from crates/treehouse/src/dirs.rs rename to src/dirs.rs diff --git a/crates/treehouse/src/fun.rs b/src/fun.rs similarity index 100% rename from crates/treehouse/src/fun.rs rename to src/fun.rs diff --git a/crates/treehouse/src/fun/seasons.rs b/src/fun/seasons.rs similarity index 100% rename from crates/treehouse/src/fun/seasons.rs rename to src/fun/seasons.rs diff --git a/src/generate.rs b/src/generate.rs new file mode 100644 index 0000000..3caa73e --- /dev/null +++ b/src/generate.rs @@ -0,0 +1,128 @@ +mod atom; +mod dir_helper; +mod doc; +mod include_static_helper; +mod simple_template; +mod tree; + +use std::{ops::ControlFlow, sync::Arc}; + +use atom::FeedDir; +use chrono::{DateTime, Utc}; +use dir_helper::DirHelper; +use handlebars::{handlebars_helper, Handlebars}; +use include_static_helper::IncludeStaticHelper; +use serde::Serialize; +use tracing::{error, info_span, instrument}; + +use crate::{ + config::Config, + dirs::Dirs, + fun::seasons::Season, + generate::{ + doc::DocDir, + simple_template::SimpleTemplateDir, + tree::{DirIndex, TreehouseDir}, + }, + sources::Sources, + vfs::{ + self, layered_dir, AnchoredAtExt, Cd, Content, ContentCache, Dir, DynDir, HtmlCanonicalize, + MemDir, ToDynDir, VPath, VPathBuf, + }, +}; + +#[derive(Serialize)] +struct BaseTemplateData<'a> { + config: &'a Config, + import_map: String, + season: Option, + dev: bool, + feeds: Vec, +} + +impl<'a> BaseTemplateData<'a> { + fn new(sources: &'a Sources) -> Self { + Self { + config: &sources.config, + import_map: serde_json::to_string_pretty(&sources.import_map) + .expect("import map should be serializable to JSON"), + season: Season::current(), + dev: cfg!(debug_assertions), + feeds: sources.treehouse.feeds_by_name.keys().cloned().collect(), + } + } +} + +fn create_handlebars(site: &str, static_: DynDir) -> Handlebars<'static> { + let mut handlebars = Handlebars::new(); + + handlebars_helper!(cat: |a: String, b: String| a + &b); + handlebars_helper!(iso_date: |d: DateTime| d.format("%F").to_string()); + + handlebars.register_helper("cat", Box::new(cat)); + handlebars.register_helper("iso_date", Box::new(iso_date)); + handlebars.register_helper("asset", Box::new(DirHelper::new(site, static_.clone()))); + handlebars.register_helper( + "include_static", + Box::new(IncludeStaticHelper::new(static_)), + ); + + handlebars +} + +#[instrument(skip(handlebars))] +fn load_templates(handlebars: &mut Handlebars, dir: &dyn Dir) { + vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| { + if path.extension() == Some("hbs") { + if let Some(content) = vfs::query::(dir, path).and_then(|c| c.string().ok()) { + let _span = info_span!("register_template", ?path).entered(); + if let Err(err) = handlebars.register_template_string(path.as_str(), content) { + error!("in template: {err}"); + } + } + } + ControlFlow::Continue(()) + }); +} + +pub fn target(dirs: Arc, sources: Arc) -> DynDir { + let mut handlebars = create_handlebars(&sources.config.site, dirs.static_.clone()); + load_templates(&mut handlebars, &dirs.template); + let handlebars = Arc::new(handlebars); + + let mut root = MemDir::new(); + root.add( + VPath::new("feed"), + ContentCache::new(FeedDir::new( + dirs.clone(), + sources.clone(), + handlebars.clone(), + )) + .to_dyn(), + ); + root.add(VPath::new("static"), dirs.static_.clone()); + root.add( + VPath::new("robots.txt"), + Cd::new(dirs.static_.clone(), VPathBuf::new("robots.txt")).to_dyn(), + ); + + let dir_index = DirIndex::new(sources.treehouse.files_by_tree_path.keys().map(|x| &**x)); + let treehouse_dir = layered_dir(&[ + TreehouseDir::new(dirs.clone(), sources.clone(), handlebars.clone(), dir_index).to_dyn(), + DocDir { + sources: sources.clone(), + dirs, + handlebars: handlebars.clone(), + } + .to_dyn(), + SimpleTemplateDir::new(sources.clone(), handlebars.clone()).to_dyn(), + ]); + + let tree_view = ContentCache::new(treehouse_dir); + tree_view.warm_up(); + let tree_view = HtmlCanonicalize::new(tree_view); + + layered_dir(&[tree_view.to_dyn(), root.to_dyn()]) + .anchored_at(VPath::ROOT.to_owned()) + .to_dyn() +} diff --git a/crates/treehouse/src/generate/atom.rs b/src/generate/atom.rs similarity index 64% rename from crates/treehouse/src/generate/atom.rs rename to src/generate/atom.rs index 9a39f7c..1c042eb 100644 --- a/crates/treehouse/src/generate/atom.rs +++ b/src/generate/atom.rs @@ -4,15 +4,14 @@ use anyhow::Context; use chrono::{DateTime, Utc}; use handlebars::Handlebars; use serde::Serialize; -use tracing::{info, info_span, instrument}; -use ulid::Ulid; +use tracing::{info_span, instrument}; use crate::{ dirs::Dirs, html::djot::{self, resolve_link}, sources::Sources, state::FileId, - tree::SemaBranchId, + tree::{feed, SemaBranchId}, vfs::{self, Content, Dir, Entries, VPath, VPathBuf}, }; @@ -156,7 +155,7 @@ fn extract_entries(sources: &Sources, dirs: &Dirs, file_id: FileId) -> Vec Vec, - link: Option, -} - -fn parse_entry( - sources: &Sources, - dirs: &Dirs, - file_id: FileId, - parser: jotdown::Parser, -) -> ParsedEntry { - let mut parser = parser.into_offset_iter(); - while let Some((event, span)) = parser.next() { - if let jotdown::Event::Start(jotdown::Container::Heading { .. }, _attrs) = &event { - let mut events = vec![(event, span)]; - for (event, span) in parser.by_ref() { - // To my knowledge headings cannot nest, so it's okay not keeping a stack here. - let is_heading = matches!( - event, - jotdown::Event::End(jotdown::Container::Heading { .. }) - ); - events.push((event, span)); - if is_heading { - break; - } - } - - let title_events: Vec<_> = events - .iter() - .filter(|(event, _)| { - !matches!( - event, - // A little repetitive, but I don't mind. - // The point of this is not to include extra

and in the link text, - // but preserve other formatting such as bold, italic, code, etc. - jotdown::Event::Start( - jotdown::Container::Link(_, _) | jotdown::Container::Heading { .. }, - _ - ) | jotdown::Event::End( - jotdown::Container::Link(_, _) | jotdown::Container::Heading { .. } - ) - ) - }) - .cloned() - .collect(); - let mut title = String::new(); - let _render_diagnostics = djot::Renderer { - config: &sources.config, - dirs, - treehouse: &sources.treehouse, - file_id, - - // How. Just, stop. - page_id: "liquidex-you-reeeeeal-dummy".into(), - } - .render(&title_events, &mut title); - - let link = events.iter().find_map(|(event, _)| { - if let jotdown::Event::Start(jotdown::Container::Link(link, link_type), _) = event { - Some(link_url(sources, dirs, link, *link_type)) - } else { - None - } - }); - - return ParsedEntry { - title: (!title.is_empty()).then_some(title), - link, - }; - } - } - - ParsedEntry { - title: None, - link: None, - } -} - -fn link_url(sources: &Sources, dirs: &Dirs, url: &str, link_type: jotdown::LinkType) -> String { - if let jotdown::LinkType::Span(jotdown::SpanLinkType::Unresolved) = link_type { - if let Some(url) = resolve_link(&sources.config, &sources.treehouse, dirs, url) { - return url; - } - } - url.to_owned() -} - /// Extremely simple HTML renderer without the treehouse's fancy branch folding and linking features. fn branches_to_html_simple( s: &mut String, diff --git a/crates/treehouse/src/generate/dir_helper.rs b/src/generate/dir_helper.rs similarity index 100% rename from crates/treehouse/src/generate/dir_helper.rs rename to src/generate/dir_helper.rs diff --git a/src/generate/doc.rs b/src/generate/doc.rs new file mode 100644 index 0000000..eda596f --- /dev/null +++ b/src/generate/doc.rs @@ -0,0 +1,242 @@ +use std::{ + fmt::{self}, + sync::Arc, +}; + +use anyhow::Context; +use chrono::{DateTime, Utc}; +use handlebars::Handlebars; +use serde::{Deserialize, Serialize}; +use tracing::{error, instrument}; + +use crate::{ + dirs::Dirs, + generate::BaseTemplateData, + html::djot, + sources::Sources, + state::{report_diagnostics, toml_error_to_diagnostic, FileId, TomlError}, + tree::{attributes::Picture, feed}, + vfs::{Content, Dir, Query, VPath}, +}; + +#[derive(Default, Deserialize)] +struct Attributes { + /// Template to use for generating the page. + /// Defaults to `_tree.hbs`. + #[serde(default)] + template: Option, + + /// Title of the page. + /// The only necessary field. + /// Unlike tree pages, doc pages always have titles. + title: String, + + /// ID of picture attached to the page, to be used as a thumbnail. + #[serde(default)] + thumbnail: Option, + + /// Additional scripts to load into to the page. + /// These are relative to the /static/js directory. + #[serde(default)] + scripts: Vec, + + /// Additional styles to load into to the page. + /// These are relative to the /static/css directory. + #[serde(default)] + styles: Vec, + + /// If not `None`, the page will get an additional 'feed' field in template data, containing + /// updates from the news feed of the specified name. + #[serde(default)] + include_feed: Option, +} + +#[derive(Deserialize)] +struct IncludeFeed { + /// The name of the feed (within the treehouse database.) + name: String, + + /// The title of the feed shown on the page. + title: String, +} + +#[derive(Serialize)] +struct Page { + title: String, + thumbnail: Option, + scripts: Vec, + styles: Vec, + tree_path: String, + doc: String, + feed: Option, +} + +#[derive(Serialize)] +struct Thumbnail { + url: String, + alt: Option, +} + +#[derive(Serialize)] +struct Feed { + title: String, + entries: Vec, +} + +#[derive(Serialize)] +struct Entry { + title: String, + url: String, + updated: DateTime, + categories: Vec, +} + +#[derive(Serialize)] +struct PageTemplateData<'a> { + #[serde(flatten)] + base: &'a BaseTemplateData<'a>, + page: Page, +} + +pub struct DocDir { + pub sources: Arc, + pub dirs: Arc, + + pub handlebars: Arc>, +} + +impl DocDir { + #[instrument("DocDir::content", skip(self))] + pub fn content(&self, path: &VPath) -> Option { + if let Some(file_id) = self + .sources + .treehouse + .files_by_doc_path + .get(&path.with_extension("dj")) + { + let source = self.sources.treehouse.source(*file_id).input(); + return Some(Content::new( + "text/html", + self.generate(*file_id, path, source).into_bytes(), + )); + } + + None + } + + fn generate(&self, file_id: FileId, path: &VPath, source: &str) -> String { + let (front_matter, text) = source.split_once("+++").unwrap_or(("", source)); + let attributes: Attributes = + toml_edit::de::from_str(front_matter).unwrap_or_else(|error| { + _ = report_diagnostics( + &self.sources.treehouse, + &[toml_error_to_diagnostic(TomlError { + message: error.message().to_owned(), + span: error.span(), + file_id, + input_range: 0..front_matter.len(), + })], + ); + Attributes::default() + }); + + let events: Vec<_> = jotdown::Parser::new(text).into_offset_iter().collect(); + let mut rendered_markup = String::new(); + let render_diagnostics = djot::Renderer { + config: &self.sources.config, + dirs: &self.dirs, + treehouse: &self.sources.treehouse, + file_id, + page_id: path.to_string(), + } + .render(&events, &mut rendered_markup); + + let template_name = attributes.template.as_deref().unwrap_or("_doc.hbs"); + + let render_result = self + .handlebars + .render( + template_name, + &PageTemplateData { + base: &BaseTemplateData::new(&self.sources), + page: Page { + title: attributes.title, + thumbnail: attributes.thumbnail.map(|pic| Thumbnail { + url: self.sources.config.pic_url(&*self.dirs.pic, &pic.id), + alt: pic.alt, + }), + scripts: attributes.scripts, + styles: attributes.styles, + tree_path: path.to_string(), + doc: rendered_markup, + feed: attributes.include_feed.and_then(|feed| { + Some(Feed { + title: feed.title, + entries: self + .generate_feed(&feed.name) + .inspect_err(|e| { + error!("generating feed for {path} failed: {e}") + }) + .ok()?, + }) + }), + }, + }, + ) + .context("template rendering failed"); + match render_result { + Ok(rendered) => rendered, + Err(error) => format!("{error:#?}"), + } + } + + fn generate_feed(&self, name: &str) -> anyhow::Result> { + let file_id = *self + .sources + .treehouse + .feeds_by_name + .get(name) + .context("no feed with the given name")?; + let roots = &self.sources.treehouse.roots[&file_id]; + + Ok(roots + .branches + .iter() + .flat_map(|&branch_id| { + let branch = self.sources.treehouse.tree.branch(branch_id); + + let text = &self.sources.treehouse.source(file_id).input()[branch.content.clone()]; + let parsed = feed::parse_entry( + &self.sources, + &self.dirs, + file_id, + jotdown::Parser::new(text), + ); + + let updated = branch + .attributes + .timestamp() + .unwrap_or(DateTime::UNIX_EPOCH); // if you see the Unix epoch... oops + + parsed.link.map(|url| Entry { + updated, + url, + title: parsed.title.unwrap_or_else(|| "untitled".into()), + categories: branch.attributes.tags.clone(), + }) + }) + .collect()) + } +} + +impl Dir for DocDir { + fn query(&self, path: &VPath, query: &mut Query) { + query.try_provide(|| self.content(path)); + } +} + +impl fmt::Debug for DocDir { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("DocDir") + } +} diff --git a/crates/treehouse/src/generate/include_static_helper.rs b/src/generate/include_static_helper.rs similarity index 100% rename from crates/treehouse/src/generate/include_static_helper.rs rename to src/generate/include_static_helper.rs diff --git a/src/generate/simple_template.rs b/src/generate/simple_template.rs new file mode 100644 index 0000000..879485b --- /dev/null +++ b/src/generate/simple_template.rs @@ -0,0 +1,70 @@ +use std::{fmt, sync::Arc}; + +use anyhow::Context; +use handlebars::Handlebars; +use tracing::instrument; + +use crate::{ + sources::Sources, + vfs::{Content, Dir, Query, VPath}, +}; + +use super::BaseTemplateData; + +pub struct SimpleTemplateDir { + sources: Arc, + handlebars: Arc>, +} + +impl SimpleTemplateDir { + pub fn new(sources: Arc, handlebars: Arc>) -> Self { + Self { + sources, + handlebars, + } + } + + #[instrument(name = "simple_template::generate", skip(self))] + fn generate(&self, template_name: &str) -> anyhow::Result { + let base_template_data = BaseTemplateData::new(&self.sources); + self.handlebars + .render(template_name, &base_template_data) + .context("failed to render template") + } + + fn generate_or_error(&self, template_name: &str) -> String { + match self.generate(template_name) { + Ok(html) => html, + Err(error) => format!("error: {error:?}"), + } + } + + #[instrument("TreehouseDir::content", skip(self))] + fn content(&self, path: &VPath) -> Option { + if path.file_name().is_some_and(|s| !s.starts_with('_')) { + let template_name = path.with_extension("hbs"); + if self.handlebars.has_template(template_name.as_str()) { + return Some(Content::new( + "text/html", + self.generate_or_error(template_name.as_str()).into(), + )); + } + } + + None + } +} + +impl Dir for SimpleTemplateDir { + fn query(&self, path: &VPath, query: &mut Query) { + // NOTE: An implementation of Entries is not currently provided, because SimpleTemplateDir + // isn't used enough to need one. + query.try_provide(|| self.content(path)); + } +} + +impl fmt::Debug for SimpleTemplateDir { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("SimpleTemplateDir") + } +} diff --git a/src/generate/tree.rs b/src/generate/tree.rs new file mode 100644 index 0000000..9ecd14c --- /dev/null +++ b/src/generate/tree.rs @@ -0,0 +1,224 @@ +use std::{collections::HashMap, fmt, sync::Arc}; + +use anyhow::{ensure, Context}; +use handlebars::Handlebars; +use serde::Serialize; +use tracing::{info_span, instrument}; + +use crate::{ + dirs::Dirs, + generate::{simple_template, BaseTemplateData}, + html::{breadcrumbs::breadcrumbs_to_html, tree}, + sources::Sources, + state::FileId, + vfs::{self, Content, Dir, Entries, VPath, VPathBuf}, +}; + +#[derive(Serialize)] +struct Page { + title: String, + thumbnail: Option, + scripts: Vec, + styles: Vec, + breadcrumbs: String, + tree_path: Option, + tree: String, +} + +#[derive(Serialize)] +struct Thumbnail { + url: String, + alt: Option, +} + +#[derive(Serialize)] +struct PageTemplateData<'a> { + #[serde(flatten)] + base: &'a BaseTemplateData<'a>, + page: Page, +} + +#[instrument(skip(sources, dirs, handlebars))] +pub fn generate( + sources: &Sources, + dirs: &Dirs, + handlebars: &Handlebars, + file_id: FileId, +) -> anyhow::Result { + let breadcrumbs = breadcrumbs_to_html(&sources.config, &sources.navigation_map, file_id); + + let roots = sources + .treehouse + .roots + .get(&file_id) + .expect("tree should have been added to the treehouse"); + + let tree = { + let _span = info_span!("generate_tree::root_to_html").entered(); + let renderer = tree::Renderer { + sources, + dirs, + file_id, + }; + let mut tree = String::new(); + renderer.root(&mut tree); + tree + }; + + let template_data = PageTemplateData { + base: &BaseTemplateData::new(sources), + page: Page { + title: roots.attributes.title.clone(), + thumbnail: roots + .attributes + .thumbnail + .as_ref() + .map(|thumbnail| Thumbnail { + url: sources.config.pic_url(&*dirs.pic, &thumbnail.id), + alt: thumbnail.alt.clone(), + }), + scripts: roots.attributes.scripts.clone(), + styles: roots.attributes.styles.clone(), + breadcrumbs, + tree_path: sources.treehouse.tree_path(file_id).map(|s| s.to_string()), + tree, + }, + }; + let template_name = roots + .attributes + .template + .clone() + .unwrap_or_else(|| "_tree.hbs".into()); + + ensure!( + handlebars.has_template(&template_name), + "template {template_name} does not exist" + ); + + let _span = info_span!("handlebars::render").entered(); + handlebars + .render(&template_name, &template_data) + .context("template rendering failed") +} + +pub fn generate_or_error( + sources: &Sources, + dirs: &Dirs, + handlebars: &Handlebars, + file_id: FileId, +) -> String { + match generate(sources, dirs, handlebars, file_id) { + Ok(html) => html, + Err(error) => format!("error: {error:?}"), + } +} + +pub struct TreehouseDir { + dirs: Arc, + sources: Arc, + handlebars: Arc>, + dir_index: DirIndex, +} + +impl TreehouseDir { + pub fn new( + dirs: Arc, + sources: Arc, + handlebars: Arc>, + dir_index: DirIndex, + ) -> Self { + Self { + dirs, + sources, + handlebars, + dir_index, + } + } + + #[instrument("TreehouseDir::dir", skip(self))] + fn dir(&self, path: &VPath) -> Vec { + // NOTE: This does not include simple templates, because that's not really needed right now. + + let mut index = &self.dir_index; + for component in path.segments() { + if let Some(child) = index.children.get(component) { + index = child; + } else { + // There cannot possibly be any entries under an invalid path. + // Bail early. + return vec![]; + } + } + + index + .children + .values() + .map(|child| child.full_path.clone()) + .collect() + } + + #[instrument("TreehouseDir::content", skip(self))] + fn content(&self, path: &VPath) -> Option { + let path = if path.is_root() { + VPath::new_const("index") + } else { + path + }; + + self.sources + .treehouse + .files_by_tree_path + .get(path) + .map(|&file_id| { + Content::new( + "text/html", + generate_or_error(&self.sources, &self.dirs, &self.handlebars, file_id).into(), + ) + }) + } +} + +impl Dir for TreehouseDir { + fn query(&self, path: &VPath, query: &mut vfs::Query) { + query.provide(|| Entries(self.dir(path))); + query.try_provide(|| self.content(path)); + } +} + +impl fmt::Debug for TreehouseDir { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("TreehouseDir") + } +} + +/// Acceleration structure for `dir` operations on [`TreehouseDir`]s. +#[derive(Debug, Default)] +pub struct DirIndex { + full_path: VPathBuf, + children: HashMap, +} + +impl DirIndex { + #[instrument(name = "DirIndex::new", skip(paths))] + pub fn new<'a>(paths: impl Iterator) -> Self { + let mut root = DirIndex::default(); + + for path in paths { + let mut parent = &mut root; + let mut full_path = VPath::ROOT.to_owned(); + for segment in path.segments() { + full_path.push(segment); + let child = parent + .children + .entry(segment.to_owned()) + .or_insert_with(|| DirIndex { + full_path: full_path.clone(), + children: HashMap::new(), + }); + parent = child; + } + } + + root + } +} diff --git a/crates/treehouse/src/history.rs b/src/history.rs similarity index 100% rename from crates/treehouse/src/history.rs rename to src/history.rs diff --git a/crates/treehouse/src/html.rs b/src/html.rs similarity index 100% rename from crates/treehouse/src/html.rs rename to src/html.rs diff --git a/crates/treehouse/src/html/breadcrumbs.rs b/src/html/breadcrumbs.rs similarity index 100% rename from crates/treehouse/src/html/breadcrumbs.rs rename to src/html/breadcrumbs.rs diff --git a/crates/treehouse/src/html/djot.rs b/src/html/djot.rs similarity index 94% rename from crates/treehouse/src/html/djot.rs rename to src/html/djot.rs index 582aae4..8ce3b11 100644 --- a/crates/treehouse/src/html/djot.rs +++ b/src/html/djot.rs @@ -106,21 +106,6 @@ impl<'a> Writer<'a> { range: Range, out: &mut String, ) -> std::fmt::Result { - if let Event::Start(Container::Footnote { label: _ }, ..) = e { - self.diagnostics.push(Diagnostic { - severity: Severity::Error, - code: Some("djot".into()), - message: "Djot footnotes are not supported".into(), - labels: vec![Label { - style: LabelStyle::Primary, - file_id: self.renderer.file_id, - range: range.clone(), - message: "".into(), - }], - notes: vec![], - }) - } - if matches!(&e, Event::Start(Container::LinkDefinition { .. }, ..)) { self.ignore_next_event = true; return Ok(()); @@ -163,7 +148,7 @@ impl<'a> Writer<'a> { } => { out.push_str(" 1 { - write!(out, r#" start="{}""#, start)?; + write!(out, r#" start="{start}""#)?; } if let Some(ty) = match numbering { Decimal => None, @@ -172,7 +157,7 @@ impl<'a> Writer<'a> { RomanLower => Some('i'), RomanUpper => Some('I'), } { - write!(out, r#" type="{}""#, ty)?; + write!(out, r#" type="{ty}""#)?; } } } @@ -182,7 +167,7 @@ impl<'a> Writer<'a> { } Container::DescriptionList => out.push_str(" out.push_str(" unreachable!(), + Container::Footnote { label } => out.push_str(label), Container::Table => out.push_str(" out.push_str(" {} @@ -193,7 +178,7 @@ impl<'a> Writer<'a> { } out.push_str(" write!(out, " write!(out, " out.push_str(" out.push_str(" out.push_str(" Writer<'a> { .into_iter() .filter(|(a, _)| !(*a == "class" || a.starts_with(':'))) { - write!(out, r#" {}=""#, key)?; + write!(out, r#" {key}=""#)?; value.parts().for_each(|part| write_attr(part, out)); out.push('"'); } @@ -338,7 +323,7 @@ impl<'a> Writer<'a> { Alignment::Center => "center", Alignment::Right => "right", }; - write!(out, r#" style="text-align: {};">"#, a)?; + write!(out, r#" style="text-align: {a};">"#)?; } Container::CodeBlock { language } => { if language.is_empty() { @@ -444,7 +429,7 @@ impl<'a> Writer<'a> { } Container::DescriptionList => out.push_str(""), Container::DescriptionDetails => out.push_str(""), - Container::Footnote { .. } => unreachable!(), + Container::Footnote { label } => out.push_str(label), Container::Table => out.push_str(""), Container::TableRow { .. } => out.push_str(""), Container::Section { .. } => {} @@ -455,7 +440,7 @@ impl<'a> Writer<'a> { } out.push_str("

"); } - Container::Heading { level, .. } => write!(out, "", level)?, + Container::Heading { level, .. } => write!(out, "")?, Container::TableCell { head: false, .. } => out.push_str(""), Container::TableCell { head: true, .. } => out.push_str(""), Container::Caption => out.push_str(""), @@ -537,19 +522,8 @@ impl<'a> Writer<'a> { Raw::Html => out.push_str(s), Raw::Other => {} }, - Event::FootnoteReference(_label) => { - self.diagnostics.push(Diagnostic { - severity: Severity::Error, - code: Some("djot".into()), - message: "Djot footnotes are unsupported".into(), - labels: vec![Label { - style: LabelStyle::Primary, - file_id: self.renderer.file_id, - range, - message: "".into(), - }], - notes: vec![], - }); + Event::FootnoteReference(label) => { + out.push_str(label); } Event::Symbol(sym) => { if let Some(vpath) = self.renderer.config.emoji.get(sym.as_ref()) { @@ -624,7 +598,7 @@ impl<'a> Writer<'a> { } out.push_str(" Some("""), _ => None, } - .map_or(false, |s| { + .is_some_and(|s| { ent = s; true }) diff --git a/crates/treehouse/src/html/highlight.rs b/src/html/highlight.rs similarity index 100% rename from crates/treehouse/src/html/highlight.rs rename to src/html/highlight.rs diff --git a/crates/treehouse/src/html/highlight/compiled.rs b/src/html/highlight/compiled.rs similarity index 100% rename from crates/treehouse/src/html/highlight/compiled.rs rename to src/html/highlight/compiled.rs diff --git a/crates/treehouse/src/html/highlight/tokenize.rs b/src/html/highlight/tokenize.rs similarity index 100% rename from crates/treehouse/src/html/highlight/tokenize.rs rename to src/html/highlight/tokenize.rs diff --git a/crates/treehouse/src/html/navmap.rs b/src/html/navmap.rs similarity index 100% rename from crates/treehouse/src/html/navmap.rs rename to src/html/navmap.rs diff --git a/crates/treehouse/src/html/tree.rs b/src/html/tree.rs similarity index 99% rename from crates/treehouse/src/html/tree.rs rename to src/html/tree.rs index 7453bb5..a574e39 100644 --- a/crates/treehouse/src/html/tree.rs +++ b/src/html/tree.rs @@ -1,7 +1,6 @@ use std::fmt::Write; use chrono::{DateTime, Utc}; -use treehouse_format::pull::BranchKind; use crate::{ config::Config, @@ -11,7 +10,9 @@ use crate::{ state::{FileId, Treehouse}, tree::{ attributes::{Content, Stage, Visibility}, - mini_template, SemaBranchId, + mini_template, + pull::BranchKind, + SemaBranchId, }, vfs::{self, VPath, VPathBuf}, }; diff --git a/crates/treehouse/src/import_map.rs b/src/import_map.rs similarity index 100% rename from crates/treehouse/src/import_map.rs rename to src/import_map.rs diff --git a/crates/treehouse/src/lib.rs b/src/lib.rs similarity index 100% rename from crates/treehouse/src/lib.rs rename to src/lib.rs diff --git a/crates/treehouse/src/main.rs b/src/main.rs similarity index 100% rename from crates/treehouse/src/main.rs rename to src/main.rs diff --git a/crates/treehouse/src/parse.rs b/src/parse.rs similarity index 85% rename from crates/treehouse/src/parse.rs rename to src/parse.rs index 56fe2f4..e24bcbe 100644 --- a/crates/treehouse/src/parse.rs +++ b/src/parse.rs @@ -2,9 +2,11 @@ use std::{ops::Range, str::FromStr}; use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle, Severity}; use tracing::instrument; -use treehouse_format::ast::Roots; -use crate::state::{toml_error_to_diagnostic, FileId, TomlError, Treehouse}; +use crate::{ + state::{toml_error_to_diagnostic, FileId, TomlError, Treehouse}, + tree::{self, ast::Roots}, +}; pub struct ErrorsEmitted; @@ -13,7 +15,7 @@ pub fn parse_tree_with_diagnostics( file_id: FileId, input: &str, ) -> Result>> { - Roots::parse(&mut treehouse_format::pull::Parser { input, position: 0 }).map_err(|error| { + Roots::parse(&mut tree::pull::Parser { input, position: 0 }).map_err(|error| { vec![Diagnostic { severity: Severity::Error, code: Some("tree".into()), diff --git a/crates/treehouse/src/paths.rs b/src/paths.rs similarity index 100% rename from crates/treehouse/src/paths.rs rename to src/paths.rs diff --git a/crates/treehouse/src/sources.rs b/src/sources.rs similarity index 85% rename from crates/treehouse/src/sources.rs rename to src/sources.rs index 56feac4..9e0938a 100644 --- a/crates/treehouse/src/sources.rs +++ b/src/sources.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, ops::ControlFlow}; use anyhow::{anyhow, Context}; use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; -use tracing::{info_span, instrument}; +use tracing::{error, info_span, instrument}; use crate::{ config::Config, @@ -66,14 +66,19 @@ fn load_trees(config: &Config, dirs: &Dirs) -> anyhow::Result { let mut parsed_trees = HashMap::new(); let mut paths = vec![]; + let mut doc_paths = vec![]; vfs::walk_dir_rec(&*dirs.content, VPath::ROOT, &mut |path| { - if path.extension() == Some("tree") { - paths.push(path.to_owned()); + match path.extension() { + Some("tree") => paths.push(path.to_owned()), + Some("dj") => doc_paths.push(path.to_owned()), + _ => (), } ControlFlow::Continue(()) }); + // Trees + // NOTE: Sources are filled in later; they can be left out until a call to report_diagnostics. let file_ids: Vec<_> = paths .iter() @@ -132,5 +137,18 @@ fn load_trees(config: &Config, dirs: &Dirs) -> anyhow::Result { report_diagnostics(&treehouse, &diagnostics)?; + // Docs + + for path in doc_paths { + if let Some(input) = + vfs::query::(&dirs.content, &path).and_then(|c| c.string().ok()) + { + let file_id = treehouse.add_file(path.clone(), Source::Other(input)); + treehouse.files_by_doc_path.insert(path, file_id); + } else { + error!("doc {path} does not exist in content directory even though it was enumerated via walk_dir_rec"); + } + } + Ok(treehouse) } diff --git a/crates/treehouse/src/state.rs b/src/state.rs similarity index 97% rename from crates/treehouse/src/state.rs rename to src/state.rs index 9a592b9..ab98670 100644 --- a/crates/treehouse/src/state.rs +++ b/src/state.rs @@ -66,7 +66,8 @@ pub struct FileId(usize); /// Treehouse compilation context. pub struct Treehouse { pub files: Vec, - pub files_by_tree_path: HashMap, + pub files_by_tree_path: HashMap, // trees only + pub files_by_doc_path: HashMap, // docs only pub feeds_by_name: HashMap, pub tree: SemaTree, @@ -83,6 +84,7 @@ impl Treehouse { Self { files: vec![], files_by_tree_path: HashMap::new(), + files_by_doc_path: HashMap::new(), feeds_by_name: HashMap::new(), tree: SemaTree::default(), diff --git a/crates/treehouse/src/tree.rs b/src/tree.rs similarity index 94% rename from crates/treehouse/src/tree.rs rename to src/tree.rs index 6c3bd9d..c5a62e1 100644 --- a/crates/treehouse/src/tree.rs +++ b/src/tree.rs @@ -1,20 +1,23 @@ +pub mod ast; pub mod attributes; +pub mod feed; pub mod mini_template; +pub mod pull; use std::ops::Range; use attributes::Timestamps; use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle, Severity}; use tracing::instrument; -use treehouse_format::{ - ast::{Branch, Roots}, - pull::BranchKind, -}; use crate::{ config::Config, state::{toml_error_to_diagnostic, FileId, Source, TomlError, Treehouse}, - tree::attributes::{Attributes, Content}, + tree::{ + ast::{Branch, Roots}, + attributes::{Attributes, Content}, + pull::BranchKind, + }, }; use self::attributes::RootAttributes; @@ -409,3 +412,31 @@ impl SemaBranch { attributes } } + +#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)] +pub enum ParseErrorKind { + #[error("branch kind (`+` or `-`) expected")] + BranchKindExpected, + + #[error("root branches must not be indented")] + RootIndentLevel, + + #[error("at least {expected} spaces of indentation were expected, but got {got}")] + InconsistentIndentation { got: usize, expected: usize }, + + #[error("unterminated code block")] + UnterminatedCodeBlock, +} + +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[error("{range:?}: {kind}")] +pub struct ParseError { + pub kind: ParseErrorKind, + pub range: Range, +} + +impl ParseErrorKind { + pub fn at(self, range: Range) -> ParseError { + ParseError { kind: self, range } + } +} diff --git a/crates/treehouse-format/src/ast.rs b/src/tree/ast.rs similarity index 99% rename from crates/treehouse-format/src/ast.rs rename to src/tree/ast.rs index e8e4915..5c97879 100644 --- a/crates/treehouse-format/src/ast.rs +++ b/src/tree/ast.rs @@ -1,6 +1,6 @@ use std::ops::Range; -use crate::{ +use super::{ pull::{Attributes, BranchEvent, BranchKind, Parser}, ParseError, ParseErrorKind, }; diff --git a/crates/treehouse/src/tree/attributes.rs b/src/tree/attributes.rs similarity index 97% rename from crates/treehouse/src/tree/attributes.rs rename to src/tree/attributes.rs index 60fed8b..c18fa3e 100644 --- a/crates/treehouse/src/tree/attributes.rs +++ b/src/tree/attributes.rs @@ -23,10 +23,6 @@ pub struct RootAttributes { #[serde(default = "default_icon")] pub icon: String, - /// Summary of the generated .html page. - #[serde(default)] - pub description: Option, - /// ID of picture attached to the page, to be used as a thumbnail. #[serde(default)] pub thumbnail: Option, @@ -50,7 +46,7 @@ pub struct RootAttributes { #[serde(default)] pub timestamps: Option, - /// When specified, this page will have a corresponding Atom feed under `rss/{feed}.xml`. + /// When specified, this page will have a corresponding Atom feed under `feed/{feed}.atom`. /// /// In feeds, top-level branches are expected to have a single heading containing the post title. /// Their children are turned into the post description diff --git a/src/tree/feed.rs b/src/tree/feed.rs new file mode 100644 index 0000000..b8c0d77 --- /dev/null +++ b/src/tree/feed.rs @@ -0,0 +1,94 @@ +use crate::{ + dirs::Dirs, + html::djot::{self, resolve_link}, + sources::Sources, + state::FileId, +}; + +#[derive(Debug, Clone)] +pub struct ParsedEntry { + pub title: Option, + pub link: Option, +} + +pub fn parse_entry( + sources: &Sources, + dirs: &Dirs, + file_id: FileId, + parser: jotdown::Parser, +) -> ParsedEntry { + let mut parser = parser.into_offset_iter(); + while let Some((event, span)) = parser.next() { + if let jotdown::Event::Start(jotdown::Container::Heading { .. }, _attrs) = &event { + let mut events = vec![(event, span)]; + for (event, span) in parser.by_ref() { + // To my knowledge headings cannot nest, so it's okay not keeping a stack here. + let is_heading = matches!( + event, + jotdown::Event::End(jotdown::Container::Heading { .. }) + ); + events.push((event, span)); + if is_heading { + break; + } + } + + let title_events: Vec<_> = events + .iter() + .filter(|(event, _)| { + !matches!( + event, + // A little repetitive, but I don't mind. + // The point of this is not to include extra

and in the link text, + // but preserve other formatting such as bold, italic, code, etc. + jotdown::Event::Start( + jotdown::Container::Link(_, _) | jotdown::Container::Heading { .. }, + _ + ) | jotdown::Event::End( + jotdown::Container::Link(_, _) | jotdown::Container::Heading { .. } + ) + ) + }) + .cloned() + .collect(); + let mut title = String::new(); + let _render_diagnostics = djot::Renderer { + config: &sources.config, + dirs, + treehouse: &sources.treehouse, + file_id, + + // How. Just, stop. + page_id: "liquidex-you-reeeeeal-dummy".into(), + } + .render(&title_events, &mut title); + + let link = events.iter().find_map(|(event, _)| { + if let jotdown::Event::Start(jotdown::Container::Link(link, link_type), _) = event { + Some(link_url(sources, dirs, link, *link_type)) + } else { + None + } + }); + + return ParsedEntry { + title: (!title.is_empty()).then_some(title), + link, + }; + } + } + + ParsedEntry { + title: None, + link: None, + } +} + +fn link_url(sources: &Sources, dirs: &Dirs, url: &str, link_type: jotdown::LinkType) -> String { + if let jotdown::LinkType::Span(jotdown::SpanLinkType::Unresolved) = link_type { + if let Some(url) = resolve_link(&sources.config, &sources.treehouse, dirs, url) { + return url; + } + } + url.to_owned() +} diff --git a/src/tree/lib.rs b/src/tree/lib.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/tree/lib.rs @@ -0,0 +1 @@ + diff --git a/crates/treehouse/src/tree/mini_template.rs b/src/tree/mini_template.rs similarity index 100% rename from crates/treehouse/src/tree/mini_template.rs rename to src/tree/mini_template.rs diff --git a/crates/treehouse-format/src/pull.rs b/src/tree/pull.rs similarity index 99% rename from crates/treehouse-format/src/pull.rs rename to src/tree/pull.rs index 964ca4e..c1e4fd6 100644 --- a/crates/treehouse-format/src/pull.rs +++ b/src/tree/pull.rs @@ -1,6 +1,6 @@ use std::{convert::identity, ops::Range}; -use crate::{ParseError, ParseErrorKind}; +use super::{ParseError, ParseErrorKind}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BranchKind { diff --git a/crates/treehouse/src/vfs.rs b/src/vfs.rs similarity index 98% rename from crates/treehouse/src/vfs.rs rename to src/vfs.rs index 7d9c6b4..772592e 100644 --- a/crates/treehouse/src/vfs.rs +++ b/src/vfs.rs @@ -168,6 +168,12 @@ impl<'a> dyn Erased<'a> + 'a { } } +impl Dir for () { + fn query(&self, _path: &VPath, _query: &mut Query) { + // Noop implementation. + } +} + impl Dir for &T where T: Dir, diff --git a/crates/treehouse/src/vfs/anchored.rs b/src/vfs/anchored.rs similarity index 100% rename from crates/treehouse/src/vfs/anchored.rs rename to src/vfs/anchored.rs diff --git a/crates/treehouse/src/vfs/asynch.rs b/src/vfs/asynch.rs similarity index 100% rename from crates/treehouse/src/vfs/asynch.rs rename to src/vfs/asynch.rs diff --git a/crates/treehouse/src/vfs/cd.rs b/src/vfs/cd.rs similarity index 100% rename from crates/treehouse/src/vfs/cd.rs rename to src/vfs/cd.rs diff --git a/crates/treehouse/src/vfs/content_cache.rs b/src/vfs/content_cache.rs similarity index 100% rename from crates/treehouse/src/vfs/content_cache.rs rename to src/vfs/content_cache.rs diff --git a/crates/treehouse/src/vfs/content_version_cache.rs b/src/vfs/content_version_cache.rs similarity index 100% rename from crates/treehouse/src/vfs/content_version_cache.rs rename to src/vfs/content_version_cache.rs diff --git a/crates/treehouse/src/vfs/edit.rs b/src/vfs/edit.rs similarity index 100% rename from crates/treehouse/src/vfs/edit.rs rename to src/vfs/edit.rs diff --git a/crates/treehouse/src/vfs/file.rs b/src/vfs/file.rs similarity index 100% rename from crates/treehouse/src/vfs/file.rs rename to src/vfs/file.rs diff --git a/crates/treehouse/src/vfs/html_canonicalize.rs b/src/vfs/html_canonicalize.rs similarity index 87% rename from crates/treehouse/src/vfs/html_canonicalize.rs rename to src/vfs/html_canonicalize.rs index fb3ee54..bd3e772 100644 --- a/crates/treehouse/src/vfs/html_canonicalize.rs +++ b/src/vfs/html_canonicalize.rs @@ -2,6 +2,7 @@ use core::fmt; use super::{Dir, Query, VPath}; +/// This Dir exists to serve as a compatibility layer for very old links that end with .html. pub struct HtmlCanonicalize { inner: T, } diff --git a/crates/treehouse/src/vfs/image_size_cache.rs b/src/vfs/image_size_cache.rs similarity index 100% rename from crates/treehouse/src/vfs/image_size_cache.rs rename to src/vfs/image_size_cache.rs diff --git a/crates/treehouse/src/vfs/mem_dir.rs b/src/vfs/mem_dir.rs similarity index 100% rename from crates/treehouse/src/vfs/mem_dir.rs rename to src/vfs/mem_dir.rs diff --git a/crates/treehouse/src/vfs/overlay.rs b/src/vfs/overlay.rs similarity index 64% rename from crates/treehouse/src/vfs/overlay.rs rename to src/vfs/overlay.rs index 13907ed..470a076 100644 --- a/crates/treehouse/src/vfs/overlay.rs +++ b/src/vfs/overlay.rs @@ -2,6 +2,8 @@ use std::fmt; use tracing::instrument; +use crate::vfs::ToDynDir; + use super::{entries, Dir, DynDir, Entries, Query, VPath, VPathBuf}; pub struct Overlay { @@ -38,3 +40,18 @@ impl fmt::Debug for Overlay { write!(f, "Overlay({:?}, {:?})", self.base, self.overlay) } } + +pub fn layered_dir(layers: &[DynDir]) -> DynDir { + match layers { + [] => ().to_dyn(), + [dir] => dir.clone(), + [left, right] => Overlay::new(left.clone(), right.clone()).to_dyn(), + [left, right, rest @ ..] => { + let mut overlay = Overlay::new(left.clone(), right.clone()); + for dir in rest { + overlay = Overlay::new(overlay.to_dyn(), dir.clone()); + } + overlay.to_dyn() + } + } +} diff --git a/crates/treehouse/src/vfs/path.rs b/src/vfs/path.rs similarity index 100% rename from crates/treehouse/src/vfs/path.rs rename to src/vfs/path.rs diff --git a/crates/treehouse/src/vfs/physical.rs b/src/vfs/physical.rs similarity index 100% rename from crates/treehouse/src/vfs/physical.rs rename to src/vfs/physical.rs diff --git a/static/character/riki/sitting.png b/static/character/riki/sitting.png new file mode 100644 index 0000000..316229e Binary files /dev/null and b/static/character/riki/sitting.png differ diff --git a/static/css/base.css b/static/css/base.css index 0ef71d0..7f32d49 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -1,4 +1,4 @@ -/* Color scheme. */ +/* Color scheme */ :root { --accent-red: #fb4c9e; @@ -36,6 +36,18 @@ } } +/* Animations */ + +:root { + --transition-duration: 0.15s; +} + +@media (prefers-reduced-motion: reduce) { + :root { + --transition-duration: 0; + } +} + /* Reset things to more sensible sizing rules */ * { diff --git a/static/css/doc.css b/static/css/doc.css new file mode 100644 index 0000000..2832972 --- /dev/null +++ b/static/css/doc.css @@ -0,0 +1,84 @@ +main.doc { + --doc-text-width: 80ch; + + display: flex; + flex-direction: row; + align-items: start; + + & .vertical-center { + min-height: 100vh; + flex-grow: 1; + + display: flex; + flex-direction: column; + + align-items: center; + justify-content: center; + } + + & .doc-text { + padding: 1.6rem; + max-width: var(--doc-text-width); + + line-height: 1.6; + + /* I was thinking a bunch about whether documents should be justified, and it honestly + causes more awkwardness than it solves. Web pages aren't quite books, unfortunately. + + A cool feature that would help is text-wrap: pretty; but only Safari implements a nice + text layout algorithm for it. (Chrome prevents short last lines, Firefox doesn't + implement it at all) */ + + /* text-align: justify; + hyphens: auto; */ + + & p { + padding-top: 0.5lh; + padding-bottom: 0.5lh; + } + + & h2 { + margin: 0; + padding-top: 1lh; + padding-bottom: 0.5lh; + } + + & ul, + & ol { + /* Is there a better way to add spacing to the marker, other than adding whitespace? */ + list-style: "– "; + margin-top: 0; + margin-bottom: 0; + padding-bottom: 0.5lh; + } + } + + & section.feed { + max-width: 40ch; + padding: 0.8rem; + padding-top: 3.2rem; + } +} + +@media (max-width: 1500px) { + main.doc { + flex-direction: column; + align-items: center; + + & .vertical-center { + min-height: 0; + } + + & footer { + padding: 0.8rem; + } + + & section.feed { + max-width: var(--doc-text-width); + + margin-top: 2.4em; + padding: 1.6rem; + border-top: 1px solid var(--border-1); + } + } +} diff --git a/static/css/main.css b/static/css/main.css index 79a0d53..df1456d 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -1,73 +1,46 @@ -/* Lay out the main containers. */ +/* Main layout */ body { - --top-min-spacing: 40px; + --main-min-size: 100vh; margin: 0; display: grid; grid-template-columns: - [left] minmax( - 0, - clamp(136px, calc(100vw - (1920px - 360px - 160px)), 160px) - ) - [center] minmax(0, auto) - [right] minmax(0, calc(100vw - (1920px - 360px))); + [left] 1fr + [right] auto; grid-template-rows: - [top] minmax( - clamp( - var(--top-min-spacing), - calc(100vw - (1920px - 360px - 160px)), - 128px - ), - min-content - ) - [title] minmax(9.6rem, min-content) - [main] 1fr - [bottom] min-content; + [nav] auto + [main] minmax(var(--main-min-size), auto) + [virtual] 100vh; } html { - /* Try to always leave a bunch of empty space at the bottom, but don't overdo it. - It's kind of awkward when you scroll to the bottom and your page just turns blank. */ - --virtual-space-ratio: 1.75; - - height: calc(100% * var(--virtual-space-ratio)); - /* Leave a bunch of space at the top when scrolling to elements. I'm honestly not sure why this is needed on and not the scrolled-to element... */ scroll-padding-top: 10vh; } -body { - min-height: calc(100% / var(--virtual-space-ratio)); +.sidebar-sticky { + grid-column: right; + grid-row: main; } -.noscript { - grid-row: top; - grid-column: center; -} +aside.sidebar { + position: sticky; + top: 0px; -#nav-logo { - grid-row: title; - grid-column: left; + max-width: 50rem; + height: 100vh; + padding: 0.8rem; - align-self: center; - justify-self: end; -} - -section.page-header { - grid-row: title; - grid-column: center; - - align-self: center; + display: flex; } main { + grid-column: left; grid-row: main; - grid-column: center / center; - - margin-right: 0.8rem; + min-width: 0; } footer { @@ -75,27 +48,25 @@ footer { grid-column: center / center; } -@media (max-width: 1200px) { - main { - grid-column: left / -1; - } +/* Narrower layout: sidebar is pushed to the top */ - footer { - grid-column: 1 / -1; - } -} - -@media (max-width: 450px) { +@media (max-width: 1280px) { body { - --top-min-spacing: 0px; + --main-min-size: 0; } - section.page-header { - grid-column: 1 / -1; + .sidebar-sticky { + grid-column: left; + grid-row: nav; + + display: flex; + justify-content: center; } - nav#nav-logo { - display: none; + aside.sidebar { + position: relative; + height: auto; + padding: 0; } } @@ -119,16 +90,6 @@ body { /* Set up typography */ -@font-face { - font-family: "RecVar"; - /* NOTE: I put the hash in here manually instead of adding the complexity of piping CSS through - Handlebars because I don't really think it's worth it for this single asset. - Other assets are referenced rarely enough that caching probably isn't gonna make too much of - an impact. - It's unlikely I'll ever update the font anyways, so eh, whatever. */ - src: url("../font/Recursive_VF_1.085.woff2?v=b3-41236e2f"); -} - body, pre, code, @@ -147,7 +108,7 @@ html { } body { - font-size: 1.4rem; + font-size: 1.6rem; } pre, @@ -183,15 +144,16 @@ input { "slnt" var(--recursive-slnt), "CRSV" var(--recursive-crsv); - font-feature-settings: var(--recursive-simplified-f), - var(--recursive-simplified-g), var(--recursive-simplified-l), - var(--recursive-simplified-r), var(--recursive-no-serif-L-Z); + font-feature-settings: + var(--recursive-simplified-f), var(--recursive-simplified-g), + var(--recursive-simplified-l), var(--recursive-simplified-r), + var(--recursive-no-serif-L-Z); } h1 { --recursive-wght: 900; - font-size: 5.6rem; + font-size: 4.8rem; font-feature-settings: var(--recursive-simplified-r) 0; } @@ -245,6 +207,12 @@ h6 { text-wrap: balance; } +/* Other classes for controlling typography */ + +.nowrap { + white-space: nowrap; +} + /* Lay out elements a bit more compactly */ p, @@ -319,21 +287,21 @@ th-literate-program { overflow: auto; } -/* Also don't let images get out of hand */ +/* Images */ img { + /* Prevent images from causing horizontal scrolling */ max-width: 100%; + height: auto; } -/* Also regarding images - make them look a bit more pretty by default */ - img.pic { border-radius: 0.6rem; margin: 0.8rem 0; } -/* Image hints for tweaking rendering */ img { + /* Hints for tweaking rendering */ &[src*="+pixel"] { image-rendering: pixelated; border-radius: 0; @@ -377,8 +345,6 @@ a:visited { color: var(--link-color-visited); } -/* Allow for some secret links */ - a.secret { color: var(--text-color); text-decoration: none; @@ -459,32 +425,224 @@ hr { color: #6c2380; } -/* Navigation button */ +/* Feeds */ -#nav-logo { - width: min-content; - height: min-content; +section.feed { + display: flex; + flex-direction: column; + + /* Titles */ + + & a, + & a:visited { + color: var(--text-color); + } + + & a:visited { + color: color-mix( + in srgb, + var(--background-color), + var(--text-color) 60% + ); + } + + & h1 { + --recursive-wght: 800; + font-size: 125%; + padding-top: 1.2rem; + padding-bottom: 1.2rem; + } + + & h2 { + --recursive-wght: 600; + font-size: 100%; + padding: 0; + } + + /* Articles */ + + & article { + display: flex; + flex-direction: column; + + padding-bottom: 1.2rem; + + line-height: 1.4; + + & .info { + display: flex; + flex-direction: row; + flex-wrap: wrap; + font-size: 87.5%; + + & > *:not(:first-child)::before { + content: "·"; + padding: 0 0.4rem; + } + } + + & .categories { + display: flex; + flex-direction: row; + flex-wrap: wrap; + + list-style: none; + margin: 0; + padding: 0; + + & > *::before { + content: "#"; + } + + & > *:not(:first-child)::before { + padding-left: 0.4rem; + } + } + } } -#nav-logo .logo { - /* NOTE: Measurements in px for pixel perfection */ - width: 120px; - height: 120px; +/* Page sidebar */ - display: block; - opacity: 100%; - color: var(--text-color); +aside.sidebar { + overflow: clip; + + & > a { + display: block; + height: min-content; + margin-top: auto; + } +} + +header.floof { + margin-top: auto; + + position: relative; + + & > img { + display: block; + min-width: 0; + object-fit: cover; + object-position: 33% 0; + } + + & > h1 { + position: absolute; + top: 3rem; + left: 3rem; + + display: flex; + flex-direction: column; + line-height: 1; + width: min-content; + + --recursive-wght: 900; + font-size: 5.6rem; + text-align: right; + + transform: skew(-5deg, -5deg); + + & .rikis { + width: max-content; + background-color: var(--text-color); + color: var(--background-color); + padding: 0.1em; + + --shadow-color: var(--accent-pink); + box-shadow: + 0.5px 0.5px 0 var(--shadow-color), + 1px 1px 0 var(--shadow-color), + 1.5px 1.5px 0 var(--shadow-color), + 2px 2px 0 var(--shadow-color), + 2.5px 2.5px 0 var(--shadow-color), + 3px 3px 0 var(--shadow-color), + 3.5px 3.5px 0 var(--shadow-color), + 4px 4px 0 var(--shadow-color); + + /* + import math + + print("box-shadow:") + x = 0 + max_x = 16 + while x < max_x: + print(f"{x}px {x}px {math.pow(x / max_x, 2) * 16}px rgba(from var(--shadow-color) r g b / {math.pow(1 - x / max_x, 3)}),") + x += 0.5 + */ + /* prettier-ignore */ + box-shadow: + 0px 0px 0.0px rgba(from var(--shadow-color) r g b / 1.0), + 0.5px 0.5px 0.015625px rgba(from var(--shadow-color) r g b / 0.909149169921875), + 1.0px 1.0px 0.0625px rgba(from var(--shadow-color) r g b / 0.823974609375), + 1.5px 1.5px 0.140625px rgba(from var(--shadow-color) r g b / 0.744293212890625), + 2.0px 2.0px 0.25px rgba(from var(--shadow-color) r g b / 0.669921875), + 2.5px 2.5px 0.390625px rgba(from var(--shadow-color) r g b / 0.600677490234375), + 3.0px 3.0px 0.5625px rgba(from var(--shadow-color) r g b / 0.536376953125), + 3.5px 3.5px 0.765625px rgba(from var(--shadow-color) r g b / 0.476837158203125), + 4.0px 4.0px 1.0px rgba(from var(--shadow-color) r g b / 0.421875), + 4.5px 4.5px 1.265625px rgba(from var(--shadow-color) r g b / 0.371307373046875), + 5.0px 5.0px 1.5625px rgba(from var(--shadow-color) r g b / 0.324951171875), + 5.5px 5.5px 1.890625px rgba(from var(--shadow-color) r g b / 0.282623291015625), + 6.0px 6.0px 2.25px rgba(from var(--shadow-color) r g b / 0.244140625), + 6.5px 6.5px 2.640625px rgba(from var(--shadow-color) r g b / 0.209320068359375), + 7.0px 7.0px 3.0625px rgba(from var(--shadow-color) r g b / 0.177978515625), + 7.5px 7.5px 3.515625px rgba(from var(--shadow-color) r g b / 0.149932861328125), + 8.0px 8.0px 4.0px rgba(from var(--shadow-color) r g b / 0.125), + 8.5px 8.5px 4.515625px rgba(from var(--shadow-color) r g b / 0.102996826171875), + 9.0px 9.0px 5.0625px rgba(from var(--shadow-color) r g b / 0.083740234375), + 9.5px 9.5px 5.640625px rgba(from var(--shadow-color) r g b / 0.067047119140625), + 10.0px 10.0px 6.25px rgba(from var(--shadow-color) r g b / 0.052734375), + 10.5px 10.5px 6.890625px rgba(from var(--shadow-color) r g b / 0.040618896484375), + 11.0px 11.0px 7.5625px rgba(from var(--shadow-color) r g b / 0.030517578125), + 11.5px 11.5px 8.265625px rgba(from var(--shadow-color) r g b / 0.022247314453125), + 12.0px 12.0px 9.0px rgba(from var(--shadow-color) r g b / 0.015625), + 12.5px 12.5px 9.765625px rgba(from var(--shadow-color) r g b / 0.010467529296875), + 13.0px 13.0px 10.5625px rgba(from var(--shadow-color) r g b / 0.006591796875), + 13.5px 13.5px 11.390625px rgba(from var(--shadow-color) r g b / 0.003814697265625), + 14.0px 14.0px 12.25px rgba(from var(--shadow-color) r g b / 0.001953125), + 14.5px 14.5px 13.140625px rgba(from var(--shadow-color) r g b / 0.000823974609375), + 15.0px 15.0px 14.0625px rgba(from var(--shadow-color) r g b / 0.000244140625), + 15.5px 15.5px 15.015625px rgba(from var(--shadow-color) r g b / 3.0517578125e-05) + ; + } + + & .fluffy-little-house { + display: flex; + flex-direction: column; + + background-color: var(--background-color); + width: min-content; + align-self: end; + + padding-left: 0.8rem; + padding-right: 0.8rem; + padding-top: 0.4rem; + + z-index: -1; + + color: var(--text-color); + + & .adjectives { + --recursive-wght: 800; + font-size: 1.6rem; + padding-top: 0.6rem; + } + + & .house { + margin-top: -0.2em; + font-size: 3.6rem; + } + } + } } /* Navigation header (contains page title & breadcrumbs) */ h1.page-title { - --recursive-wght: 850; + --recursive-wght: 900; - margin-top: 0.32rem; - margin-bottom: 0.32rem; - margin-left: 3.6rem; - font-size: 4rem; + line-height: 1.2; + padding-top: 3lh; + padding-bottom: 0.5lh; & a { color: var(--text-color); @@ -505,7 +663,18 @@ h1.page-title { } } -/* Style badges */ +@media (max-width: 1280px) { + h1.page-title { + padding-top: 0.25lh; + } +} + +@media (max-width: 700px) { + h1.page-title { + font-size: 4rem; + } +} + span.badge { --recursive-wght: 800; --recursive-mono: 1; @@ -532,11 +701,9 @@ span.badge { /* Style the footer */ footer { - padding-left: 1.6rem; - padding-right: 1.6rem; - - margin-top: 6.4rem; - padding-bottom: 6.4rem; + width: 100%; + max-width: 90ch; + padding: 1.6rem 0.8rem; display: flex; flex-direction: row; @@ -609,9 +776,9 @@ dialog[open] { /* Style emojis to be readable */ img[data-cast~="emoji"] { - max-width: 1.5em; - max-height: 1.5em; - vertical-align: bottom; + max-width: 1.3125em; + max-height: 1.3125em; + vertical-align: text-bottom; object-fit: contain; } diff --git a/static/css/page/index.css b/static/css/page/index.css index ac95a5a..a6efd39 100644 --- a/static/css/page/index.css +++ b/static/css/page/index.css @@ -1,98 +1,3 @@ -h1.page-title { - --recursive-wght: 900; - font-size: 5.6rem; - - display: flex; - flex-direction: column; - line-height: 1; - width: min-content; - - transform: skew(-5deg, -5deg); - - & .rikis { - width: max-content; - background-color: var(--text-color); - color: var(--background-color); - padding: 0.1em; - - --shadow-color: var(--accent-pink); - box-shadow: - 0.5px 0.5px 0 var(--shadow-color), - 1px 1px 0 var(--shadow-color), - 1.5px 1.5px 0 var(--shadow-color), - 2px 2px 0 var(--shadow-color), - 2.5px 2.5px 0 var(--shadow-color), - 3px 3px 0 var(--shadow-color), - 3.5px 3.5px 0 var(--shadow-color), - 4px 4px 0 var(--shadow-color); - - /* - import math - - print("box-shadow:") - x = 0 - max_x = 16 - while x < max_x: - print(f"{x}px {x}px {math.pow(x / max_x, 2) * 16}px rgba(from var(--shadow-color) r g b / {math.pow(1 - x / max_x, 3)}),") - x += 0.5 - */ - /* prettier-ignore */ - box-shadow: - 0px 0px 0.0px rgba(from var(--shadow-color) r g b / 1.0), - 0.5px 0.5px 0.015625px rgba(from var(--shadow-color) r g b / 0.909149169921875), - 1.0px 1.0px 0.0625px rgba(from var(--shadow-color) r g b / 0.823974609375), - 1.5px 1.5px 0.140625px rgba(from var(--shadow-color) r g b / 0.744293212890625), - 2.0px 2.0px 0.25px rgba(from var(--shadow-color) r g b / 0.669921875), - 2.5px 2.5px 0.390625px rgba(from var(--shadow-color) r g b / 0.600677490234375), - 3.0px 3.0px 0.5625px rgba(from var(--shadow-color) r g b / 0.536376953125), - 3.5px 3.5px 0.765625px rgba(from var(--shadow-color) r g b / 0.476837158203125), - 4.0px 4.0px 1.0px rgba(from var(--shadow-color) r g b / 0.421875), - 4.5px 4.5px 1.265625px rgba(from var(--shadow-color) r g b / 0.371307373046875), - 5.0px 5.0px 1.5625px rgba(from var(--shadow-color) r g b / 0.324951171875), - 5.5px 5.5px 1.890625px rgba(from var(--shadow-color) r g b / 0.282623291015625), - 6.0px 6.0px 2.25px rgba(from var(--shadow-color) r g b / 0.244140625), - 6.5px 6.5px 2.640625px rgba(from var(--shadow-color) r g b / 0.209320068359375), - 7.0px 7.0px 3.0625px rgba(from var(--shadow-color) r g b / 0.177978515625), - 7.5px 7.5px 3.515625px rgba(from var(--shadow-color) r g b / 0.149932861328125), - 8.0px 8.0px 4.0px rgba(from var(--shadow-color) r g b / 0.125), - 8.5px 8.5px 4.515625px rgba(from var(--shadow-color) r g b / 0.102996826171875), - 9.0px 9.0px 5.0625px rgba(from var(--shadow-color) r g b / 0.083740234375), - 9.5px 9.5px 5.640625px rgba(from var(--shadow-color) r g b / 0.067047119140625), - 10.0px 10.0px 6.25px rgba(from var(--shadow-color) r g b / 0.052734375), - 10.5px 10.5px 6.890625px rgba(from var(--shadow-color) r g b / 0.040618896484375), - 11.0px 11.0px 7.5625px rgba(from var(--shadow-color) r g b / 0.030517578125), - 11.5px 11.5px 8.265625px rgba(from var(--shadow-color) r g b / 0.022247314453125), - 12.0px 12.0px 9.0px rgba(from var(--shadow-color) r g b / 0.015625), - 12.5px 12.5px 9.765625px rgba(from var(--shadow-color) r g b / 0.010467529296875), - 13.0px 13.0px 10.5625px rgba(from var(--shadow-color) r g b / 0.006591796875), - 13.5px 13.5px 11.390625px rgba(from var(--shadow-color) r g b / 0.003814697265625), - 14.0px 14.0px 12.25px rgba(from var(--shadow-color) r g b / 0.001953125), - 14.5px 14.5px 13.140625px rgba(from var(--shadow-color) r g b / 0.000823974609375), - 15.0px 15.0px 14.0625px rgba(from var(--shadow-color) r g b / 0.000244140625), - 15.5px 15.5px 15.015625px rgba(from var(--shadow-color) r g b / 3.0517578125e-05) - ; - } - - & .adjectives { - --recursive-wght: 800; - font-size: 2rem; - vertical-align: 50%; - } - - & .house { - width: max-content; - font-size: 4rem; - padding-left: 1em; - padding-top: 0.1em; - } -} - -@media (hover: none) { - h1.page-title a { - text-decoration: none; - } -} - @media (max-width: 450px) { body { --top-min-spacing: 40px; diff --git a/static/css/tree.css b/static/css/tree.css index 6aaed5e..ae73ba2 100644 --- a/static/css/tree.css +++ b/static/css/tree.css @@ -2,7 +2,6 @@ :root { --tree-indent-width: 3.2rem; - --transition-duration: 0.15s; --button-bar-icon-size: 2.8rem; } @@ -82,6 +81,21 @@ .tree { --tree-indent-guide-dim: transparent; --tree-indent-guide-highlighted: transparent; + + display: flex; + flex-direction: column; + + align-self: start; + align-items: center; + + & > article { + padding: 1.6rem; + width: 100%; + } + + & > footer { + max-width: none; + } } .tree:has(.branch-container:hover) { @@ -116,8 +130,9 @@ } /* Top level should not have an indent or a border. */ -.tree > ul { +.tree article > ul { padding-left: 0; + margin-left: 0; border-left: none; } diff --git a/template/_doc.hbs b/template/_doc.hbs new file mode 100644 index 0000000..2c68496 --- /dev/null +++ b/template/_doc.hbs @@ -0,0 +1,33 @@ + + + + + + {{> components/_head.hbs }} + + + + {{#each page.styles}} + + {{/each}} + + + + + + {{> components/_sidebar.hbs }} + + {{~> components/_doc.hbs }} + + + + + + + +{{~> components/_jar.hbs }} diff --git a/template/_tree.hbs b/template/_tree.hbs index f395225..233f511 100644 --- a/template/_tree.hbs +++ b/template/_tree.hbs @@ -4,31 +4,30 @@ {{> components/_head.hbs }} + + + + {{#each page.styles}} + + {{/each}} + + - - - {{> components/_noscript.hbs }} + {{> components/_sidebar.hbs }} - {{> components/_nav.hbs }} - {{> components/_header.hbs }} - - {{!-- - NOTE: ~ because components/_tree.hbs must not include any extra indentation, because it may - contain pre elements which shouldn't be indented. - --}} {{~> components/_tree.hbs }} - {{!-- For all pages except the one linked from the footer, include the footer icon. --}} - {{#if (ne page.tree_path "treehouse")}} - {{> components/_footer.hbs }} - {{/if}} - + +{{~> components/_jar.hbs }} diff --git a/template/components/_doc.hbs b/template/components/_doc.hbs new file mode 100644 index 0000000..0afa630 --- /dev/null +++ b/template/components/_doc.hbs @@ -0,0 +1,17 @@ +
+
+ + + {{> components/_footer.hbs }} +
+ + {{> components/_feed.hbs }} +
diff --git a/template/components/_feed.hbs b/template/components/_feed.hbs new file mode 100644 index 0000000..780ef75 --- /dev/null +++ b/template/components/_feed.hbs @@ -0,0 +1,20 @@ +{{#if page.feed}} +
+

{{ page.feed.title }}

+ + {{#each page.feed.entries}} +
+

{{{ title }}}

+
+ +
    + {{#each categories as |category|}} +
  • {{ category }}
  • + {{/each}} +
+
+
+ {{/each}} +
+{{/if}} + diff --git a/template/components/_footer.hbs b/template/components/_footer.hbs index e216374..064b07e 100644 --- a/template/components/_footer.hbs +++ b/template/components/_footer.hbs @@ -9,6 +9,8 @@ + {{!-- For all pages except the one linked from the footer, include the footer icon. --}} + {{#if (ne page.tree_path "treehouse")}}
@@ -58,4 +60,5 @@
+ {{/if}} diff --git a/template/components/_head.hbs b/template/components/_head.hbs index cf19b2e..bbed9c8 100644 --- a/template/components/_head.hbs +++ b/template/components/_head.hbs @@ -4,12 +4,15 @@ - + - {{!-- Import maps currently don't support the src="" attribute. Unless we come up with something @@ -38,14 +41,14 @@ clever to do while browser vendors figure that out, we'll just have to do a cach }; diff --git a/template/components/_jar.hbs b/template/components/_jar.hbs new file mode 100644 index 0000000..48941e1 --- /dev/null +++ b/template/components/_jar.hbs @@ -0,0 +1,16 @@ + + diff --git a/template/components/_nav.hbs b/template/components/_nav.hbs deleted file mode 100644 index 7ce797d..0000000 --- a/template/components/_nav.hbs +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/template/components/_sidebar.hbs b/template/components/_sidebar.hbs new file mode 100644 index 0000000..ae1dff5 --- /dev/null +++ b/template/components/_sidebar.hbs @@ -0,0 +1,17 @@ + + diff --git a/template/components/_tree.hbs b/template/components/_tree.hbs index cf3379d..c9368f1 100644 --- a/template/components/_tree.hbs +++ b/template/components/_tree.hbs @@ -1,18 +1,13 @@
- {{!-- Append page styles and scripts into the main content, such that they can be inlined - into linked branches when those are loaded in. Putting them in the page's head would make - extracting them way more painful than it needs to be. --}} +
+ {{#if (ne page.tree_path 'index')}} +
+

{{ page.title }}

+
+ {{/if}} - {{#each page.styles}} - - {{/each}} + {{{~ page.tree }}} +
- - - {{{ page.tree }}} + {{> components/_footer.hbs }}
diff --git a/crates/treehouse/tests/it/main.rs b/tests/it/main.rs similarity index 100% rename from crates/treehouse/tests/it/main.rs rename to tests/it/main.rs diff --git a/crates/treehouse/tests/it/vfs.rs b/tests/it/vfs.rs similarity index 100% rename from crates/treehouse/tests/it/vfs.rs rename to tests/it/vfs.rs diff --git a/crates/treehouse/tests/it/vfs/cd.rs b/tests/it/vfs/cd.rs similarity index 100% rename from crates/treehouse/tests/it/vfs/cd.rs rename to tests/it/vfs/cd.rs diff --git a/crates/treehouse/tests/it/vfs/file.rs b/tests/it/vfs/file.rs similarity index 100% rename from crates/treehouse/tests/it/vfs/file.rs rename to tests/it/vfs/file.rs diff --git a/crates/treehouse/tests/it/vfs/mount_points.rs b/tests/it/vfs/mount_points.rs similarity index 100% rename from crates/treehouse/tests/it/vfs/mount_points.rs rename to tests/it/vfs/mount_points.rs diff --git a/crates/treehouse/tests/it/vfs/physical.rs b/tests/it/vfs/physical.rs similarity index 100% rename from crates/treehouse/tests/it/vfs/physical.rs rename to tests/it/vfs/physical.rs diff --git a/crates/treehouse/tests/it/vfs_physical/test.txt b/tests/it/vfs_physical/test.txt similarity index 100% rename from crates/treehouse/tests/it/vfs_physical/test.txt rename to tests/it/vfs_physical/test.txt diff --git a/treehouse.toml b/treehouse.toml index 34ec8f3..8881c23 100644 --- a/treehouse.toml +++ b/treehouse.toml @@ -12,7 +12,7 @@ commit_base_url = "https://src.liquidev.net/liquidex/treehouse/src/commit" [user] title = "riki's house" author = "riki" -description = "a fluffy ragdoll's fluffy house = —w— =" +description = "a pink ragdoll's fluffy house = —w— =" canonical_url = "https://riki.house" # URI prefix to use for entry IDs in feeds. @@ -26,6 +26,7 @@ feed_id_prefix = "https://liquidex.house" "social/github" = "https://github.com/liquidev" "social/soundcloud" = "https://soundcloud.com/daknus" "social/listenbrainz" = "https://listenbrainz.org/user/liquidev/" +"social/bandcamp" = "https://bandcamp.com/rikimoe" # treehouse management facilities "treehouse/issues" = "https://src.liquidev.net/riki/treehouse/issues"