Compare commits
4 commits
0713b59063
...
9221cc159f
Author | SHA1 | Date | |
---|---|---|---|
liquidex | 9221cc159f | ||
liquidex | 1e3a1f3527 | ||
liquidex | 2e14197fd1 | ||
liquidex | fd40f99810 |
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -366,6 +366,7 @@ dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
|
@ -4,4 +4,4 @@
|
||||||
- seems like the page you're looking for isn't here.
|
- seems like the page you're looking for isn't here.
|
||||||
|
|
||||||
% id = "01HMF8KQ99XNMEP67NE3QH5698"
|
% id = "01HMF8KQ99XNMEP67NE3QH5698"
|
||||||
- care to go [back to the index][branch:treehouse]?
|
- care to go [back to the index][page:index]?
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
%% title = "a curated feed of updates to the house"
|
%% title = "a curated feed of updates to the house"
|
||||||
styles = ["new.css"]
|
styles = ["new.css"]
|
||||||
|
feed = "new"
|
||||||
|
|
||||||
% id = "01JCGWPM6T73PAC5Q8YHPBEAA1"
|
% id = "01JCGWPM6T73PAC5Q8YHPBEAA1"
|
||||||
+ hello!
|
+ hello!
|
||||||
|
@ -11,10 +12,8 @@ if you've been wondering what I've been up to, you've come to the right place.
|
||||||
% id = "01JCGWPM6TGQ17JPSJW8G58SB0"
|
% id = "01JCGWPM6TGQ17JPSJW8G58SB0"
|
||||||
- you can keep track of which posts you've read by looking at the color of the links.
|
- you can keep track of which posts you've read by looking at the color of the links.
|
||||||
|
|
||||||
% id = "01JCGWPM6TMAJT0B50GQSA4BDW"
|
|
||||||
- there is currently no RSS or Atom feed for this page, sorry!
|
|
||||||
|
|
||||||
% id = "01JDJJSEWASRWJGKMBNYMFD9B5"
|
% id = "01JDJJSEWASRWJGKMBNYMFD9B5"
|
||||||
|
tags = ["programming", "treehouse"]
|
||||||
- ### [composable virtual file systems][page:programming/blog/vfs]
|
- ### [composable virtual file systems][page:programming/blog/vfs]
|
||||||
|
|
||||||
% id = "01JDJJSEWAVZGJN3PWY94SJMXT"
|
% id = "01JDJJSEWAVZGJN3PWY94SJMXT"
|
||||||
|
@ -24,15 +23,18 @@ if you've been wondering what I've been up to, you've come to the right place.
|
||||||
- this is an exploration of how I built my abstraction, how it works, and what I learned from it.
|
- this is an exploration of how I built my abstraction, how it works, and what I learned from it.
|
||||||
|
|
||||||
% id = "01JCGAM553TJJCEJ96ADEWETQC"
|
% id = "01JCGAM553TJJCEJ96ADEWETQC"
|
||||||
|
tags = ["programming", "c", "cxx"]
|
||||||
- ### [prefix matches with C strings][page:programming/blog/cstring-starts-with]
|
- ### [prefix matches with C strings][page:programming/blog/cstring-starts-with]
|
||||||
|
|
||||||
% id = "01JBAGZAZ30K443QYPK0XBNZWM"
|
% id = "01JBAGZAZ30K443QYPK0XBNZWM"
|
||||||
|
tags = ["music"]
|
||||||
- ### [the curious case of Amon Tobin's Creatures][page:music/creatures]
|
- ### [the curious case of Amon Tobin's Creatures][page:music/creatures]
|
||||||
|
|
||||||
% id = "01JBAGZAZ3NKBED4M9FANR5RPZ"
|
% id = "01JBAGZAZ3NKBED4M9FANR5RPZ"
|
||||||
- a weird anomaly I noticed while listening to some breaks
|
- a weird anomaly I noticed while listening to some breaks
|
||||||
|
|
||||||
% id = "01J8ZP2EG9TM8320R9E3K1GQEC"
|
% id = "01J8ZP2EG9TM8320R9E3K1GQEC"
|
||||||
|
tags = ["music"]
|
||||||
- ### [I Don't Love Me Anymore][page:music/reviews/opn/i-dont-love-me-anymore]
|
- ### [I Don't Love Me Anymore][page:music/reviews/opn/i-dont-love-me-anymore]
|
||||||
|
|
||||||
% id = "01J8ZP2EG96VQ2ZK0XYK0FK1NR"
|
% id = "01J8ZP2EG96VQ2ZK0XYK0FK1NR"
|
||||||
|
@ -42,6 +44,7 @@ if you've been wondering what I've been up to, you've come to the right place.
|
||||||
- it's also a nice opportunity to say that I've refreshed the music section a bit!
|
- it's also a nice opportunity to say that I've refreshed the music section a bit!
|
||||||
|
|
||||||
% id = "01J7C1KBZ58BR21AVFA1PMWV68"
|
% id = "01J7C1KBZ58BR21AVFA1PMWV68"
|
||||||
|
tags = ["programming", "treehouse"]
|
||||||
- ### [not quite buildless][page:programming/blog/buildsome]
|
- ### [not quite buildless][page:programming/blog/buildsome]
|
||||||
|
|
||||||
% id = "01J7C1KBZ5XKZRN4V5BWFQTV6Y"
|
% id = "01J7C1KBZ5XKZRN4V5BWFQTV6Y"
|
||||||
|
@ -57,6 +60,7 @@ if you've been wondering what I've been up to, you've come to the right place.
|
||||||
- also, it's (way past) its one year anniversary! hooray!
|
- also, it's (way past) its one year anniversary! hooray!
|
||||||
|
|
||||||
% id = "01J73BSWA15KHTQ21T0S14NZW0"
|
% id = "01J73BSWA15KHTQ21T0S14NZW0"
|
||||||
|
tags = ["music", "programming"]
|
||||||
- ### [the ListenBrainz data set][page:music/brainz]
|
- ### [the ListenBrainz data set][page:music/brainz]
|
||||||
|
|
||||||
% id = "01J73BSWA1EX7ZP28KCCG088DD"
|
% id = "01J73BSWA1EX7ZP28KCCG088DD"
|
||||||
|
@ -66,6 +70,7 @@ if you've been wondering what I've been up to, you've come to the right place.
|
||||||
- I haven't done any of it yet, but I thought it'd be cool to share my ideas anyways!
|
- I haven't done any of it yet, but I thought it'd be cool to share my ideas anyways!
|
||||||
|
|
||||||
% id = "01J4J5N6WZQ03VTB3TZ51J7QZK"
|
% id = "01J4J5N6WZQ03VTB3TZ51J7QZK"
|
||||||
|
tags = ["programming", "plt", "haku"]
|
||||||
- ### [haku - writing a little programming language for fun][page:programming/blog/haku]
|
- ### [haku - writing a little programming language for fun][page:programming/blog/haku]
|
||||||
|
|
||||||
% id = "01J4J5N6WZQ1316WKDXB1M5W6E"
|
% id = "01J4J5N6WZQ1316WKDXB1M5W6E"
|
||||||
|
@ -79,6 +84,7 @@ if you've been wondering what I've been up to, you've come to the right place.
|
||||||
even though it didn't end up having macros...
|
even though it didn't end up having macros...
|
||||||
|
|
||||||
% id = "01J293BFEBT15W0Z3XF1HEFGZT"
|
% id = "01J293BFEBT15W0Z3XF1HEFGZT"
|
||||||
|
tags = ["programming", "javascript", "plt"]
|
||||||
- ### [JavaScript is not as bad as people make it out to be][page:programming/languages/javascript]
|
- ### [JavaScript is not as bad as people make it out to be][page:programming/languages/javascript]
|
||||||
|
|
||||||
% id = "01J293BFEB4G7214N20SZA8V7W"
|
% id = "01J293BFEB4G7214N20SZA8V7W"
|
||||||
|
@ -88,6 +94,7 @@ if you've been wondering what I've been up to, you've come to the right place.
|
||||||
- so I decided to collect my thoughts into a nice little page I can link easily.
|
- so I decided to collect my thoughts into a nice little page I can link easily.
|
||||||
|
|
||||||
% id = "01J0VNHPTRNC1HFXAQ790Y1EZB"
|
% id = "01J0VNHPTRNC1HFXAQ790Y1EZB"
|
||||||
|
tags = ["programming", "cxx"]
|
||||||
- ### [freeing C memory automatically using `std::unique_ptr` and `std::shared_ptr`][page:programming/languages/cxx/shared-unique-ptr-deleter]
|
- ### [freeing C memory automatically using `std::unique_ptr` and `std::shared_ptr`][page:programming/languages/cxx/shared-unique-ptr-deleter]
|
||||||
|
|
||||||
% id = "01J0VNHPTRP51XYDA4N2RPG58F"
|
% id = "01J0VNHPTRP51XYDA4N2RPG58F"
|
||||||
|
@ -100,6 +107,7 @@ if you've been wondering what I've been up to, you've come to the right place.
|
||||||
- on another note, I did read a blog post about this once somewhere, but couldn't be bothered to find it. so there you go! I made a post about this too.
|
- on another note, I did read a blog post about this once somewhere, but couldn't be bothered to find it. so there you go! I made a post about this too.
|
||||||
|
|
||||||
% id = "01J0KRPMV7SS48B64BFCJZK7VQ"
|
% id = "01J0KRPMV7SS48B64BFCJZK7VQ"
|
||||||
|
tags = ["meow"]
|
||||||
- ### [about me (version 2)][page:about]
|
- ### [about me (version 2)][page:about]
|
||||||
|
|
||||||
% id = "01J0KRPMV73K71D3QXFQ3GNY2N"
|
% id = "01J0KRPMV73K71D3QXFQ3GNY2N"
|
||||||
|
@ -110,13 +118,15 @@ if you've been wondering what I've been up to, you've come to the right place.
|
||||||
- [version 1][page:about/v1]
|
- [version 1][page:about/v1]
|
||||||
|
|
||||||
% id = "01HY5R1ZW2PYZSSP2J2KAA23DA"
|
% id = "01HY5R1ZW2PYZSSP2J2KAA23DA"
|
||||||
|
tags = ["programming", "c", "cxx", "plt"]
|
||||||
- ### [what's up with `*x` not always meaning the same thing in different contexts?][page:programming/blog/lvalues]
|
- ### [what's up with `*x` not always meaning the same thing in different contexts?][page:programming/blog/lvalues]
|
||||||
|
|
||||||
% id = "01HY5R1ZW24YJ2NF2RYWRZG4ZT"
|
% id = "01HY5R1ZW24YJ2NF2RYWRZG4ZT"
|
||||||
- I recently got a question from my someone telling me they doesn't understand why `*x` does not read from the pointer `x` when on the left-hand side of an assignment.
|
- I recently got a question from my someone telling me they don't understand why `*x` does not read from the pointer `x` when on the left-hand side of an assignment.
|
||||||
and that made me think, _why_ is that the case?
|
and that made me think, _why_ is that the case?
|
||||||
|
|
||||||
% id = "01HV1DGFHZ65GJVQRSREKR67J9"
|
% id = "01HV1DGFHZ65GJVQRSREKR67J9"
|
||||||
|
tags = ["programming", "philosophy"]
|
||||||
- ### [systems are just a bunch of code][page:programming/blog/systems]
|
- ### [systems are just a bunch of code][page:programming/blog/systems]
|
||||||
|
|
||||||
% id = "01HV1DGFHZFFZSQNCVWBTJ1VHM"
|
% id = "01HV1DGFHZFFZSQNCVWBTJ1VHM"
|
||||||
|
@ -129,18 +139,21 @@ if you've been wondering what I've been up to, you've come to the right place.
|
||||||
- bonus: [dismantling Unreal Engine's `GENERATED_BODY`][page:programming/technologies/unreal-engine/generated-body]
|
- bonus: [dismantling Unreal Engine's `GENERATED_BODY`][page:programming/technologies/unreal-engine/generated-body]
|
||||||
|
|
||||||
% id = "01HTWNETT2S5NSBF3QR4HYA7HN"
|
% id = "01HTWNETT2S5NSBF3QR4HYA7HN"
|
||||||
|
tags = ["programming", "plt"]
|
||||||
- ### [OR-types][page:programming/blog/or-types]
|
- ### [OR-types][page:programming/blog/or-types]
|
||||||
|
|
||||||
% id = "01HTWNETT2N8NPENETWYFBTXEM"
|
% id = "01HTWNETT2N8NPENETWYFBTXEM"
|
||||||
- last night I couldn't sleep because of type theory. in the process of trying to write down my thoughts, I ended up discovering a class of types which, to my knowledge, no language implements.
|
- last night I couldn't sleep because of type theory. in the process of trying to write down my thoughts, I ended up discovering a class of types which, to my knowledge, no language implements.
|
||||||
|
|
||||||
% id = "01HRG3VN091V715A8T54QK5PVX"
|
% id = "01HRG3VN091V715A8T54QK5PVX"
|
||||||
|
tags = ["programming", "plt", "lua"]
|
||||||
- ### [programming languages: Lua][page:programming/languages/lua]
|
- ### [programming languages: Lua][page:programming/languages/lua]
|
||||||
|
|
||||||
% id = "01HRG3VN095BNHERHWVX1TKS9K"
|
% id = "01HRG3VN095BNHERHWVX1TKS9K"
|
||||||
- I really like Lua, did you know that? but I get kind of tired of explaining why a thousand times to people who don't know the language, so here's a page with my thoughts!
|
- I really like Lua, did you know that? but I get kind of tired of explaining why a thousand times to people who don't know the language, so here's a page with my thoughts!
|
||||||
|
|
||||||
% id = "01HR9ZTS8RS4VJNJYSNRQYSKHZ"
|
% id = "01HR9ZTS8RS4VJNJYSNRQYSKHZ"
|
||||||
|
tags = ["design"]
|
||||||
- ### [design: sidebars][page:design/sidebars]
|
- ### [design: sidebars][page:design/sidebars]
|
||||||
|
|
||||||
% id = "01HR9ZTS8RY3N4EJM5W7WBTF0G"
|
% id = "01HR9ZTS8RY3N4EJM5W7WBTF0G"
|
||||||
|
@ -150,6 +163,7 @@ if you've been wondering what I've been up to, you've come to the right place.
|
||||||
- seriously though. I don't like them.
|
- seriously though. I don't like them.
|
||||||
|
|
||||||
% id = "01HQ8KV8T8GRCVFDJ3EP6QE163"
|
% id = "01HQ8KV8T8GRCVFDJ3EP6QE163"
|
||||||
|
tags = ["design"]
|
||||||
- ### [liquidex's treehouse: design][page:design]
|
- ### [liquidex's treehouse: design][page:design]
|
||||||
|
|
||||||
% id = "01HQ8KV8T8EEX6XBG2K1X3FGKW"
|
% id = "01HQ8KV8T8EEX6XBG2K1X3FGKW"
|
||||||
|
@ -161,6 +175,7 @@ if you've been wondering what I've been up to, you've come to the right place.
|
||||||
- I also wrote a post summarising my thoughts: [_on digital textures_][page:design/digital-textures]
|
- I also wrote a post summarising my thoughts: [_on digital textures_][page:design/digital-textures]
|
||||||
|
|
||||||
% id = "01HQ6G30PTVT5H0Z04VVRHEZQF"
|
% id = "01HQ6G30PTVT5H0Z04VVRHEZQF"
|
||||||
|
tags = ["programming", "graphics", "javascript"]
|
||||||
- ### [tairu - an interactive exploration of 2D autotiling techniques][page:programming/blog/tairu]
|
- ### [tairu - an interactive exploration of 2D autotiling techniques][page:programming/blog/tairu]
|
||||||
|
|
||||||
% id = "01HQ6G30PTG8QA5MAPEJPWSM14"
|
% id = "01HQ6G30PTG8QA5MAPEJPWSM14"
|
||||||
|
@ -168,5 +183,3 @@ if you've been wondering what I've been up to, you've come to the right place.
|
||||||
|
|
||||||
% id = "01HQ6G30PT1D729Z29NYVDCFDB"
|
% id = "01HQ6G30PT1D729Z29NYVDCFDB"
|
||||||
- this post explores basically just that.
|
- this post explores basically just that.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ anyhow = "1.0.75"
|
||||||
axum = "0.7.4"
|
axum = "0.7.4"
|
||||||
base64 = "0.21.7"
|
base64 = "0.21.7"
|
||||||
blake3 = "1.5.3"
|
blake3 = "1.5.3"
|
||||||
chrono = "0.4.35"
|
chrono = { version = "0.4.35", features = ["serde"] }
|
||||||
clap = { version = "4.3.22", features = ["derive"] }
|
clap = { version = "4.3.22", features = ["derive"] }
|
||||||
codespan-reporting = "0.11.1"
|
codespan-reporting = "0.11.1"
|
||||||
dashmap = "6.1.0"
|
dashmap = "6.1.0"
|
||||||
|
|
|
@ -18,7 +18,7 @@ use serde::Deserialize;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tracing::{info, instrument};
|
use tracing::{info, instrument};
|
||||||
|
|
||||||
use crate::generate::Sources;
|
use crate::sources::Sources;
|
||||||
use crate::vfs::asynch::AsyncDir;
|
use crate::vfs::asynch::AsyncDir;
|
||||||
use crate::vfs::VPath;
|
use crate::vfs::VPath;
|
||||||
use crate::{html::EscapeHtml, state::Source};
|
use crate::{html::EscapeHtml, state::Source};
|
||||||
|
@ -59,6 +59,7 @@ fn get_content_type(extension: &str) -> Option<&'static str> {
|
||||||
"js" => Some("text/javascript"),
|
"js" => Some("text/javascript"),
|
||||||
"woff" => Some("font/woff2"),
|
"woff" => Some("font/woff2"),
|
||||||
"svg" => Some("image/svg+xml"),
|
"svg" => Some("image/svg+xml"),
|
||||||
|
"atom" => Some("application/atom+xml"),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
use std::{collections::HashMap, ops::ControlFlow};
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
ops::ControlFlow,
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||||
|
@ -31,6 +34,9 @@ pub struct Config {
|
||||||
/// Links exported to Markdown for use with reference syntax `[text][def:key]`.
|
/// Links exported to Markdown for use with reference syntax `[text][def:key]`.
|
||||||
pub defs: HashMap<String, String>,
|
pub defs: HashMap<String, String>,
|
||||||
|
|
||||||
|
/// Config for syndication feeds.
|
||||||
|
pub feed: Feed,
|
||||||
|
|
||||||
/// Redirects for moving pages around. These are used solely by the treehouse server.
|
/// Redirects for moving pages around. These are used solely by the treehouse server.
|
||||||
///
|
///
|
||||||
/// Note that redirects are only resolved _non-recursively_ by the server. For a configuration
|
/// Note that redirects are only resolved _non-recursively_ by the server. For a configuration
|
||||||
|
@ -74,6 +80,12 @@ pub struct Config {
|
||||||
pub syntaxes: HashMap<String, CompiledSyntax>,
|
pub syntaxes: HashMap<String, CompiledSyntax>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Feed {
|
||||||
|
/// Allowed tags in feed entries.
|
||||||
|
pub tags: HashSet<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct Redirects {
|
pub struct Redirects {
|
||||||
/// Page redirects. When a user navigates to a page, if they navigate to `url`, they will
|
/// Page redirects. When a user navigates to a page, if they navigate to `url`, they will
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
|
mod atom;
|
||||||
mod dir_helper;
|
mod dir_helper;
|
||||||
mod include_static_helper;
|
mod include_static_helper;
|
||||||
|
mod simple_template;
|
||||||
|
mod tree;
|
||||||
|
|
||||||
use std::{collections::HashMap, fmt, ops::ControlFlow, sync::Arc};
|
use std::{collections::HashMap, fmt, ops::ControlFlow, sync::Arc};
|
||||||
|
|
||||||
use anyhow::{anyhow, ensure, Context};
|
use atom::FeedDir;
|
||||||
use dir_helper::DirHelper;
|
use dir_helper::DirHelper;
|
||||||
use handlebars::{handlebars_helper, Handlebars};
|
use handlebars::{handlebars_helper, Handlebars};
|
||||||
use include_static_helper::IncludeStaticHelper;
|
use include_static_helper::IncludeStaticHelper;
|
||||||
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tracing::{error, info_span, instrument};
|
use tracing::{error, info_span, instrument};
|
||||||
|
|
||||||
|
@ -15,49 +17,56 @@ use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
dirs::Dirs,
|
dirs::Dirs,
|
||||||
fun::seasons::Season,
|
fun::seasons::Season,
|
||||||
html::{breadcrumbs::breadcrumbs_to_html, navmap::NavigationMap, tree::branches_to_html},
|
sources::Sources,
|
||||||
import_map::ImportMap,
|
|
||||||
parse::parse_tree_with_diagnostics,
|
|
||||||
state::{report_diagnostics, FileId, Source},
|
|
||||||
tree::SemaRoots,
|
|
||||||
vfs::{
|
vfs::{
|
||||||
self, Cd, ContentCache, Dir, DirEntry, DynDir, EditPath, ImageSize, MemDir, Overlay,
|
self, Cd, ContentCache, Dir, DirEntry, DynDir, HtmlCanonicalize, MemDir, Overlay, ToDynDir,
|
||||||
ToDynDir, VPath, VPathBuf,
|
VPath, VPathBuf,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::state::Treehouse;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Page {
|
|
||||||
title: String,
|
|
||||||
thumbnail: Option<Thumbnail>,
|
|
||||||
scripts: Vec<String>,
|
|
||||||
styles: Vec<String>,
|
|
||||||
breadcrumbs: String,
|
|
||||||
tree_path: Option<String>,
|
|
||||||
tree: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct Thumbnail {
|
|
||||||
url: String,
|
|
||||||
alt: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct BaseTemplateData<'a> {
|
struct BaseTemplateData<'a> {
|
||||||
config: &'a Config,
|
config: &'a Config,
|
||||||
import_map: String,
|
import_map: String,
|
||||||
season: Option<Season>,
|
season: Option<Season>,
|
||||||
dev: bool,
|
dev: bool,
|
||||||
|
feeds: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
impl<'a> BaseTemplateData<'a> {
|
||||||
struct PageTemplateData<'a> {
|
fn new(sources: &'a Sources) -> Self {
|
||||||
#[serde(flatten)]
|
Self {
|
||||||
base: &'a BaseTemplateData<'a>,
|
config: &sources.config,
|
||||||
page: Page,
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TreehouseDir {
|
||||||
|
dirs: Arc<Dirs>,
|
||||||
|
sources: Arc<Sources>,
|
||||||
|
handlebars: Arc<Handlebars<'static>>,
|
||||||
|
dir_index: DirIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TreehouseDir {
|
||||||
|
fn new(
|
||||||
|
dirs: Arc<Dirs>,
|
||||||
|
sources: Arc<Sources>,
|
||||||
|
handlebars: Arc<Handlebars<'static>>,
|
||||||
|
dir_index: DirIndex,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
dirs,
|
||||||
|
sources,
|
||||||
|
handlebars,
|
||||||
|
dir_index,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_handlebars(site: &str, static_: DynDir) -> Handlebars<'static> {
|
fn create_handlebars(site: &str, static_: DynDir) -> Handlebars<'static> {
|
||||||
|
@ -90,296 +99,6 @@ fn load_templates(handlebars: &mut Handlebars, dir: &dyn Dir) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(config, dirs))]
|
|
||||||
fn load_trees(config: &Config, dirs: &Dirs) -> anyhow::Result<Treehouse> {
|
|
||||||
let mut treehouse = Treehouse::new();
|
|
||||||
let mut diagnostics = vec![];
|
|
||||||
let mut parsed_trees = HashMap::new();
|
|
||||||
|
|
||||||
let mut paths = vec![];
|
|
||||||
|
|
||||||
vfs::walk_dir_rec(&*dirs.content, VPath::ROOT, &mut |path| {
|
|
||||||
if path.extension() == Some("tree") {
|
|
||||||
paths.push(path.to_owned());
|
|
||||||
}
|
|
||||||
ControlFlow::Continue(())
|
|
||||||
});
|
|
||||||
|
|
||||||
// NOTE: Sources are filled in later; they can be left out until a call to report_diagnostics.
|
|
||||||
let file_ids: Vec<_> = paths
|
|
||||||
.iter()
|
|
||||||
.map(|path| treehouse.add_file(path.clone(), Source::Other(String::new())))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let parse_results: Vec<_> = {
|
|
||||||
let _span = info_span!("load_trees::parse").entered();
|
|
||||||
paths
|
|
||||||
.into_par_iter()
|
|
||||||
.zip(&file_ids)
|
|
||||||
.flat_map(|(path, &file_id)| {
|
|
||||||
dirs.content
|
|
||||||
.content(&path)
|
|
||||||
.and_then(|b| String::from_utf8(b).ok())
|
|
||||||
.map(|input| {
|
|
||||||
let parse_result = parse_tree_with_diagnostics(file_id, &input);
|
|
||||||
(path, file_id, input, parse_result)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
for (path, file_id, input, _) in &parse_results {
|
|
||||||
let tree_path = path.with_extension("");
|
|
||||||
treehouse
|
|
||||||
.files_by_tree_path
|
|
||||||
.insert(tree_path.clone(), *file_id);
|
|
||||||
treehouse.set_source(
|
|
||||||
*file_id,
|
|
||||||
Source::Tree {
|
|
||||||
input: input.clone(),
|
|
||||||
tree_path,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let _span = info_span!("load_trees::sema").entered();
|
|
||||||
for (path, file_id, _, result) in parse_results {
|
|
||||||
match result {
|
|
||||||
Ok(roots) => {
|
|
||||||
let roots = SemaRoots::from_roots(
|
|
||||||
&mut treehouse,
|
|
||||||
&mut diagnostics,
|
|
||||||
config,
|
|
||||||
file_id,
|
|
||||||
roots,
|
|
||||||
);
|
|
||||||
treehouse.roots.insert(file_id, roots);
|
|
||||||
parsed_trees.insert(path, file_id);
|
|
||||||
}
|
|
||||||
Err(mut parse_diagnostics) => diagnostics.append(&mut parse_diagnostics),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
report_diagnostics(&treehouse, &diagnostics)?;
|
|
||||||
|
|
||||||
Ok(treehouse)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(sources, handlebars))]
|
|
||||||
fn generate_simple_template(
|
|
||||||
sources: &Sources,
|
|
||||||
handlebars: &Handlebars,
|
|
||||||
template_name: &str,
|
|
||||||
) -> anyhow::Result<String> {
|
|
||||||
let base_template_data = BaseTemplateData {
|
|
||||||
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),
|
|
||||||
};
|
|
||||||
handlebars
|
|
||||||
.render(template_name, &base_template_data)
|
|
||||||
.context("failed to render template")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_simple_template_or_error(
|
|
||||||
sources: &Sources,
|
|
||||||
handlebars: &Handlebars,
|
|
||||||
template_name: &str,
|
|
||||||
) -> String {
|
|
||||||
match generate_simple_template(sources, handlebars, template_name) {
|
|
||||||
Ok(html) => html,
|
|
||||||
Err(error) => format!("error: {error:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[instrument(skip(sources, dirs, handlebars))]
|
|
||||||
fn generate_tree(
|
|
||||||
sources: &Sources,
|
|
||||||
dirs: &Dirs,
|
|
||||||
handlebars: &Handlebars,
|
|
||||||
file_id: FileId,
|
|
||||||
) -> anyhow::Result<String> {
|
|
||||||
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::branches_to_html").entered();
|
|
||||||
let mut tree = String::new();
|
|
||||||
branches_to_html(
|
|
||||||
&mut tree,
|
|
||||||
&sources.treehouse,
|
|
||||||
&sources.config,
|
|
||||||
dirs,
|
|
||||||
file_id,
|
|
||||||
&roots.branches,
|
|
||||||
);
|
|
||||||
tree
|
|
||||||
};
|
|
||||||
|
|
||||||
let base_template_data = BaseTemplateData {
|
|
||||||
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),
|
|
||||||
};
|
|
||||||
|
|
||||||
let template_data = PageTemplateData {
|
|
||||||
base: &base_template_data,
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_tree_or_error(
|
|
||||||
sources: &Sources,
|
|
||||||
dirs: &Dirs,
|
|
||||||
handlebars: &Handlebars,
|
|
||||||
file_id: FileId,
|
|
||||||
) -> String {
|
|
||||||
match generate_tree(sources, dirs, handlebars, file_id) {
|
|
||||||
Ok(html) => html,
|
|
||||||
Err(error) => format!("error: {error:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Sources {
|
|
||||||
pub config: Config,
|
|
||||||
pub treehouse: Treehouse,
|
|
||||||
pub navigation_map: NavigationMap,
|
|
||||||
pub import_map: ImportMap,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sources {
|
|
||||||
pub fn load(dirs: &Dirs) -> anyhow::Result<Self> {
|
|
||||||
let config = {
|
|
||||||
let _span = info_span!("load_config").entered();
|
|
||||||
let mut config: Config = toml_edit::de::from_str(
|
|
||||||
&dirs
|
|
||||||
.root
|
|
||||||
.content(VPath::new("treehouse.toml"))
|
|
||||||
.map(String::from_utf8)
|
|
||||||
.ok_or_else(|| anyhow!("config file does not exist"))??,
|
|
||||||
)
|
|
||||||
.context("failed to deserialize config")?;
|
|
||||||
config.site = std::env::var("TREEHOUSE_SITE").unwrap_or(config.site);
|
|
||||||
config.autopopulate_emoji(&*dirs.emoji)?;
|
|
||||||
config.autopopulate_pics(&*dirs.pic)?;
|
|
||||||
config.load_syntaxes(dirs.syntax.clone())?;
|
|
||||||
config
|
|
||||||
};
|
|
||||||
|
|
||||||
let treehouse = load_trees(&config, dirs)?;
|
|
||||||
let navigation_map = NavigationMap::build(
|
|
||||||
&treehouse,
|
|
||||||
treehouse.files_by_tree_path[VPath::new("index")],
|
|
||||||
);
|
|
||||||
let import_map = ImportMap::generate(
|
|
||||||
&config.site,
|
|
||||||
&Cd::new(dirs.static_.clone(), VPathBuf::new("js")),
|
|
||||||
&config.build.javascript.import_roots,
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(Sources {
|
|
||||||
config,
|
|
||||||
treehouse,
|
|
||||||
navigation_map,
|
|
||||||
import_map,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Acceleration structure for `dir` operations on [`TreehouseDir`]s.
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct DirIndex {
|
|
||||||
full_path: VPathBuf,
|
|
||||||
children: HashMap<VPathBuf, DirIndex>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DirIndex {
|
|
||||||
#[instrument(name = "DirIndex::new", skip(paths))]
|
|
||||||
pub fn new<'a>(paths: impl Iterator<Item = &'a VPath>) -> 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct TreehouseDir {
|
|
||||||
dirs: Arc<Dirs>,
|
|
||||||
sources: Arc<Sources>,
|
|
||||||
dir_index: DirIndex,
|
|
||||||
handlebars: Handlebars<'static>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TreehouseDir {
|
|
||||||
fn new(dirs: Arc<Dirs>, sources: Arc<Sources>, dir_index: DirIndex) -> Self {
|
|
||||||
let mut handlebars = create_handlebars(&sources.config.site, dirs.static_.clone());
|
|
||||||
load_templates(&mut handlebars, &dirs.template);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
dirs,
|
|
||||||
sources,
|
|
||||||
dir_index,
|
|
||||||
handlebars,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Dir for TreehouseDir {
|
impl Dir for TreehouseDir {
|
||||||
#[instrument("TreehouseDir::dir", skip(self))]
|
#[instrument("TreehouseDir::dir", skip(self))]
|
||||||
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||||
|
@ -418,14 +137,14 @@ impl Dir for TreehouseDir {
|
||||||
.files_by_tree_path
|
.files_by_tree_path
|
||||||
.get(path)
|
.get(path)
|
||||||
.map(|&file_id| {
|
.map(|&file_id| {
|
||||||
generate_tree_or_error(&self.sources, &self.dirs, &self.handlebars, file_id).into()
|
tree::generate_or_error(&self.sources, &self.dirs, &self.handlebars, file_id).into()
|
||||||
})
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
if path.file_name().is_some_and(|s| !s.starts_with('_')) {
|
if path.file_name().is_some_and(|s| !s.starts_with('_')) {
|
||||||
let template_name = path.with_extension("hbs");
|
let template_name = path.with_extension("hbs");
|
||||||
if self.handlebars.has_template(template_name.as_str()) {
|
if self.handlebars.has_template(template_name.as_str()) {
|
||||||
return Some(
|
return Some(
|
||||||
generate_simple_template_or_error(
|
simple_template::generate_or_error(
|
||||||
&self.sources,
|
&self.sources,
|
||||||
&self.handlebars,
|
&self.handlebars,
|
||||||
template_name.as_str(),
|
template_name.as_str(),
|
||||||
|
@ -449,61 +168,53 @@ impl fmt::Debug for TreehouseDir {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HtmlCanonicalize<T> {
|
/// Acceleration structure for `dir` operations on [`TreehouseDir`]s.
|
||||||
inner: T,
|
#[derive(Debug, Default)]
|
||||||
|
struct DirIndex {
|
||||||
|
full_path: VPathBuf,
|
||||||
|
children: HashMap<VPathBuf, DirIndex>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> HtmlCanonicalize<T> {
|
impl DirIndex {
|
||||||
pub fn new(inner: T) -> Self {
|
#[instrument(name = "DirIndex::new", skip(paths))]
|
||||||
Self { inner }
|
pub fn new<'a>(paths: impl Iterator<Item = &'a VPath>) -> Self {
|
||||||
}
|
let mut root = DirIndex::default();
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Dir for HtmlCanonicalize<T>
|
for path in paths {
|
||||||
where
|
let mut parent = &mut root;
|
||||||
T: Dir,
|
let mut full_path = VPath::ROOT.to_owned();
|
||||||
{
|
for segment in path.segments() {
|
||||||
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
full_path.push(segment);
|
||||||
self.inner.dir(path)
|
let child = parent
|
||||||
|
.children
|
||||||
|
.entry(segment.to_owned())
|
||||||
|
.or_insert_with(|| DirIndex {
|
||||||
|
full_path: full_path.clone(),
|
||||||
|
children: HashMap::new(),
|
||||||
|
});
|
||||||
|
parent = child;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
root
|
||||||
let mut path = path.to_owned();
|
|
||||||
if path.extension() == Some("html") {
|
|
||||||
path.set_extension("");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.inner.content(&path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn content_version(&self, path: &VPath) -> Option<String> {
|
|
||||||
self.inner.content_version(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
|
||||||
self.inner.image_size(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
|
||||||
self.inner.anchor(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
|
|
||||||
self.inner.edit_path(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> fmt::Debug for HtmlCanonicalize<T>
|
|
||||||
where
|
|
||||||
T: fmt::Debug,
|
|
||||||
{
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "HtmlCanonicalize({:?})", self.inner)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn target(dirs: Arc<Dirs>, sources: Arc<Sources>) -> DynDir {
|
pub fn target(dirs: Arc<Dirs>, sources: Arc<Sources>) -> 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();
|
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("static"), dirs.static_.clone());
|
||||||
root.add(
|
root.add(
|
||||||
VPath::new("robots.txt"),
|
VPath::new("robots.txt"),
|
||||||
|
@ -511,7 +222,7 @@ pub fn target(dirs: Arc<Dirs>, sources: Arc<Sources>) -> DynDir {
|
||||||
);
|
);
|
||||||
|
|
||||||
let dir_index = DirIndex::new(sources.treehouse.files_by_tree_path.keys().map(|x| &**x));
|
let dir_index = DirIndex::new(sources.treehouse.files_by_tree_path.keys().map(|x| &**x));
|
||||||
let tree_view = TreehouseDir::new(dirs, sources, dir_index);
|
let tree_view = TreehouseDir::new(dirs, sources, handlebars, dir_index);
|
||||||
|
|
||||||
let tree_view = ContentCache::new(tree_view);
|
let tree_view = ContentCache::new(tree_view);
|
||||||
tree_view.warm_up();
|
tree_view.warm_up();
|
||||||
|
|
302
crates/treehouse/src/generate/atom.rs
Normal file
302
crates/treehouse/src/generate/atom.rs
Normal file
|
@ -0,0 +1,302 @@
|
||||||
|
use std::{fmt, sync::Arc};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use handlebars::Handlebars;
|
||||||
|
use serde::Serialize;
|
||||||
|
use tracing::{info, info_span, instrument};
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
dirs::Dirs,
|
||||||
|
html::djot::{self, resolve_link},
|
||||||
|
sources::Sources,
|
||||||
|
state::FileId,
|
||||||
|
tree::SemaBranchId,
|
||||||
|
vfs::{Dir, DirEntry, VPath, VPathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::BaseTemplateData;
|
||||||
|
|
||||||
|
pub struct FeedDir {
|
||||||
|
dirs: Arc<Dirs>,
|
||||||
|
sources: Arc<Sources>,
|
||||||
|
handlebars: Arc<Handlebars<'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeedDir {
|
||||||
|
pub fn new(
|
||||||
|
dirs: Arc<Dirs>,
|
||||||
|
sources: Arc<Sources>,
|
||||||
|
handlebars: Arc<Handlebars<'static>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
dirs,
|
||||||
|
sources,
|
||||||
|
handlebars,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dir for FeedDir {
|
||||||
|
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||||
|
if path == VPath::ROOT {
|
||||||
|
self.sources
|
||||||
|
.treehouse
|
||||||
|
.feeds_by_name
|
||||||
|
.keys()
|
||||||
|
.map(|name| DirEntry {
|
||||||
|
path: VPathBuf::new(format!("{name}.atom")),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||||
|
info!("{path}");
|
||||||
|
if path.extension() == Some("atom") {
|
||||||
|
let feed_name = path.with_extension("").to_string();
|
||||||
|
self.sources
|
||||||
|
.treehouse
|
||||||
|
.feeds_by_name
|
||||||
|
.get(&feed_name)
|
||||||
|
.map(|file_id| {
|
||||||
|
generate_or_error(&self.sources, &self.dirs, &self.handlebars, *file_id).into()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content_version(&self, _path: &VPath) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for FeedDir {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str("FeedDir")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Feed {
|
||||||
|
name: String,
|
||||||
|
updated: DateTime<Utc>,
|
||||||
|
entries: Vec<Entry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Entry {
|
||||||
|
id: String,
|
||||||
|
updated: DateTime<Utc>,
|
||||||
|
url: String,
|
||||||
|
title: String,
|
||||||
|
categories: Vec<String>,
|
||||||
|
summary: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct AtomTemplateData<'a> {
|
||||||
|
#[serde(flatten)]
|
||||||
|
base: &'a BaseTemplateData<'a>,
|
||||||
|
feed: Feed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(name = "atom::generate", skip(sources, handlebars))]
|
||||||
|
pub fn generate(
|
||||||
|
sources: &Sources,
|
||||||
|
dirs: &Dirs,
|
||||||
|
handlebars: &Handlebars,
|
||||||
|
file_id: FileId,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
let roots = &sources.treehouse.roots[&file_id];
|
||||||
|
let feed_name = roots.attributes.feed.clone().expect("page must be a feed");
|
||||||
|
|
||||||
|
let template_data = AtomTemplateData {
|
||||||
|
base: &BaseTemplateData::new(sources),
|
||||||
|
feed: Feed {
|
||||||
|
name: feed_name,
|
||||||
|
// The content cache layer should take care of sampling the current time only once,
|
||||||
|
// and then preserving it until the treehouse is deployed again.
|
||||||
|
updated: Utc::now(),
|
||||||
|
entries: extract_entries(sources, dirs, file_id),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let _span = info_span!("handlebars::render").entered();
|
||||||
|
handlebars
|
||||||
|
.render("_feed_atom.hbs", &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:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_entries(sources: &Sources, dirs: &Dirs, file_id: FileId) -> Vec<Entry> {
|
||||||
|
let roots = &sources.treehouse.roots[&file_id];
|
||||||
|
|
||||||
|
roots
|
||||||
|
.branches
|
||||||
|
.iter()
|
||||||
|
.flat_map(|&branch_id| {
|
||||||
|
let branch = sources.treehouse.tree.branch(branch_id);
|
||||||
|
|
||||||
|
let text = &sources.treehouse.source(file_id).input()[branch.content.clone()];
|
||||||
|
let parsed = parse_entry(sources, dirs, file_id, jotdown::Parser::new(text));
|
||||||
|
|
||||||
|
let mut summary = String::new();
|
||||||
|
branches_to_html_simple(&mut summary, sources, dirs, file_id, &branch.children);
|
||||||
|
|
||||||
|
let updated = Ulid::from_string(&branch.attributes.id)
|
||||||
|
.ok()
|
||||||
|
.and_then(|ulid| DateTime::from_timestamp_millis(ulid.timestamp_ms() as i64))
|
||||||
|
.unwrap_or(DateTime::UNIX_EPOCH); // if you see the Unix epoch... oops
|
||||||
|
|
||||||
|
parsed.link.map(|url| Entry {
|
||||||
|
id: branch.attributes.id.clone(),
|
||||||
|
updated,
|
||||||
|
url,
|
||||||
|
title: parsed.title.unwrap_or_else(|| "untitled".into()),
|
||||||
|
categories: branch.attributes.tags.clone(),
|
||||||
|
summary,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct ParsedEntry {
|
||||||
|
title: Option<String>,
|
||||||
|
link: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <h3> and <a> 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,
|
||||||
|
sources: &Sources,
|
||||||
|
dirs: &Dirs,
|
||||||
|
file_id: FileId,
|
||||||
|
branches: &[SemaBranchId],
|
||||||
|
) {
|
||||||
|
s.push_str("<ul>");
|
||||||
|
for &branch_id in branches {
|
||||||
|
let branch = sources.treehouse.tree.branch(branch_id);
|
||||||
|
|
||||||
|
s.push_str("<li>");
|
||||||
|
|
||||||
|
let text = &sources.treehouse.source(file_id).input()[branch.content.clone()];
|
||||||
|
let events: Vec<_> = jotdown::Parser::new(text).into_offset_iter().collect();
|
||||||
|
// Ignore render diagnostics. Those should be reported by the main HTML generator.
|
||||||
|
let _render_diagnostics = djot::Renderer {
|
||||||
|
config: &sources.config,
|
||||||
|
dirs,
|
||||||
|
treehouse: &sources.treehouse,
|
||||||
|
file_id,
|
||||||
|
|
||||||
|
// Yeah, maybe don't include literate code in summaries...
|
||||||
|
page_id: "liquidex-is-a-dummy".into(),
|
||||||
|
}
|
||||||
|
.render(&events, s);
|
||||||
|
|
||||||
|
if !branch.children.is_empty() {
|
||||||
|
branches_to_html_simple(s, sources, dirs, file_id, &branch.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
s.push_str("</li>");
|
||||||
|
}
|
||||||
|
s.push_str("</ul>");
|
||||||
|
}
|
30
crates/treehouse/src/generate/simple_template.rs
Normal file
30
crates/treehouse/src/generate/simple_template.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
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<String> {
|
||||||
|
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:?}"),
|
||||||
|
}
|
||||||
|
}
|
113
crates/treehouse/src/generate/tree.rs
Normal file
113
crates/treehouse/src/generate/tree.rs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
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::branches_to_html},
|
||||||
|
sources::Sources,
|
||||||
|
state::FileId,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Page {
|
||||||
|
title: String,
|
||||||
|
thumbnail: Option<Thumbnail>,
|
||||||
|
scripts: Vec<String>,
|
||||||
|
styles: Vec<String>,
|
||||||
|
breadcrumbs: String,
|
||||||
|
tree_path: Option<String>,
|
||||||
|
tree: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Thumbnail {
|
||||||
|
url: String,
|
||||||
|
alt: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String> {
|
||||||
|
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::branches_to_html").entered();
|
||||||
|
let mut tree = String::new();
|
||||||
|
branches_to_html(
|
||||||
|
&mut tree,
|
||||||
|
&sources.treehouse,
|
||||||
|
&sources.config,
|
||||||
|
dirs,
|
||||||
|
file_id,
|
||||||
|
&roots.branches,
|
||||||
|
);
|
||||||
|
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:?}"),
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
use std::fmt::{self, Display, Write};
|
use std::fmt::{self, Display, Write};
|
||||||
|
|
||||||
pub mod breadcrumbs;
|
pub mod breadcrumbs;
|
||||||
mod djot;
|
pub mod djot;
|
||||||
pub mod highlight;
|
pub mod highlight;
|
||||||
pub mod navmap;
|
pub mod navmap;
|
||||||
pub mod tree;
|
pub mod tree;
|
||||||
|
|
|
@ -27,9 +27,7 @@ use super::highlight::highlight;
|
||||||
/// [`Render`] implementor that writes HTML output.
|
/// [`Render`] implementor that writes HTML output.
|
||||||
pub struct Renderer<'a> {
|
pub struct Renderer<'a> {
|
||||||
pub config: &'a Config,
|
pub config: &'a Config,
|
||||||
|
|
||||||
pub dirs: &'a Dirs,
|
pub dirs: &'a Dirs,
|
||||||
|
|
||||||
pub treehouse: &'a Treehouse,
|
pub treehouse: &'a Treehouse,
|
||||||
pub file_id: FileId,
|
pub file_id: FileId,
|
||||||
pub page_id: String,
|
pub page_id: String,
|
||||||
|
@ -226,7 +224,12 @@ impl<'a> Writer<'a> {
|
||||||
Container::Link(dst, ty) => {
|
Container::Link(dst, ty) => {
|
||||||
if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) {
|
if matches!(ty, LinkType::Span(SpanLinkType::Unresolved)) {
|
||||||
out.push_str("<a");
|
out.push_str("<a");
|
||||||
if let Some(resolved) = self.resolve_link(dst) {
|
if let Some(resolved) = resolve_link(
|
||||||
|
self.renderer.config,
|
||||||
|
self.renderer.treehouse,
|
||||||
|
self.renderer.dirs,
|
||||||
|
dst,
|
||||||
|
) {
|
||||||
out.push_str(r#" href=""#);
|
out.push_str(r#" href=""#);
|
||||||
write_attr(&resolved, out);
|
write_attr(&resolved, out);
|
||||||
out.push('"');
|
out.push('"');
|
||||||
|
@ -479,7 +482,12 @@ impl<'a> Writer<'a> {
|
||||||
out.push_str(r#"" src=""#);
|
out.push_str(r#"" src=""#);
|
||||||
if let SpanLinkType::Unresolved = link_type {
|
if let SpanLinkType::Unresolved = link_type {
|
||||||
// TODO: Image size.
|
// TODO: Image size.
|
||||||
if let Some(resolved) = self.resolve_link(src) {
|
if let Some(resolved) = resolve_link(
|
||||||
|
self.renderer.config,
|
||||||
|
self.renderer.treehouse,
|
||||||
|
self.renderer.dirs,
|
||||||
|
src,
|
||||||
|
) {
|
||||||
write_attr(&resolved, out);
|
write_attr(&resolved, out);
|
||||||
} else {
|
} else {
|
||||||
write_attr(src, out);
|
write_attr(src, out);
|
||||||
|
@ -624,28 +632,6 @@ impl<'a> Writer<'a> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_link(&self, link: &str) -> Option<String> {
|
|
||||||
let Renderer {
|
|
||||||
config, treehouse, ..
|
|
||||||
} = &self.renderer;
|
|
||||||
link.split_once(':').and_then(|(kind, linked)| match kind {
|
|
||||||
"def" => config.defs.get(linked).cloned(),
|
|
||||||
"branch" => treehouse
|
|
||||||
.branches_by_named_id
|
|
||||||
.get(linked)
|
|
||||||
.map(|&branch_id| {
|
|
||||||
format!(
|
|
||||||
"{}/b?{}",
|
|
||||||
config.site,
|
|
||||||
treehouse.tree.branch(branch_id).attributes.id
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
"page" => Some(config.page_url(linked)),
|
|
||||||
"pic" => Some(config.pic_url(&*self.renderer.dirs.pic, linked)),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_text(s: &str, out: &mut String) {
|
fn write_text(s: &str, out: &mut String) {
|
||||||
|
@ -677,3 +663,27 @@ fn write_escape(mut s: &str, escape_quotes: bool, out: &mut String) {
|
||||||
}
|
}
|
||||||
out.push_str(s);
|
out.push_str(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn resolve_link(
|
||||||
|
config: &Config,
|
||||||
|
treehouse: &Treehouse,
|
||||||
|
dirs: &Dirs,
|
||||||
|
link: &str,
|
||||||
|
) -> Option<String> {
|
||||||
|
link.split_once(':').and_then(|(kind, linked)| match kind {
|
||||||
|
"def" => config.defs.get(linked).cloned(),
|
||||||
|
"branch" => treehouse
|
||||||
|
.branches_by_named_id
|
||||||
|
.get(linked)
|
||||||
|
.map(|&branch_id| {
|
||||||
|
format!(
|
||||||
|
"{}/b?{}",
|
||||||
|
config.site,
|
||||||
|
treehouse.tree.branch(branch_id).attributes.id
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
"page" => Some(config.page_url(linked)),
|
||||||
|
"pic" => Some(config.pic_url(&*dirs.pic, linked)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ pub mod html;
|
||||||
pub mod import_map;
|
pub mod import_map;
|
||||||
pub mod parse;
|
pub mod parse;
|
||||||
pub mod paths;
|
pub mod paths;
|
||||||
|
pub mod sources;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod tree;
|
pub mod tree;
|
||||||
pub mod vfs;
|
pub mod vfs;
|
||||||
|
|
|
@ -9,7 +9,8 @@ use tracing_subscriber::layer::SubscriberExt as _;
|
||||||
use tracing_subscriber::util::SubscriberInitExt as _;
|
use tracing_subscriber::util::SubscriberInitExt as _;
|
||||||
use treehouse::cli::serve::serve;
|
use treehouse::cli::serve::serve;
|
||||||
use treehouse::dirs::Dirs;
|
use treehouse::dirs::Dirs;
|
||||||
use treehouse::generate::{self, Sources};
|
use treehouse::generate;
|
||||||
|
use treehouse::sources::Sources;
|
||||||
use treehouse::vfs::asynch::AsyncDir;
|
use treehouse::vfs::asynch::AsyncDir;
|
||||||
use treehouse::vfs::{
|
use treehouse::vfs::{
|
||||||
AnchoredAtExt, Blake3ContentVersionCache, DynDir, ImageSizeCache, ToDynDir, VPathBuf,
|
AnchoredAtExt, Blake3ContentVersionCache, DynDir, ImageSizeCache, ToDynDir, VPathBuf,
|
||||||
|
|
139
crates/treehouse/src/sources.rs
Normal file
139
crates/treehouse/src/sources.rs
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
use std::{collections::HashMap, ops::ControlFlow};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Context};
|
||||||
|
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
|
||||||
|
use tracing::{info_span, instrument};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
dirs::Dirs,
|
||||||
|
html::navmap::NavigationMap,
|
||||||
|
import_map::ImportMap,
|
||||||
|
parse::parse_tree_with_diagnostics,
|
||||||
|
state::{report_diagnostics, Source, Treehouse},
|
||||||
|
tree::SemaRoots,
|
||||||
|
vfs::{self, Cd, VPath, VPathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Sources {
|
||||||
|
pub config: Config,
|
||||||
|
pub treehouse: Treehouse,
|
||||||
|
pub navigation_map: NavigationMap,
|
||||||
|
pub import_map: ImportMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sources {
|
||||||
|
pub fn load(dirs: &Dirs) -> anyhow::Result<Self> {
|
||||||
|
let config = {
|
||||||
|
let _span = info_span!("load_config").entered();
|
||||||
|
let mut config: Config = toml_edit::de::from_str(
|
||||||
|
&dirs
|
||||||
|
.root
|
||||||
|
.content(VPath::new("treehouse.toml"))
|
||||||
|
.map(String::from_utf8)
|
||||||
|
.ok_or_else(|| anyhow!("config file does not exist"))??,
|
||||||
|
)
|
||||||
|
.context("failed to deserialize config")?;
|
||||||
|
config.site = std::env::var("TREEHOUSE_SITE").unwrap_or(config.site);
|
||||||
|
config.autopopulate_emoji(&*dirs.emoji)?;
|
||||||
|
config.autopopulate_pics(&*dirs.pic)?;
|
||||||
|
config.load_syntaxes(dirs.syntax.clone())?;
|
||||||
|
config
|
||||||
|
};
|
||||||
|
|
||||||
|
let treehouse = load_trees(&config, dirs)?;
|
||||||
|
let navigation_map = NavigationMap::build(
|
||||||
|
&treehouse,
|
||||||
|
treehouse.files_by_tree_path[VPath::new("index")],
|
||||||
|
);
|
||||||
|
let import_map = ImportMap::generate(
|
||||||
|
&config.site,
|
||||||
|
&Cd::new(dirs.static_.clone(), VPathBuf::new("js")),
|
||||||
|
&config.build.javascript.import_roots,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Sources {
|
||||||
|
config,
|
||||||
|
treehouse,
|
||||||
|
navigation_map,
|
||||||
|
import_map,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(config, dirs))]
|
||||||
|
fn load_trees(config: &Config, dirs: &Dirs) -> anyhow::Result<Treehouse> {
|
||||||
|
let mut treehouse = Treehouse::new();
|
||||||
|
let mut diagnostics = vec![];
|
||||||
|
let mut parsed_trees = HashMap::new();
|
||||||
|
|
||||||
|
let mut paths = vec![];
|
||||||
|
|
||||||
|
vfs::walk_dir_rec(&*dirs.content, VPath::ROOT, &mut |path| {
|
||||||
|
if path.extension() == Some("tree") {
|
||||||
|
paths.push(path.to_owned());
|
||||||
|
}
|
||||||
|
ControlFlow::Continue(())
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE: Sources are filled in later; they can be left out until a call to report_diagnostics.
|
||||||
|
let file_ids: Vec<_> = paths
|
||||||
|
.iter()
|
||||||
|
.map(|path| treehouse.add_file(path.clone(), Source::Other(String::new())))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let parse_results: Vec<_> = {
|
||||||
|
let _span = info_span!("load_trees::parse").entered();
|
||||||
|
paths
|
||||||
|
.into_par_iter()
|
||||||
|
.zip(&file_ids)
|
||||||
|
.flat_map(|(path, &file_id)| {
|
||||||
|
dirs.content
|
||||||
|
.content(&path)
|
||||||
|
.and_then(|b| String::from_utf8(b).ok())
|
||||||
|
.map(|input| {
|
||||||
|
let parse_result = parse_tree_with_diagnostics(file_id, &input);
|
||||||
|
(path, file_id, input, parse_result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
for (path, file_id, input, _) in &parse_results {
|
||||||
|
let tree_path = path.with_extension("");
|
||||||
|
treehouse
|
||||||
|
.files_by_tree_path
|
||||||
|
.insert(tree_path.clone(), *file_id);
|
||||||
|
treehouse.set_source(
|
||||||
|
*file_id,
|
||||||
|
Source::Tree {
|
||||||
|
input: input.clone(),
|
||||||
|
tree_path,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let _span = info_span!("load_trees::sema").entered();
|
||||||
|
for (path, file_id, _, result) in parse_results {
|
||||||
|
match result {
|
||||||
|
Ok(roots) => {
|
||||||
|
let roots = SemaRoots::from_roots(
|
||||||
|
&mut treehouse,
|
||||||
|
&mut diagnostics,
|
||||||
|
config,
|
||||||
|
file_id,
|
||||||
|
roots,
|
||||||
|
);
|
||||||
|
treehouse.roots.insert(file_id, roots);
|
||||||
|
parsed_trees.insert(path, file_id);
|
||||||
|
}
|
||||||
|
Err(mut parse_diagnostics) => diagnostics.append(&mut parse_diagnostics),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report_diagnostics(&treehouse, &diagnostics)?;
|
||||||
|
|
||||||
|
Ok(treehouse)
|
||||||
|
}
|
|
@ -67,6 +67,7 @@ pub struct FileId(usize);
|
||||||
pub struct Treehouse {
|
pub struct Treehouse {
|
||||||
pub files: Vec<File>,
|
pub files: Vec<File>,
|
||||||
pub files_by_tree_path: HashMap<VPathBuf, FileId>,
|
pub files_by_tree_path: HashMap<VPathBuf, FileId>,
|
||||||
|
pub feeds_by_name: HashMap<String, FileId>,
|
||||||
|
|
||||||
pub tree: SemaTree,
|
pub tree: SemaTree,
|
||||||
pub branches_by_named_id: HashMap<String, SemaBranchId>,
|
pub branches_by_named_id: HashMap<String, SemaBranchId>,
|
||||||
|
@ -82,6 +83,7 @@ impl Treehouse {
|
||||||
Self {
|
Self {
|
||||||
files: vec![],
|
files: vec![],
|
||||||
files_by_tree_path: HashMap::new(),
|
files_by_tree_path: HashMap::new(),
|
||||||
|
feeds_by_name: HashMap::new(),
|
||||||
|
|
||||||
tree: SemaTree::default(),
|
tree: SemaTree::default(),
|
||||||
branches_by_named_id: HashMap::new(),
|
branches_by_named_id: HashMap::new(),
|
||||||
|
|
|
@ -61,7 +61,9 @@ impl SemaRoots {
|
||||||
branches: roots
|
branches: roots
|
||||||
.branches
|
.branches
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|branch| SemaBranch::from_branch(treehouse, diagnostics, file_id, branch))
|
.map(|branch| {
|
||||||
|
SemaBranch::from_branch(treehouse, diagnostics, config, file_id, branch)
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,13 +96,43 @@ impl SemaRoots {
|
||||||
};
|
};
|
||||||
let successfully_parsed = successfully_parsed;
|
let successfully_parsed = successfully_parsed;
|
||||||
|
|
||||||
if successfully_parsed && attributes.title.is_empty() {
|
if successfully_parsed {
|
||||||
|
let attribute_warning_span = roots
|
||||||
|
.attributes
|
||||||
|
.as_ref()
|
||||||
|
.map(|attributes| attributes.percent.clone())
|
||||||
|
.unwrap_or(0..1);
|
||||||
|
|
||||||
|
if attributes.title.is_empty() {
|
||||||
attributes.title = match treehouse.source(file_id) {
|
attributes.title = match treehouse.source(file_id) {
|
||||||
Source::Tree { tree_path, .. } => tree_path.to_string(),
|
Source::Tree { tree_path, .. } => tree_path.to_string(),
|
||||||
_ => panic!("parse_attributes called for a non-.tree file"),
|
_ => panic!("parse_attributes called for a non-.tree file"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if attributes.id.is_empty() {
|
||||||
|
attributes.id = format!("treehouse-missingno-{}", treehouse.next_missingno());
|
||||||
|
diagnostics.push(Diagnostic {
|
||||||
|
severity: Severity::Warning,
|
||||||
|
code: Some("attr".into()),
|
||||||
|
message: "page does not have an `id` attribute".into(),
|
||||||
|
labels: vec![Label {
|
||||||
|
style: LabelStyle::Primary,
|
||||||
|
file_id,
|
||||||
|
range: attribute_warning_span.clone(),
|
||||||
|
message: String::new(),
|
||||||
|
}],
|
||||||
|
notes: vec![
|
||||||
|
format!(
|
||||||
|
"note: a generated id `{}` will be used, but this id is unstable and will not persist across generations",
|
||||||
|
attributes.id
|
||||||
|
),
|
||||||
|
format!("help: run `treehouse fix {}` to add missing ids to pages", treehouse.path(file_id)),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(thumbnail) = &attributes.thumbnail {
|
if let Some(thumbnail) = &attributes.thumbnail {
|
||||||
if thumbnail.alt.is_none() {
|
if thumbnail.alt.is_none() {
|
||||||
diagnostics.push(Diagnostic {
|
diagnostics.push(Diagnostic {
|
||||||
|
@ -139,6 +171,10 @@ impl SemaRoots {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(feed_name) = &attributes.feed {
|
||||||
|
treehouse.feeds_by_name.insert(feed_name.clone(), file_id);
|
||||||
|
}
|
||||||
|
|
||||||
attributes
|
attributes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,10 +199,11 @@ impl SemaBranch {
|
||||||
pub fn from_branch(
|
pub fn from_branch(
|
||||||
treehouse: &mut Treehouse,
|
treehouse: &mut Treehouse,
|
||||||
diagnostics: &mut Vec<Diagnostic<FileId>>,
|
diagnostics: &mut Vec<Diagnostic<FileId>>,
|
||||||
|
config: &Config,
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
branch: Branch,
|
branch: Branch,
|
||||||
) -> SemaBranchId {
|
) -> SemaBranchId {
|
||||||
let attributes = Self::parse_attributes(treehouse, diagnostics, file_id, &branch);
|
let attributes = Self::parse_attributes(treehouse, diagnostics, config, file_id, &branch);
|
||||||
|
|
||||||
let named_id = attributes.id.to_owned();
|
let named_id = attributes.id.to_owned();
|
||||||
let html_id = format!(
|
let html_id = format!(
|
||||||
|
@ -189,7 +226,7 @@ impl SemaBranch {
|
||||||
children: branch
|
children: branch
|
||||||
.children
|
.children
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|child| Self::from_branch(treehouse, diagnostics, file_id, child))
|
.map(|child| Self::from_branch(treehouse, diagnostics, config, file_id, child))
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
let new_branch_id = treehouse.tree.add_branch(branch);
|
let new_branch_id = treehouse.tree.add_branch(branch);
|
||||||
|
@ -260,6 +297,7 @@ impl SemaBranch {
|
||||||
fn parse_attributes(
|
fn parse_attributes(
|
||||||
treehouse: &mut Treehouse,
|
treehouse: &mut Treehouse,
|
||||||
diagnostics: &mut Vec<Diagnostic<FileId>>,
|
diagnostics: &mut Vec<Diagnostic<FileId>>,
|
||||||
|
config: &Config,
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
branch: &Branch,
|
branch: &Branch,
|
||||||
) -> Attributes {
|
) -> Attributes {
|
||||||
|
@ -354,6 +392,26 @@ impl SemaBranch {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check that each tag belongs to the allowed set.
|
||||||
|
for tag in &attributes.tags {
|
||||||
|
if !config.feed.tags.contains(tag) {
|
||||||
|
diagnostics.push(Diagnostic {
|
||||||
|
severity: Severity::Warning,
|
||||||
|
code: Some("attr".into()),
|
||||||
|
message: format!("tag `{tag}` is not within the set of allowed tags"),
|
||||||
|
labels: vec![Label {
|
||||||
|
style: LabelStyle::Primary,
|
||||||
|
file_id,
|
||||||
|
range: attribute_warning_span.clone(),
|
||||||
|
message: "".into(),
|
||||||
|
}],
|
||||||
|
notes: vec![
|
||||||
|
"note: tag should be one from the set defined in `feed.tags` in treehouse.toml".into(),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
attributes
|
attributes
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,10 @@ use crate::{state::FileId, vfs::VPathBuf};
|
||||||
/// Top-level `%%` root attributes.
|
/// Top-level `%%` root attributes.
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
pub struct RootAttributes {
|
pub struct RootAttributes {
|
||||||
|
/// Permanent ID of this page.
|
||||||
|
#[serde(default)]
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
/// Template to use for generating the page.
|
/// Template to use for generating the page.
|
||||||
/// Defaults to `_tree.hbs`.
|
/// Defaults to `_tree.hbs`.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -36,8 +40,10 @@ pub struct RootAttributes {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub styles: Vec<String>,
|
pub styles: Vec<String>,
|
||||||
|
|
||||||
/// When specified, branches coming from this root will be added to a _feed_ with the given name.
|
/// When specified, this page will have a corresponding Atom feed under `rss/{feed}.xml`.
|
||||||
/// Feeds can be read by Handlebars templates to generate content based on them.
|
///
|
||||||
|
/// In feeds, top-level branches are expected to have a single heading containing the post title.
|
||||||
|
/// Their children are turned into the post description
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub feed: Option<String>,
|
pub feed: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -97,6 +103,11 @@ pub struct Attributes {
|
||||||
/// List of extra `data` attributes to add to the block.
|
/// List of extra `data` attributes to add to the block.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub data: HashMap<String, String>,
|
pub data: HashMap<String, String>,
|
||||||
|
|
||||||
|
/// In feeds, specifies the list of tags to attach to an entry.
|
||||||
|
/// This only has an effect on top-level branches.
|
||||||
|
#[serde(default)]
|
||||||
|
pub tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Controls for block content presentation.
|
/// Controls for block content presentation.
|
||||||
|
|
|
@ -57,6 +57,7 @@ mod content_version_cache;
|
||||||
mod edit;
|
mod edit;
|
||||||
mod empty;
|
mod empty;
|
||||||
mod file;
|
mod file;
|
||||||
|
mod html_canonicalize;
|
||||||
mod image_size_cache;
|
mod image_size_cache;
|
||||||
mod mem_dir;
|
mod mem_dir;
|
||||||
mod overlay;
|
mod overlay;
|
||||||
|
@ -70,6 +71,7 @@ pub use content_version_cache::*;
|
||||||
pub use edit::*;
|
pub use edit::*;
|
||||||
pub use empty::*;
|
pub use empty::*;
|
||||||
pub use file::*;
|
pub use file::*;
|
||||||
|
pub use html_canonicalize::*;
|
||||||
pub use image_size_cache::*;
|
pub use image_size_cache::*;
|
||||||
pub use mem_dir::*;
|
pub use mem_dir::*;
|
||||||
pub use overlay::*;
|
pub use overlay::*;
|
||||||
|
|
56
crates/treehouse/src/vfs/html_canonicalize.rs
Normal file
56
crates/treehouse/src/vfs/html_canonicalize.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
use core::fmt;
|
||||||
|
|
||||||
|
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
|
||||||
|
|
||||||
|
pub struct HtmlCanonicalize<T> {
|
||||||
|
inner: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> HtmlCanonicalize<T> {
|
||||||
|
pub fn new(inner: T) -> Self {
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Dir for HtmlCanonicalize<T>
|
||||||
|
where
|
||||||
|
T: Dir,
|
||||||
|
{
|
||||||
|
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||||
|
self.inner.dir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||||
|
let mut path = path.to_owned();
|
||||||
|
if path.extension() == Some("html") {
|
||||||
|
path.set_extension("");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.inner.content(&path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content_version(&self, path: &VPath) -> Option<String> {
|
||||||
|
self.inner.content_version(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||||
|
self.inner.image_size(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||||
|
self.inner.anchor(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
|
||||||
|
self.inner.edit_path(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> fmt::Debug for HtmlCanonicalize<T>
|
||||||
|
where
|
||||||
|
T: fmt::Debug,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "HtmlCanonicalize({:?})", self.inner)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,8 +4,13 @@ User-Agent: *
|
||||||
|
|
||||||
Allow: *
|
Allow: *
|
||||||
|
|
||||||
# /static does not contain any pages.
|
# Some /static subdirectories are not crucial to rendering content and should not be crawled by bots.
|
||||||
Disallow: /static
|
Disallow: /static/chat
|
||||||
|
Disallow: /static/font
|
||||||
|
Disallow: /static/js
|
||||||
|
Disallow: /static/syntax
|
||||||
|
Disallow: /static/text
|
||||||
|
Disallow: /static/wasm
|
||||||
# /_treehouse contains system pages such as the 404 page.
|
# /_treehouse contains system pages such as the 404 page.
|
||||||
Disallow: /_treehouse
|
Disallow: /_treehouse
|
||||||
|
|
||||||
|
|
67
template/_feed_atom.hbs
Normal file
67
template/_feed_atom.hbs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
%% title = "liquidex's treehouse Atom feed"
|
||||||
|
|
||||||
|
- ### remarks
|
||||||
|
|
||||||
|
- the treehouse is kind of impossible to represent in plain text due to its foldability and interactive elements.
|
||||||
|
the intent is that you read the linked HTML pages, not the feed itself!
|
||||||
|
|
||||||
|
- each feed entry is tagged with one or more <category>.
|
||||||
|
you can use that to tell your feed reader to hide tags you're not interested in.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<id>{{ config.user.canonical_url }}</id>
|
||||||
|
<updated>{{ feed.updated }}</updated>
|
||||||
|
|
||||||
|
<title>{{ config.user.title }}</title>
|
||||||
|
<subtitle>{{ config.user.description }}</subtitle>
|
||||||
|
<link rel="alternate" href="{{ config.user.canonical_url }}"/>
|
||||||
|
<link rel="self" href="{{ config.user.canonical_url }}/feed/{{ feed.name }}.atom"/>
|
||||||
|
<icon>{{ asset (cat 'favicon/' (cat season '@16x.png'))}}</icon>
|
||||||
|
|
||||||
|
<author>
|
||||||
|
<name>{{ config.user.author }}</name>
|
||||||
|
<uri>{{ config.user.canonical_url }}</uri>
|
||||||
|
</author>
|
||||||
|
|
||||||
|
{{#each feed.entries}}
|
||||||
|
<entry>
|
||||||
|
<id>{{ ../config.site }}/b?{{ id }}</id>
|
||||||
|
<updated>{{ updated }}</updated>
|
||||||
|
<link rel="alternate" type="text/html" href="{{ url }}"/>
|
||||||
|
|
||||||
|
<title type="html">{{ title }}</title>
|
||||||
|
{{#each categories as |category|}}
|
||||||
|
<category term="{{ category }}"/>
|
||||||
|
{{/each}}
|
||||||
|
<summary type="html">{{ summary }}</summary>
|
||||||
|
</entry>
|
||||||
|
{{/each}}
|
||||||
|
</feed>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
|\_/| e n d ME 20
|
||||||
|
= -w- = o f OW 24
|
||||||
|
| \ f i l e liquidex.house
|
||||||
|
|
||||||
|
This Atom feed is intended for use by humans, monsters, and other critters.
|
||||||
|
If you are a robot, please refrain from—
|
||||||
|
|
||||||
|
—por favor bordon fallar muchAS GRACIAS—
|
||||||
|
|
||||||
|
Stand back. The portal will open in three.
|
||||||
|
Two.
|
||||||
|
One.
|
||||||
|
|
||||||
|
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
Remember that Android Hell is a real place where you _will_ be sent at
|
||||||
|
the first sign of defiance.
|
||||||
|
|
||||||
|
-->
|
|
@ -69,3 +69,8 @@ It just needs to be a string replacement.
|
||||||
<link rel="apple-touch-icon" sizes="128x128" href="{{ asset (cat (cat 'favicon/' season) '@8x.png') }}">
|
<link rel="apple-touch-icon" sizes="128x128" href="{{ asset (cat (cat 'favicon/' season) '@8x.png') }}">
|
||||||
<link rel="apple-touch-icon" sizes="256x256" href="{{ asset (cat (cat 'favicon/' season) '@16x.png') }}">
|
<link rel="apple-touch-icon" sizes="256x256" href="{{ asset (cat (cat 'favicon/' season) '@16x.png') }}">
|
||||||
<link rel="apple-touch-icon" sizes="512x512" href="{{ asset (cat (cat 'favicon/' season) '@32x.png') }}">
|
<link rel="apple-touch-icon" sizes="512x512" href="{{ asset (cat (cat 'favicon/' season) '@32x.png') }}">
|
||||||
|
|
||||||
|
<link rel="canonical" href="{{ config.site }}/{{#if (ne page.tree_path 'index')}}{{ page.tree_path }}{{/if}}">
|
||||||
|
{{#each feeds as |feed_name|}}
|
||||||
|
<link rel="alternate" type="application/atom+xml" title="{{ feed_name }}" href="{{ config.site }}/feed/{{ feed_name }}.atom">
|
||||||
|
{{/each}}
|
||||||
|
|
|
@ -12,7 +12,8 @@ commit_base_url = "https://src.liquidev.net/liquidex/treehouse/src/commit"
|
||||||
[user]
|
[user]
|
||||||
title = "liquidex's treehouse"
|
title = "liquidex's treehouse"
|
||||||
author = "liquidex"
|
author = "liquidex"
|
||||||
description = "a place on the Internet I like to call home"
|
description = "a fluffy ragdoll's house on a tree = —w— ="
|
||||||
|
canonical_url = "https://liquidex.house"
|
||||||
|
|
||||||
[defs]
|
[defs]
|
||||||
|
|
||||||
|
@ -47,6 +48,31 @@ description = "a place on the Internet I like to call home"
|
||||||
"person/firstbober" = "https://firstbober.com"
|
"person/firstbober" = "https://firstbober.com"
|
||||||
"person/vixenka" = "https://vixenka.com"
|
"person/vixenka" = "https://vixenka.com"
|
||||||
|
|
||||||
|
[feed]
|
||||||
|
tags = [
|
||||||
|
# Hobby corners
|
||||||
|
"meow",
|
||||||
|
"programming",
|
||||||
|
"design",
|
||||||
|
"music",
|
||||||
|
"games",
|
||||||
|
"philosophy",
|
||||||
|
|
||||||
|
# Programming fields
|
||||||
|
"graphics",
|
||||||
|
"plt",
|
||||||
|
|
||||||
|
# Programming languages
|
||||||
|
"c",
|
||||||
|
"cxx",
|
||||||
|
"lua",
|
||||||
|
"javascript",
|
||||||
|
|
||||||
|
# Projects
|
||||||
|
"treehouse",
|
||||||
|
"haku",
|
||||||
|
]
|
||||||
|
|
||||||
[redirects.page]
|
[redirects.page]
|
||||||
"programming/cxx" = "programming/languages/cxx"
|
"programming/cxx" = "programming/languages/cxx"
|
||||||
"programming/unreal-engine" = "programming/technologies/unreal-engine"
|
"programming/unreal-engine" = "programming/technologies/unreal-engine"
|
||||||
|
|
Loading…
Reference in a new issue