add a tagging system to the website

This commit is contained in:
りき萌 2025-08-24 13:18:51 +02:00
parent 701da6bc4b
commit e1b6578b2a
97 changed files with 1025 additions and 979 deletions

3
.ignore Normal file
View file

@ -0,0 +1,3 @@
*.png
*.jpg
*.webp

329
Cargo.lock generated
View file

@ -540,17 +540,6 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "either"
version = "1.13.0"
@ -684,19 +673,6 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "git2"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
dependencies = [
"bitflags 2.6.0",
"libc",
"libgit2-sys",
"log",
"url",
]
[[package]]
name = "glob"
version = "0.3.2"
@ -855,145 +831,6 @@ dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locid_transform"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
dependencies = [
"displaydoc",
"icu_locid",
"icu_locid_transform_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locid_transform_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
[[package]]
name = "icu_normalizer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
[[package]]
name = "icu_properties"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locid_transform",
"icu_properties_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
[[package]]
name = "icu_provider"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
dependencies = [
"displaydoc",
"icu_locid",
"icu_provider_macros",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_provider_macros"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "idna"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "image"
version = "0.25.5"
@ -1134,18 +971,6 @@ dependencies = [
"cc",
]
[[package]]
name = "libgit2-sys"
version = "0.17.0+1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224"
dependencies = [
"cc",
"libc",
"libz-sys",
"pkg-config",
]
[[package]]
name = "libwebp-sys"
version = "0.9.6"
@ -1156,24 +981,6 @@ dependencies = [
"glob",
]
[[package]]
name = "libz-sys"
version = "1.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "litemap"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
[[package]]
name = "lock_api"
version = "0.4.12"
@ -1851,12 +1658,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strsim"
version = "0.11.1"
@ -1886,17 +1687,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "synstructure"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "system-deps"
version = "6.2.2"
@ -1986,16 +1776,6 @@ dependencies = [
"weezl",
]
[[package]]
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tokio"
version = "1.41.1"
@ -2186,7 +1966,6 @@ dependencies = [
"clap",
"codespan-reporting",
"dashmap",
"git2",
"handlebars",
"image",
"indexmap",
@ -2242,29 +2021,6 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "url"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "utf16_iter"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
@ -2288,12 +2044,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version-compare"
version = "0.2.0"
@ -2533,48 +2283,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "write16"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
[[package]]
name = "writeable"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "xmlparser"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
[[package]]
name = "yoke"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerocopy"
version = "0.7.35"
@ -2596,49 +2310,6 @@ dependencies = [
"syn",
]
[[package]]
name = "zerofrom"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerovec"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zune-core"
version = "0.4.12"

View file

@ -1,7 +1,7 @@
[package]
name = "treehouse"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
anyhow = "1.0.75"
@ -13,7 +13,6 @@ 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"] }
@ -31,7 +30,7 @@ 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"
xmlparser = "0.13.6" # for parsing SVG
[profile.dev]
package.webp.opt-level = 3

11
content/_treehouse/404.dj Normal file
View file

@ -0,0 +1,11 @@
title = "404"
+++
Seems like what you're looking for isn't here.\
Perhaps you pasted in an invalid URL, perhaps it has been removed.\
Perhaps it was never written in the first place.
Care to [go back to the index][page:index]?
~Please feel free to report this if you think you've found a bug.~

View file

@ -1,5 +1,7 @@
%% title = "who that! (about me)"
%% id = "b?01J0KRPMV7SS48B64BFCJZK7VQ"
title = "who that! (about me)"
visibility = "Private"
tags = ["all", "meow"]
% id = "01J09B2BZXJ989S2SGWBNZ397C"
+ my name's *riki!*

View file

@ -1,4 +1,6 @@
title = "Iterating on design for code blocks"
id = "b?01K39ZH9DSF8Q13V47V42HNM3B"
tags = ["all", "design"]
+++

View file

@ -1,4 +1,6 @@
%% title = "animations & perception of sluggishness"
%% id = "b?01JEPCD4EPZJ2ACJX9J15ZBFSG"
title = "animations & perception of sluggishness"
tags = ["all", "design"]
% id = "01JEPBVCJXC6NBJZ90JBHNYKGR"
- there's a thing I always do with my computing devices: I make the animations faster than default.

View file

@ -1,4 +1,6 @@
%% title = "on digital textures"
%% id = "b?01HQ8KV8T8GRCVFDJ3EP6QE163"
title = "on digital textures"
tags = ["all", "design"]
% id = "01HQ8JHZ5NP1K7PHW4MJQS65ND"
- this is not about textures in the graphics programming sort of way. this is about textures in the _you can feel it under your fingers_ way

View file

@ -1,4 +1,6 @@
%% title = "design for the free soul"
%% id = "b?01JG6Y6JNRQA4SSK3TEZ0RRTEB"
title = "design for the free soul"
tags = ["all", "design", "shower"]
% id = "01JG6Y2YWD4JF34E64W1GK583J"
- as I'm writing this, I'm sitting in the passenger seat of a car, with my girlfriend driving us home.

View file

@ -1,4 +1,6 @@
%% title = "idea: freeing the hobby corners"
%% id = "b?01JHGYTZN196N51ZBS4MZGHJ73"
title = "idea: freeing the hobby corners"
tags = ["all", "design", "treehouse"]
% id = "01JHGX51XQMBYYQ5VCV427N169"
- also known as, _freehouse_.

View file

@ -1,4 +1,6 @@
%% title = "maybe don't use sidebars on your website"
%% id = "b?01HR9ZTS8RS4VJNJYSNRQYSKHZ"
title = "maybe don't use sidebars on your website"
tags = ["all", "design"]
% id = "01HR9ZTS6PHTK6VRBF7S0M6MZ7"
- imagine if this page had a distracting bunch of text to the right of regular content. or even worse, advertisements.

View file

@ -1,4 +1,6 @@
%% title = "touch panels"
%% id = "b?01JE75N3B0Y9H53TMJJV7MJY9V"
title = "touch panels"
tags = ["all", "design"]
% id = "01JE246DXAKY4V4S49576JG0P9"
- 2024\. it seems like you can't go on without devices that have capacitive touch panels.

14
content/doc.dj Normal file
View file

@ -0,0 +1,14 @@
title = "Document ID"
id = "doc?2025-08-24-doc"
+++
If you've stumbled upon this page, this means you followed the URL stored in the `<id>` field inside an Atom feed `<entry>`.
I can't blame you.
It _is_ a URL after all.
But the post you're looking for isn't here, as the `https://liquidex.house/doc` namespace is reserved for document IDs, and does not resolve to actual documents.
If your feed reader led you here, it is broken, because `<id>`s are *not meant to be followed.*
To read a post, follow the `<link rel="alternate" type="text/html">` link instead!

View file

@ -1,4 +1,6 @@
title = "A string formatting library in 65 lines of C++"
id = "b?01K39HT2MW0JWTP5MNH1CHGV2Y"
tags = ["all", "programming", "cxx"]
+++

View file

@ -1,4 +1,6 @@
title = "furry! —w—"
id = "b?01K05F3E3DN1PY9ZWN98ZE5HVV"
tags = ["all", "shower"]
+++

View file

@ -1,4 +1,6 @@
%% title = "furry! —w—"
%% id = "b?01JCGVBXW42S8G91SGAKYCQZE1"
title = "furry! —w—"
tags = ["all", "shower"]
% id = "01JCGVBXW35X3965J2MJQH1AFY"
- I like fur!

View file

@ -1,4 +1,6 @@
%% title = "reflections on Minecraft"
%% id = "b?01JGSNTVGFX416HFWKFYKR9HZM"
title = "reflections on Minecraft"
tags = ["all", "games"]
% id = "01JGRJPSTR8SPPTVCGCP26YGF7"
- Minecraft has been with me for around 14 years by now; a majority of my life! I first started playing around beta `1.3_01`.

View file

@ -1,4 +1,6 @@
title = "header files are cool, actually"
id = "b?01K1Y3G5N1KGCN1E9B36QTYMSZ"
tags = ["all", "programming", "c", "cxx"]
+++

View file

@ -1,5 +1,5 @@
title = "riki's house"
include_feed = { name = "new", title = "Blog" }
include_feed = { tag = "all", title = "Blog" }
+++

View file

@ -1,4 +1,6 @@
%% title = "Radiohead - A Moon Shaped Pool"
%% id = "b?01H9JB094CHQHEYEBKBCHFFCKG"
title = "Radiohead - A Moon Shaped Pool"
tags = ["all", "music"]
% id = "01H9JB094C60Q6DR1E7SME7Y0N"
- buy: [Bandcamp](https://radiohead.bandcamp.com/album/a-moon-shaped-pool)

View file

@ -1,4 +1,6 @@
%% title = "Aphex Twin - Blue Calx"
%% id = "b?01JCY18RY6D3CXHQ0JQ56BZH60"
title = "Aphex Twin - Blue Calx"
tags = ["all", "music"]
% id = "01JCY18RY6T32XGTM21T6581Z5"
- overcoming [fear of the unknown][page:philosophy/fear-of-the-unknown], episode 1.

View file

@ -1,4 +1,6 @@
%% title = "the ListenBrainz data set"
%% id = "b?01J73BSWA15KHTQ21T0S14NZW0"
title = "the ListenBrainz data set"
tags = ["all", "programming", "music"]
% id = "01J73BSW7VS69RQ84XWRAEYEHV"
- I've been using [ListenBrainz](https://listenbrainz.org) as my primary way of keeping track of my listens for a couple years now---I

View file

@ -1,4 +1,6 @@
%% title = "the curious case of Amon Tobin's Creatures"
%% id = "b?01JBAGZAZ30K443QYPK0XBNZWM"
title = "the curious case of Amon Tobin's Creatures"
tags = ["all", "music"]
% id = "01JBAGT450TPYYB0W5Q3RB017W"
- I bought Amon Tobin's [Bricolage](https://amontobin.bandcamp.com/album/bricolage) way back in September last year, but only today noticed that my copy seemed to be... corrupted?

View file

@ -1,4 +1,6 @@
%% title = "Kettel - Dubio"
%% id = "b?01JQYKYRF2RRY2DEV2Z8MSYJ4F"
title = "Kettel - Dubio"
tags = ["all", "music"]
% id = "01JQYKNE7YW4VEQQZ9P3ZQEHRC"
- as _Dubio_ opens with its title track, I am taken into the mountains.

View file

@ -1,4 +1,6 @@
%% title = "The Flashbulb - Flacks / aBliss"
%% id = "b?01JHXVRT2HR6TXC2V9JG2XTZVB"
title = "The Flashbulb - Flacks / aBliss"
tags = ["all", "music"]
% id = "01JHXV738MT1DF4T4DQWJ16PED"
- walking in the mountains, you see flags waving in the distance...

View file

@ -1,4 +1,6 @@
%% title = "Oneohtrix Point Never - I Don't Love Me Anymore"
%% id = "b?01J8ZP2EG9TM8320R9E3K1GQEC"
title = "Oneohtrix Point Never - I Don't Love Me Anymore"
tags = ["all", "music"]
% id = "01J8ZKEB7JNVWZY0CDR7Y3HRA6"
+ a track from Oneohtrix Point Never's _Magic Oneohtrix Point Never_. [bandcamp link](https://oneohtrixpointnever.bandcamp.com/album/magic-oneohtrix-point-never)

View file

@ -1,4 +1,6 @@
%% title = "Telefon Tel Aviv - Map of What Is Effortless"
%% id = "b?01H9R1NKBB7NCQM8GJ3907P7F7"
title = "Telefon Tel Aviv - Map of What Is Effortless"
tags = ["all", "music"]
% id = "01H9R1NKBBVMRYPK024ED8P1CH"
- buy: [Bandcamp](https://telefon-tel-aviv.bandcamp.com/album/map-of-what-is-effortless)

View file

@ -1,4 +1,6 @@
%% title = "Radiohead - OK Computer"
%% id = "b?01H969NN1ACXG26T2NHCM7BHYY"
title = "Radiohead - OK Computer"
tags = ["all", "music"]
% id = "01H969NN1ACMHAYB8QX7SNCVJC"
- buy: [Bandcamp](https://radiohead.bandcamp.com/album/ok-computer)

View file

@ -1,4 +1,6 @@
%% title = "Aphex Twin - Syro"
%% id = "b?01H9DQNG9ARCX91Z15MWTB0A6B"
title = "Aphex Twin - Syro"
tags = ["all", "music"]
% id = "01H9DQNG9AYRET7KY8SBXFCH98"
- buy: [Bandcamp](https://aphextwin.bandcamp.com/album/syro)

View file

@ -1,4 +1,6 @@
%% title = "Floating Points - Tilt Shift / Ablaze"
%% id = "b?01JK5SN2ZBDZTFZ27J3KNT4SQV"
title = "Floating Points - Tilt Shift / Ablaze"
tags = ["all", "music"]
% id = "01JK5SN2WBQ1Y4SR1BQGSKRAKG"
- the two last tracks on Floating Points's latest album, [_Cascade_](https://floatingpoints.bandcamp.com/album/cascade).

View file

@ -1,4 +1,6 @@
%% title = "fear of the unknown"
%% id = "b?01JCGVBXW39D0GJTZY53A25TXT"
title = "fear of the unknown"
tags = ["all", "shower"]
% id = "01JCGVBXW41374C1TEEYH8245B"
+ no matter how long you live, you will always live in fear.

View file

@ -1,4 +1,6 @@
%% title = "hedonic treadmill"
%% id = "b?01HFYZKREV93QY3K7KNNFSW90H"
title = "hedonic treadmill"
tags = ["all", "shower"]
% id = "01HFYZKREVV2CX1GYJQ18BJS78"
- a concept that's incredibly useful in keeping it cool

View file

@ -1,4 +1,6 @@
%% title = '"I build things"'
%% id = "b?01JBAK3T1ZSSTHRN6TTSXXBAKK"
title = '"I build things"'
tags = ["all", "shower"]
% id = "01JBAK3T1ZMTJS5A9Z5VFFQR0V"
- aka how to sound like the most boring software developer ever.

View file

@ -1,4 +1,6 @@
%% title = "in wisdom you become old"
%% id = "b?01J1Q8SBGFYKDATVF85XYWMSV2"
title = "in wisdom you become old"
tags = ["all", "shower"]
% id = "01J1Q8SBGFNS3ZSSA1QXN83SMH"
+ this is kind of weird, but recently I have noticed I no longer keep up with slang

View file

@ -1,4 +1,6 @@
%% title = "my weird stim: writing with light streaks"
%% id = "b?01JG55SF4FRRF9RDW9KMYHAECF"
title = "my weird stim: writing with light streaks"
tags = ["all", "shower"]
% id = "01JG55KYCYD0CHXQ582XTAHMP6"
- a long time ago my brain built up a [stim][] that it likes to perform when bored: I draw words with my eyes.

View file

@ -1,4 +1,6 @@
%% title = "on nicknames"
%% id = "b?01JBWHXTMKYRW5XXDJG9VQNF5E"
title = "on nicknames"
tags = ["all", "shower"]
% id = "01JBWHXTMKTDGDBATFM9H27KZK"
- my given name is Gabriel.

View file

@ -1,4 +1,6 @@
%% title = "on responsibility & conformity"
%% id = "b?01JDJ0RH4DJCNS7TPCRZHRPSRF"
title = "on responsibility & conformity"
tags = ["all", "shower"]
% id = "01JDJ0RH4DTY60R2Y0Y3J9FH0W"
- most of us probably can't imagine holding a company talk on a tech conference while wearing a fursuit.

View file

@ -1,4 +1,6 @@
%% title = "nickname change!"
%% id = "b?01JGXQ5E8DHAEYFTHG0RB86VNE"
title = "nickname change!"
tags = ["all", "meow", "shower"]
% id = "01JGXMY094E9AGM67T7EXAW25Z"
- I'm changing my nickname from liquidex to *riki*!

View file

@ -1,4 +1,6 @@
%% title = "just shut up sometimes"
%% id = "b?01HREVZNAH3PMMN29C6HNFQ7P9"
title = "just shut up sometimes"
tags = ["all", "shower"]
% id = "01HREVZNAH8XFRMVB9G58TPV8R"
- I just caught myself writing a few paragraphs long comment on [Lobsters](https://lobste.rs) without really understanding what the author of the comment meant in the broader context of the discussion,

View file

@ -1,4 +1,6 @@
%% title = "Advent of Code feels"
%% id = "b?01JDZKAP3KT4AD36F6HPJTEM4Z"
title = "Advent of Code feels"
tags = ["all", "programming", "shower"]
% id = "01JDZKMFQZS2VVNQ3R9WDS81TS"
- I wonder if I'm the only one who can never get into [Advent of Code](https://adventofcode.com).

View file

@ -1,4 +1,6 @@
%% title = "not quite buildless"
%% id = "b?01J7C1KBZ58BR21AVFA1PMWV68"
title = "not quite buildless"
tags = ["all", "programming", "treehouse"]
% id = "01J7BYKQGYPF50050K67N2MP1G"
- ...buildsome?

View file

@ -1,4 +1,6 @@
%% title = "prefix matches with C strings"
%% id = "b?01JCGAM553TJJCEJ96ADEWETQC"
title = "prefix matches with C strings"
tags = ["all", "programming", "c", "cxx"]
% id = "01JCGAM55352EF247HZ358BAJ8"
- one thing I realised while reading some code at work that worked with C strings: it's surprising how easy it is to match a prefix on a C string.

View file

@ -1,4 +1,6 @@
%% title = "C++ without Classes"
%% id = "b?01JN72M4EXSR6M120A6CW2D0X7"
title = "C++ without Classes"
tags = ["all", "programming", "cxx"]
% id = "01JN6EFWCPWY65AQ27BGFDT2DV"
- one thing I often see in people's code in C++ is putting _everything_ into a class.

View file

@ -1,4 +1,6 @@
%% title = "C++ syntactic pitfall: access modifiers as labels"
%% id = "b?01H9R1KJES6FC89NMC7J0FJT6P"
title = "C++ syntactic pitfall: access modifiers as labels"
tags = ["all", "programming", "cxx"]
% id = "01H9R1KJES39Z6RBCKY4E71PYD"
- although Java and C#'s approach to symbol privacy may be verbose, it has one great advantage: it is stateless.

View file

@ -1,4 +1,6 @@
%% title = "freeing C memory automatically using `std::unique_ptr` and `std::shared_ptr`"
%% id = "b?01J0VNHPTRNC1HFXAQ790Y1EZB"
title = "freeing C memory automatically using `std::unique_ptr` and `std::shared_ptr`"
tags = ["all", "programming", "cxx"]
% id = "01J0VN48B2E9WZ4QW0X69N2KB8"
- say you need to interface with a C library such as SDL2 in your C++ code

View file

@ -1,5 +1,7 @@
%% title = "haku - writing a little programming language for fun"
%% id = "b?01J4J5N6WZQ03VTB3TZ51J7QZK"
title = "haku - writing a little programming language for fun"
scripts = ["treehouse/vendor/codejar.js", "treehouse/components/literate-programming.js"]
tags = ["all", "programming", "plt"]
% id = "01J3K8A0D1774SFDPKDK5G9GPV"
- I've had this idea on my mind as of late, of a little pure functional programming language that would run in your browser.

View file

@ -1,8 +1,10 @@
%% title = "JavaScript is not as bad as people make it out to be"
%% id = "b?01J293BFEBT15W0Z3XF1HEFGZT"
title = "JavaScript is not as bad as people make it out to be"
scripts = [
"treehouse/components/literate-programming.js",
"treehouse/vendor/codejar.js",
]
tags = ["all", "programming", "javascript", "plt"]
% id = "01J291S06DS12DCFTNKJ27BNSQ"
- _ooh I'm sure this one is gonna be really controversial but here I go_

View file

@ -1,4 +1,6 @@
%% title = "Lua - a scripting language you can like"
%% id = "b?01HRG3VN091V715A8T54QK5PVX"
title = "Lua - a scripting language you can like"
tags = ["all", "programming", "plt", "lua"]
% id = "01HRG2RJC1BATZJSGSSSF1XNFZ"
- TODO: this page could really use an interactive Lua interpreter. can we have that?

View file

@ -1,4 +1,6 @@
title = "Classes in Lua"
id = "b?01JKKQZRSG5ZRNH530D75E2660"
tags = ["all", "programming", "lua"]
+++

View file

@ -1,4 +1,6 @@
%% title = "places, or what is up with `*x` not always meaning the same thing in different contexts"
%% id = "b?01HY5R1ZW2PYZSSP2J2KAA23DA"
title = "places, or what is up with `*x` not always meaning the same thing in different contexts"
tags = ["all", "programming", "c", "cxx", "plt"]
% id = "01HY5R1ZV9DD7BV0F66Y0DHAEA"
- 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.

View file

@ -1,4 +1,6 @@
%% title = "on changing the Firefox New Tab, and software freedom"
%% id = "b?01JX0GYB1D4W3A6FRPBG738N4F"
title = "on changing the Firefox New Tab, and software freedom"
tags = ["all", "programming"]
% id = "01JX0G5CN5H8S7S3M4619R112A"
- the date is 4th of June, 2025.

View file

@ -1,4 +1,6 @@
%% title = "OR-types"
%% id = "b?01HTWNETT2S5NSBF3QR4HYA7HN"
title = "OR-types"
tags = ["all", "programming", "plt"]
% id = "01HTWN4XAD7C41X8XKRBFZMHJ8"
- last night I couldn't fall asleep because I was thinking how sites like [Anilist](https://anilist.co) implement their tag-based search systems.

View file

@ -1,4 +1,6 @@
%% title = "systems are just a bunch of code"
%% id = "b?01HV1DGFHZ65GJVQRSREKR67J9"
title = "systems are just a bunch of code"
tags = ["all", "programming", "shower"]
% id = "01HV1DGFGNV3DXD8A3CW2J4RZP"
- often enough I see people scared to dive deep into the internals of their favorite technologies

View file

@ -1,9 +1,11 @@
%% title = "tairu - an interactive exploration of 2D autotiling techniques"
%% id = "b?01HQ6G30PTVT5H0Z04VVRHEZQF"
title = "tairu - an interactive exploration of 2D autotiling techniques"
scripts = [
"treehouse/components/literate-programming.js",
"treehouse/vendor/codejar.js",
]
styles = ["page/tairu.css"]
tags = ["all", "programming", "graphics", "javascript"]
% id = "01HPD4XQPWM8ECT2QM6AT9YRWB"
- I remember since my early days doing programming, I've been interested in how games like Terraria handle automatically tiling their terrain.

View file

@ -1,4 +1,6 @@
%% title = "composable virtual file systems"
%% id = "b?01JDJJSEWASRWJGKMBNYMFD9B5"
title = "composable virtual file systems"
tags = ["programming", "treehouse"]
% id = "01JDJGVC7BRZDSYTZCH0S357B8"
- you know what I hate?

View file

@ -1,4 +1,6 @@
title = "Requiem for a Fractal Forest"
id = "b?01K02XZTW3VYKX0Q5NZ17NRVTF"
tags = ["all", "treehouse", "design"]
+++

7
content/tag/c.dj Normal file
View file

@ -0,0 +1,7 @@
title = "#c"
include_feed = { tag = "c", title = "Posts" }
+++
Programming in C.\
Also see [#cpp][page:tags/cpp].

24
content/tag/cxx.dj Normal file
View file

@ -0,0 +1,24 @@
title = "#cxx"
include_feed = { tag = "cxx", title = "Posts" }
+++
Programming in C++.
---
A lot of people don't like C++, and I can't blame them for it.
I don't love it too.
It's just that it's currently the most pragmatic choice for any sort of multi-domain program which follows [the Philosophy][page:tag/programming].
While most C++ code I write is very C-like, I don't limit myself to C, because C++ is a lot more pleasant to program in.
Just the addition of namespaces, `enum class`, references, parsimonious use of templates, and operator overloading, is enough of an improvement over C that I generally tend to prefer C++ for solving my problems.
C++ remains the most pragmatic choice for most long-term projects while [Odin](https://odin-lang.org) and [Zig](https://ziglang.org) are still cooking.
I can rely on the language not changing under my feet, remaining stable and portable for many years to come, with a library ecosystem following a similar philosophy.
Places of unpleasantness in C++ for me include: classes and encapsulation, complicated template metaprogramming, the entire `std` namespace, lack of enum-indexed arrays, and also the Unreal Engine.
~...Seriously though, why does _nobody_ but Odin and Nim implement enum-indexed arrays?\
What is wrong with you people.~

52
content/tag/design.dj Normal file
View file

@ -0,0 +1,52 @@
title = "#design"
include_feed = { tag = "design", title = "Posts" }
+++
Design.
User interface design.\
User experience design.\
Product design.\
Graphic design.
Anything encompassing _design_, by the very definition of the word, goes into this tag.
I decided to start writing about design on this blog, because I like pondering on electronic interfaces.
They're what connects machine to human, and I find it fascinating to see how other people view that very form of art.
But the act of _design_ is a lot wider than just that, which is why you may also find more philosophical musings here.
About anything with _intent_, _thoughtfulness_ behind it.
Naturally, I practice it too.
This website, including its colour palette, layout, illustrations, iconography, is entirely designed by me.
There's also the aspect of [design in programming][page:tag/programming], which goes under its own dedicated tag `#programming` (but `#design` and `#programming` overlap sometimes).
---
As a designer, I value designs which get out of my way, and let me get the thing done.
Not by virtue of minimalism, quite the opposite, actually.
Sometimes minimalism is appropriate, other times it isn't.
In professional software, the last thing I want to see is a hamburger menu.
On a blog, the last thing I want to see is [a sidebar distracting me from the post][page:design/sidebars].
Colourful icons are ugly, but are easier to read than monochrome equivalents.
Bevels, embosses, and drop shadows play an important role in communicating which elements are interactable, and how.
Do not omit them.
Google's products are the opposite of good design.
They look and act like toys for 5-year olds, rather than products for professionals.
Anyone who doesn't respect `@media (prefers-reduced-motion: reduce)` does not deserve my respect.
Including Google.
Dark mode toggled through JavaScript and user settings is bad user experience.
There's a CSS media query for querying dark mode, and it exists for a reason.
_Use it._\
If your light mode looks so bad that system light mode users want to switch away from it, rethink your life choices.
Yadda yadda.
I could go on forever, and you could disagree on any of my points.

16
content/tag/games.dj Normal file
View file

@ -0,0 +1,16 @@
title = "#games"
include_feed = { tag = "games", title = "Posts" }
+++
*Gaming.*
---
I don't really write much about video games, mostly because the games I _really_ get into, I usually get so immersed I can't get out and do anything else.
Such as writing a post.
But if there's a 2nd hobby after programming that I'm really into, it is video games.
Maybe someday I will begin writing up more of my thoughts.
There sure are lots of games I love, and would like to give a tribute to.

8
content/tag/graphics.dj Normal file
View file

@ -0,0 +1,8 @@
title = "#graphics"
include_feed = { tag = "graphics", title = "Posts" }
+++
Graphics programming, and graphics programming _only_.
For graphic design, go to [#design][page:tag/design].

26
content/tag/javascript.dj Normal file
View file

@ -0,0 +1,26 @@
title = "#javascript"
include_feed = { tag = "javascript", title = "Posts" }
+++
Programming in JavaScript.
---
Despite my attraction to lower level programming, I sometimes find myself programming in JavaScript.
It's a decent workhorse language that powers most of the modern web.
Its position as The Language of the Web makes it incredibly useful to know as a modern-day programmer, because sooner or later you'll probably find yourself in a spot where you'll say "gee, I wish I had a way to cobble together a quick and dirty tool with a UI."
It's the one language I keep coming back to, because it's the one language I cannot replace with something that follows [the Philosophy][page:tag/programming].
(No, WebAssembly is not an option.
Have you ever actually tried using it?)
I don't love its design.
I think [Lua][page:tag/lua] is a much more elegant programming language.
But I like it for what it can do for me out of the box.
---
This website purposefully avoids using JavaScript where possible though, because it would be a waste of your computer's resources.
Plain HTML is perfectly fine for most of its functionality.

21
content/tag/lua.dj Normal file
View file

@ -0,0 +1,21 @@
title = "#lua"
include_feed = { tag = "lua", title = "Posts" }
+++
Programming in Lua (_riki_th edition.)
---
I love Lua.
It's a wonderful little scripting language.
I don't generally use Lua in my own projects nowadays, because it doesn't really follow [the Philosophy][page:tag/programming], but it's a wonderful little tool to have in your belt.
Knowing Lua gives you access to tools like [LÖVE](https://love2d.org), which is a nice little audiovisual framework that is executed in an incredibly charming way.
And also the plethora of software which uses Lua for scripting, such as [Neovim](https://neovim.io).
Lua is also [a poster child of brilliant language design][page:programming/lua].
Despite a lot of its archaisms, the language design is beautifully elegant, with each piece fitting neatly into a cohesive whole.
Not to mention its implementation being famously fast for a bytecode interpreter, with [a lot of pieces](https://lua.org/doc/jucs05.pdf) that I borrowed into my own interpreted languages, too.

10
content/tag/meow.dj Normal file
View file

@ -0,0 +1,10 @@
title = "#meow"
include_feed = { tag = "meow", title = "Pages" }
+++
Meowowowow
---
It's called "meow" because it's about *me*-yow!~

28
content/tag/music.dj Normal file
View file

@ -0,0 +1,28 @@
title = "#music"
include_feed = { tag = "music", title = "Posts" }
+++
Whatever---yeah music, music.
[Without the barriers.](https://www.youtube.com/watch?v=l5ufPTvBmHU){.secret}
---
I've been addicted to music for as long as I remember.
And music has always been with me, always been there for me, no matter where I went, or how deep in shambles I was.
Always there, always comforting, as I was\
[Always breaking, always healing.](https://www.youtube.com/watch?v=k0lrgMplH58){.secret}
I owe a great lot to the artists whose music has kept me company all these years.
It is through these blog posts, (as well as direct support through buying there albums), I hope I can give them back a little something precious: a piece of my imagination, through writing.
Something in return for all the emotions they managed to spark within me.\
Something in return for the years of resonance with my heart and soul.
{style="text-align: right;"}
for Her\
my Guardian Angel\
Music

21
content/tag/plt.dj Normal file
View file

@ -0,0 +1,21 @@
title = "#plt"
include_feed = { tag = "plt", title = "Posts" }
+++
Programming language theory and design.
---
One of the more interesting fields of computer science, I believe.
I've been fascinated with programming languages ever since I was a little riki.
There's always been something magical about writing special words into a computer, and the computer being able to _interpret_ them.
So with my tendency for digging deep into any topic I'm interested in, it was only natural that at some point, I started designing and writing those programming languages myself.
An important thing of note is that I'm not an academic.
I'm a practicioner and a humanist.
Therefore, most of my programming language knowledge lives on the more human, design side of things, rather than the theoretical, mathematical side.
I know a lot of the basics behind the theory of it all, but I'm by no means on the cutting edge of language theory and computer science.

View file

@ -0,0 +1,74 @@
title = "#programming"
include_feed = { tag = "programming", title = "Pages" }
+++
Honestly my favourite thing to do.
I've been programming for over 10 years now, and there's no sign of it stopping.
Most of the posts on my website are about programming because of this.
I actually spend so much time programming, I don't have much time for other hobbies!
~It does get kind of sad when you think about it though. When folks around you talk about all the movies or games you didn't see because you were looking at monospaced letters in a text editor...~
---
If I started to describe my whole programming philosophy here, this page would be longer than your favourite roll of toilet paper.
So instead, here are some opinions you may find yourself disagreeing with, given without any further context.
Just so that you can see what sort of programmer I am.
- I write very direct, procedural code.
My programming style is about functions, data structures those functions manipulate, function capabilities expressed through arguments, and encapsulation via modules composed of those things.
- The easier your data is to inspect, the less code you have to write to accomplish a task.
Encapsulation meddles with that directly and prevents direct inspection.
Therefore, encapsulation should be limited mostly to cases where your system interacts with the "outside world," whatever that might mean in your domain of choice.
- Object-oriented programming was [originally meant for simulating distributed systems](https://www.youtube.com/watch?v=wo84LFzx5nI), and it should have stayed in that domain.
The number of invalid state bugs object-oriented programming has inflicted on me in my time as a programmer is miserable.
- I think in groups of things, rather than single elements.
It simplifies reasoning about your program and reduces the number of invalid states (including errors related to memory mismanagement.)
- I value reliability in software very highly.
I like it when I can rely on my software.
For it not to break under its own weight, or pull the rug from under my feet.
This refers both to reliability in terms of the end user side (crashes, bugs, needless UI changes), as well as the programmer side (churn, breaking API changes).
- I prefer code custom-built to a project's needs.
Home-grown game engines are great, and are how you push the industry forward.
And [a little copying is better than a little dependency](https://www.youtube.com/watch?v=PAAkCSZUG1c&t=568s).
Shame on the programmers who choose Unity or Unreal for their game and make my computer sound like a rocket engine.
- I like my software to be fast.
There's too little time in life to be spent [waiting for your tools to load](https://www.unrealengine.com/).
Likewise, I like it when my software _builds_ fast.
There's too little time in life to be spent [waiting for your game code to build and reload](https://www.unrealengine.com/).
Fast software makes it easy to stay in the flow as a creative person.
I think this is something the industry doesn't value enough.
When a piece of software causes you to wait and switch context, it should not be considered a good piece of software.
I rarely have to think about performance though, because software adhering to the rest of my philosophy is generally fast enough not to cause a problem.
It's likely these values will change or grow over time.
I build wisdom slowly, as all of us do.
---
I don't boast about my programming projects very hard online, because I don't seek clout.
I build them for myself and my friends to enjoy, and that is more than enough to satisfy me.
But in case you're curious, here's what I'm currently working on:
- *@@@@*, an untitled, unannounced video game.
- [*rakugaki*][def:rkgk/repo], an online multiplayer painting app with programmable brushes.
- this website!
Any other project you run across from me is probably dead.
Please let it rest in peace and do not resurrect it.
You software necromancer. >w<

19
content/tag/shower.dj Normal file
View file

@ -0,0 +1,19 @@
title = "#shower"
include_feed = { tag = "shower", title = "Posts" }
+++
Shower thoughts.
You didn't think I meant anything weird with that name, did you?
---
Under this tag you will find assorted philosophical thoughts, musings, and everything in between.
Reflections on myself, reality around me, growing up.
Rants, too.
By that, perhaps you could call this the most existential section of the website.
Or perhaps not.
Who knows?

8
content/tag/treehouse.dj Normal file
View file

@ -0,0 +1,8 @@
title = "#treehouse"
include_feed = { tag = "treehouse", title = "Posts" }
+++
`treehouse` is the piece of software that powers riki.house.
This tag aggregates posts related to this website and the tech behind it.

20
content/treehouse/cmd.dj Normal file
View file

@ -0,0 +1,20 @@
title = "Command line"
+++
Press `<kbd>:</kbd>`{=html} or the little `:|` icon in the footer to open the command line.
Type in your command, then press `<kbd>Enter</kbd>`{=html} to run it.
`<kbd>Esc</kbd>`{=html} closes the command line, `<kbd>Tab</kbd>`{=html} cycles through suggestions.
You may also use the mouse (or your touchscreen) to close the command line or pick a suggestion from the list.
Unknown commands to not do anything.
Known commands usually result in immediate feedback.
## riki's commentary
The command line used to be the way to access the news feed (`:new` command), but this has been removed, as the main page serves the same purpose with extra features.
Currently the command line is primarily used for housing developer tools, which are not available in production builds.

View file

@ -1,6 +1,5 @@
%% title = "a curated feed of updates to the house"
styles = ["new.css"]
feed = "new"
visibility = "Private"
% id = "01JCGWPM6T73PAC5Q8YHPBEAA1"
@ -338,7 +337,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!
% id = "01J4J5N6WZQ03VTB3TZ51J7QZK"
tags = ["programming", "plt", "haku"]
tags = ["programming", "plt"]
- ### [haku - writing a little programming language for fun][page:programming/blog/haku]
% id = "01J4J5N6WZQ1316WKDXB1M5W6E"
@ -380,7 +379,7 @@ if you've been wondering what I've been up to, you've come to the right place.
% id = "01J0KRPMV7SS48B64BFCJZK7VQ"
tags = ["meow"]
- ### [about me (version 2)][page:about]
- ### [about me (version 2)][page:about/v2]
% id = "01J0KRPMV73K71D3QXFQ3GNY2N"
- it's updatin' time! I took some time to clean up old pages and update my _about me_.\

138
src/doc.rs Normal file
View file

@ -0,0 +1,138 @@
use chrono::{DateTime, Utc};
use codespan_reporting::diagnostic::{Diagnostic, Label};
use serde::Deserialize;
use crate::{
state::{FileId, TomlError, Treehouse, toml_error_to_diagnostic},
tree::attributes::{Picture, timestamp_from_id},
};
#[derive(Debug, Clone)]
pub struct Doc {
pub attributes: Attributes,
pub text: String,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Attributes {
/// Template to use for generating the page.
/// Defaults to `_tree.hbs`.
#[serde(default)]
pub template: Option<String>,
/// The unique ID of the doc.
/// Required to appear in feeds.
///
/// - New format: `doc?{date}-{name}`, where `{date}` is a `YYYY-MM-DD` date, and `{name}` is
/// the filename of the document (or otherwise a unique name which doesn't conflict with docs
/// made that day.)
/// - Old format: `b?{ulid}`, where `{ulid}` is a ULID.
/// This follows the format of branches.
#[serde(default)]
pub id: String,
/// Title of the page.
/// The only necessary field.
/// Unlike tree pages, doc pages always have titles.
pub title: String,
/// Tags assigned to the document.
/// Generally, you want to assign public documents to #all for them to show up on the front page.
#[serde(default)]
pub tags: Vec<String>,
/// Timestamp when the document was last updated.
/// Required for inclusion in feeds.
/// For pages with old style IDs, this is inferred from the ID.
#[serde(default)]
pub updated: Option<DateTime<Utc>>,
/// ID of picture attached to the page, to be used as a thumbnail.
#[serde(default)]
pub thumbnail: Option<Picture>,
/// Additional scripts to load into to the page.
/// These are relative to the /static/js directory.
#[serde(default)]
pub scripts: Vec<String>,
/// Additional styles to load into to the page.
/// These are relative to the /static/css directory.
#[serde(default)]
pub styles: Vec<String>,
/// If not `None`, the page will get an additional 'feed' field in template data, containing
/// a feed of pages with the specified tag.
#[serde(default)]
pub include_feed: Option<IncludeFeed>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct IncludeFeed {
/// The tag to look for.
pub tag: String,
/// The title of the feed shown on the page.
pub title: String,
}
impl Doc {
pub fn parse(treehouse: &mut Treehouse, file_id: FileId) -> (Doc, Vec<Diagnostic<FileId>>) {
let mut diagnostics = vec![];
let source = treehouse.source(file_id).input();
let (front_matter, text) = source.split_once("+++").unwrap_or(("", source));
let attributes_span = 0..front_matter.len();
let mut attributes: Attributes =
toml_edit::de::from_str(front_matter).unwrap_or_else(|error| {
diagnostics.push(toml_error_to_diagnostic(TomlError {
message: error.message().to_owned(),
span: error.span(),
file_id,
input_range: attributes_span.clone(),
}));
Attributes::default()
});
// Infer attributes
if let Some(branch_id) = attributes.id.strip_prefix("b?")
&& let Some(timestamp) = timestamp_from_id(branch_id)
{
attributes.updated = Some(timestamp);
}
// Emit warnings
if !attributes.tags.is_empty() {
if attributes.id.is_empty() {
diagnostics.push(
Diagnostic::warning()
.with_code("attr")
.with_message("doc is tagged but missing id attribute")
.with_labels(vec![Label::primary(file_id, attributes_span.clone())])
.with_notes(vec!["id is required for showing up in feeds".into()]),
);
} else if attributes.updated.is_none() {
diagnostics.push(
Diagnostic::warning()
.with_code("attr")
.with_message("doc is tagged but missing updated attribute")
.with_labels(vec![Label::primary(file_id, attributes_span.clone())])
.with_notes(vec![
"updated attribute is required for showing up in feeds".into(),
]),
);
}
}
(
Doc {
attributes,
text: text.to_owned(),
},
diagnostics,
)
}
}

59
src/feed.rs Normal file
View file

@ -0,0 +1,59 @@
use chrono::{DateTime, Utc};
use crate::sources::Sources;
#[derive(Debug, Clone)]
pub struct FeedEntry {
pub id: String,
pub updated: Option<DateTime<Utc>>,
pub url: String,
pub title: String,
pub tags: Vec<String>,
}
pub fn generate(sources: &Sources, tag_name: &str) -> Option<Vec<FeedEntry>> {
let mut entries = vec![];
let tag = sources.treehouse.tags.get(tag_name)?;
for file_id in &tag.files {
if let Some(roots) = sources.treehouse.roots.get(file_id)
&& let Some(id) = roots.attributes.id.clone()
{
entries.push(FeedEntry {
id,
updated: roots.attributes.timestamps.map(|ts| ts.updated),
url: format!(
"{}/{}.tree",
sources.config.site,
sources.treehouse.tree_path(*file_id).unwrap()
),
title: roots.attributes.title.clone(),
tags: roots.attributes.tags.clone(),
});
} else if let Some(doc) = sources.treehouse.docs.get(file_id)
&& !doc.attributes.id.is_empty()
{
entries.push(FeedEntry {
id: doc.attributes.id.clone(),
updated: doc.attributes.updated,
url: format!(
"{}/{}",
sources.config.site,
sources.treehouse.path(*file_id).with_extension("")
),
title: doc.attributes.title.clone(),
tags: doc.attributes.tags.clone(),
});
} else {
unreachable!(
"{file_id:?} registered in tag #{tag_name} is not actually in the treehouse"
);
// Well... either that, or unknown variant.
}
}
entries.sort_by_key(|entry| entry.updated);
entries.reverse();
Some(entries)
}

View file

@ -10,7 +10,7 @@ use std::{ops::ControlFlow, sync::Arc};
use atom::FeedDir;
use chrono::{DateTime, Utc};
use dir_helper::DirHelper;
use handlebars::{handlebars_helper, Handlebars};
use handlebars::{Handlebars, handlebars_helper};
use include_static_helper::IncludeStaticHelper;
use serde::Serialize;
use tracing::{error, info_span, instrument};
@ -26,8 +26,8 @@ use crate::{
},
sources::Sources,
vfs::{
self, layered_dir, AnchoredAtExt, Cd, Content, ContentCache, Dir, DynDir, HtmlCanonicalize,
MemDir, ToDynDir, VPath, VPathBuf,
self, AnchoredAtExt, Cd, Content, ContentCache, Dir, DynDir, HtmlCanonicalize, MemDir,
ToDynDir, VPath, VPathBuf, layered_dir,
},
};
@ -37,7 +37,6 @@ struct BaseTemplateData<'a> {
import_map: String,
season: Option<Season>,
dev: bool,
feeds: Vec<String>,
}
impl<'a> BaseTemplateData<'a> {
@ -48,7 +47,6 @@ impl<'a> BaseTemplateData<'a> {
.expect("import map should be serializable to JSON"),
season: Season::current(),
dev: cfg!(debug_assertions),
feeds: sources.treehouse.feeds_by_name.keys().cloned().collect(),
}
}
}
@ -73,12 +71,12 @@ fn create_handlebars(site: &str, static_: DynDir) -> Handlebars<'static> {
#[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::<Content>(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}");
}
if path.extension() == Some("hbs")
&& let Some(content) = vfs::query::<Content>(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(())

View file

@ -1,6 +1,6 @@
use std::{fmt, sync::Arc};
use anyhow::Context;
use anyhow::{Context, anyhow};
use chrono::{DateTime, Utc};
use handlebars::Handlebars;
use serde::Serialize;
@ -8,10 +8,8 @@ use tracing::{info_span, instrument};
use crate::{
dirs::Dirs,
html::djot::{self, resolve_link},
feed,
sources::Sources,
state::FileId,
tree::{feed, SemaBranchId},
vfs::{self, Content, Dir, Entries, VPath, VPathBuf},
};
@ -38,12 +36,15 @@ impl FeedDir {
fn entries(&self, path: &VPath) -> Vec<VPathBuf> {
if path == VPath::ROOT {
self.sources
let mut entries: Vec<_> = self
.sources
.treehouse
.feeds_by_name
.tags
.keys()
.map(|name| VPathBuf::new(format!("{name}.atom")))
.collect()
.collect();
entries.push(VPathBuf::new("new.atom")); // redirect: new -> all
entries
} else {
vec![]
}
@ -51,21 +52,20 @@ impl FeedDir {
fn content(&self, path: &VPath) -> Option<Content> {
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| {
Content::new(
"application/atom+xml",
generate_or_error(&self.sources, &self.dirs, &self.handlebars, *file_id)
.into(),
)
})
} else {
None
let mut feed_name = path.with_extension("").to_string();
if feed_name == "new" {
feed_name = "all".into(); // redirect: new -> all
}
if self.sources.treehouse.tags.contains_key(&feed_name) {
return Some(Content::new(
"application/atom+xml",
generate_or_error(&self.sources, &self.dirs, &self.handlebars, &feed_name)
.into(),
));
}
}
None
}
}
@ -85,18 +85,16 @@ impl fmt::Debug for FeedDir {
#[derive(Serialize)]
struct Feed {
name: String,
updated: DateTime<Utc>,
entries: Vec<Entry>,
}
#[derive(Serialize)]
struct Entry {
id: String,
updated: DateTime<Utc>,
updated: Option<DateTime<Utc>>,
url: String,
title: String,
categories: Vec<String>,
summary: String,
tags: Vec<String>,
}
#[derive(Serialize)]
@ -104,6 +102,19 @@ struct AtomTemplateData<'a> {
#[serde(flatten)]
base: &'a BaseTemplateData<'a>,
feed: Feed,
updated: DateTime<Utc>,
}
pub fn generate_or_error(
sources: &Sources,
dirs: &Dirs,
handlebars: &Handlebars,
tag_name: &str,
) -> String {
match generate(sources, dirs, handlebars, tag_name) {
Ok(html) => html,
Err(error) => format!("error: {error:?}"),
}
}
#[instrument(name = "atom::generate", skip(sources, handlebars))]
@ -111,20 +122,28 @@ pub fn generate(
sources: &Sources,
dirs: &Dirs,
handlebars: &Handlebars,
file_id: FileId,
tag_name: &str,
) -> anyhow::Result<String> {
let roots = &sources.treehouse.roots[&file_id];
let feed_name = roots.attributes.feed.clone().expect("page must be a feed");
let feed = feed::generate(sources, tag_name).ok_or_else(|| anyhow!("feed does not exist"))?; // should not happen in reality; 404 should be returned
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),
name: tag_name.to_owned(),
entries: feed
.into_iter()
.map(|entry| Entry {
id: entry.id,
updated: entry.updated,
url: entry.url,
title: entry.title,
tags: entry.tags,
})
.collect(),
},
// 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(),
};
let _span = info_span!("handlebars::render").entered();
@ -132,84 +151,3 @@ pub fn generate(
.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 = feed::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 = branch
.attributes
.timestamp()
.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()
}
/// 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>");
}

View file

@ -3,63 +3,23 @@ use std::{
sync::Arc,
};
use anyhow::Context;
use anyhow::{Context, anyhow};
use chrono::{DateTime, Utc};
use handlebars::Handlebars;
use serde::{Deserialize, Serialize};
use serde::Serialize;
use tracing::{error, instrument};
use crate::{
dirs::Dirs,
doc::Doc,
feed,
generate::BaseTemplateData,
html::djot,
sources::Sources,
state::{report_diagnostics, toml_error_to_diagnostic, FileId, TomlError},
tree::{attributes::Picture, feed},
state::FileId,
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<String>,
/// 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<Picture>,
/// Additional scripts to load into to the page.
/// These are relative to the /static/js directory.
#[serde(default)]
scripts: Vec<String>,
/// Additional styles to load into to the page.
/// These are relative to the /static/css directory.
#[serde(default)]
styles: Vec<String>,
/// 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<IncludeFeed>,
}
#[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,
@ -87,8 +47,8 @@ struct Feed {
struct Entry {
title: String,
url: String,
updated: DateTime<Utc>,
categories: Vec<String>,
updated: Option<DateTime<Utc>>,
tags: Vec<String>,
}
#[derive(Serialize)]
@ -117,34 +77,19 @@ impl DocDir {
.treehouse
.files_by_doc_path
.get(&path.with_extension("dj"))
&& let Some(doc) = self.sources.treehouse.docs.get(file_id)
{
let source = self.sources.treehouse.source(*file_id).input();
return Some(Content::new(
"text/html",
self.generate(*file_id, path, source).into_bytes(),
self.generate(*file_id, path, doc).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();
fn generate(&self, file_id: FileId, path: &VPath, doc: &Doc) -> String {
let events: Vec<_> = jotdown::Parser::new(&doc.text).into_offset_iter().collect();
let mut rendered_markup = String::new();
let render_diagnostics = djot::Renderer {
config: &self.sources.config,
@ -155,7 +100,7 @@ impl DocDir {
}
.render(&events, &mut rendered_markup);
let template_name = attributes.template.as_deref().unwrap_or("_doc.hbs");
let template_name = doc.attributes.template.as_deref().unwrap_or("_doc.hbs");
let render_result = self
.handlebars
@ -164,20 +109,20 @@ impl DocDir {
&PageTemplateData {
base: &BaseTemplateData::new(&self.sources),
page: Page {
title: attributes.title,
thumbnail: attributes.thumbnail.map(|pic| Thumbnail {
title: doc.attributes.title.clone(),
thumbnail: doc.attributes.thumbnail.as_ref().map(|pic| Thumbnail {
url: self.sources.config.pic_url(&*self.dirs.pic, &pic.id),
alt: pic.alt,
alt: pic.alt.clone(),
}),
scripts: attributes.scripts,
styles: attributes.styles,
scripts: doc.attributes.scripts.clone(),
styles: doc.attributes.styles.clone(),
tree_path: path.to_string(),
doc: rendered_markup,
feed: attributes.include_feed.and_then(|feed| {
feed: doc.attributes.include_feed.as_ref().and_then(|feed| {
Some(Feed {
title: feed.title,
title: feed.title.clone(),
entries: self
.generate_feed(&feed.name)
.generate_feed(&feed.tag)
.inspect_err(|e| {
error!("generating feed for {path} failed: {e}")
})
@ -194,40 +139,23 @@ impl DocDir {
}
}
fn generate_feed(&self, name: &str) -> anyhow::Result<Vec<Entry>> {
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(),
})
fn generate_feed(&self, tag_name: &str) -> anyhow::Result<Vec<Entry>> {
let feed = feed::generate(&self.sources, tag_name)
.ok_or_else(|| anyhow!("tag #{tag_name} doesn't exist"))?;
Ok(feed
.into_iter()
.map(|entry| Entry {
title: entry.title,
url: entry.url,
updated: entry.updated,
tags: {
let mut tags = entry.tags;
// Don't show the "self" tag in the list.
if let Some(i) = tags.iter().position(|x| x == tag_name) {
tags.remove(i);
}
tags
},
})
.collect())
}

View file

@ -1,106 +0,0 @@
use std::collections::HashMap;
use indexmap::IndexMap;
use tracing::debug;
#[derive(Debug, Default, Clone)]
pub struct History {
// Sorted from newest to oldest.
pub commits: IndexMap<git2::Oid, Commit>,
pub by_page: HashMap<String, PageHistory>,
}
#[derive(Debug, Clone)]
pub struct Commit {
pub summary: String,
pub body: String,
}
#[derive(Debug, Clone, Default)]
pub struct PageHistory {
// Sorted from newest to oldest, so revision 0 is the current version.
// On the website these are sorted differently: 1 is the oldest revision, succeeding numbers are later revisions.
pub revisions: Vec<Revision>,
}
#[derive(Debug, Clone)]
pub struct Revision {
pub commit_oid: git2::Oid,
pub blob_oid: git2::Oid,
}
impl History {
pub fn get(git: &git2::Repository) -> anyhow::Result<Self> {
debug!("reading git history");
let mut history = History::default();
let mut revwalk = git.revwalk()?;
revwalk.push_head()?;
for commit_oid in revwalk {
let commit_oid = commit_oid?;
let commit = git.find_commit(commit_oid)?;
history.commits.insert(
commit_oid,
Commit {
summary: String::from_utf8_lossy(commit.summary_bytes().unwrap_or(&[]))
.into_owned(),
body: String::from_utf8_lossy(commit.body_bytes().unwrap_or(&[])).into_owned(),
},
);
let tree = commit.tree()?;
tree.walk(git2::TreeWalkMode::PreOrder, |parent_path, entry| {
if parent_path.is_empty() && entry.name() != Some("content") {
// This is content-only history, so skip all directories that don't contain content.
git2::TreeWalkResult::Skip
} else if entry.kind() == Some(git2::ObjectType::Blob)
&& entry.name().is_some_and(|name| name.ends_with(".tree"))
{
let path = format!(
"{parent_path}{}",
String::from_utf8_lossy(entry.name_bytes())
);
let page_history = history.by_page.entry(path).or_default();
let unchanged = page_history
.revisions
.last()
.is_some_and(|rev| rev.blob_oid == entry.id());
if unchanged {
// Note again that the history is reversed as we're walking from HEAD
// backwards, so we need to find the _earliest_ commit with this revision.
// Therefore we update that current revision's commit oid with the
// current commit.
page_history.revisions.last_mut().unwrap().commit_oid = commit_oid;
} else {
page_history.revisions.push(Revision {
commit_oid,
blob_oid: entry.id(),
});
}
git2::TreeWalkResult::Ok
} else {
git2::TreeWalkResult::Ok
}
})?;
}
Ok(history)
}
pub fn read_revision(
&self,
git: &git2::Repository,
revision: &Revision,
) -> anyhow::Result<Vec<u8>> {
Ok(git.find_blob(revision.blob_oid)?.content().to_owned())
}
}
impl Revision {
pub fn commit_short(&self) -> String {
self.commit_oid.to_string()[0..6].to_owned()
}
}

View file

@ -1,13 +1,13 @@
pub mod cli;
pub mod config;
pub mod dirs;
pub mod doc;
pub mod feed;
pub mod fun;
pub mod generate;
pub mod history;
pub mod html;
pub mod import_map;
pub mod parse;
pub mod paths;
pub mod sources;
pub mod state;
pub mod tree;

View file

View file

@ -1,16 +1,17 @@
use std::{collections::HashMap, ops::ControlFlow};
use anyhow::{anyhow, Context};
use anyhow::{Context, anyhow};
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
use tracing::{error, info_span, instrument};
use crate::{
config::Config,
dirs::Dirs,
doc::Doc,
html::navmap::NavigationMap,
import_map::ImportMap,
parse::parse_tree_with_diagnostics,
state::{report_diagnostics, Source, Treehouse},
state::{Source, Tag, Treehouse, report_diagnostics},
tree::SemaRoots,
vfs::{self, Cd, Content, VPath, VPathBuf},
};
@ -135,20 +136,57 @@ fn load_trees(config: &Config, dirs: &Dirs) -> anyhow::Result<Treehouse> {
}
}
report_diagnostics(&treehouse, &diagnostics)?;
// Docs
for path in doc_paths {
let mut doc_file_ids = vec![];
for path in &doc_paths {
if let Some(input) =
vfs::query::<Content>(&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);
treehouse.files_by_doc_path.insert(path.clone(), file_id);
doc_file_ids.push(file_id);
} else {
error!("doc {path} does not exist in content directory even though it was enumerated via walk_dir_rec");
error!(
"doc {path} does not exist in content directory even though it was enumerated via walk_dir_rec"
);
}
}
for file_id in doc_file_ids {
let (doc, mut doc_diagnostics) = Doc::parse(&mut treehouse, file_id);
treehouse.docs.insert(file_id, doc);
diagnostics.append(&mut doc_diagnostics);
}
// Tags
for (_, file_id) in &treehouse.files_by_tree_path {
let roots = &treehouse.roots[file_id];
for tag_name in &roots.attributes.tags {
let tag = treehouse
.tags
.entry(tag_name.clone())
.or_insert_with(Tag::default);
tag.files.push(*file_id);
}
}
for (_, file_id) in &treehouse.files_by_doc_path {
let doc = &treehouse.docs[file_id];
for tag_name in &doc.attributes.tags {
let tag = treehouse
.tags
.entry(tag_name.clone())
.or_insert_with(Tag::default);
tag.files.push(*file_id);
}
}
// Diagnostics
report_diagnostics(&treehouse, &diagnostics)?;
Ok(treehouse)
}

View file

@ -9,6 +9,7 @@ use tracing::instrument;
use ulid::Ulid;
use crate::{
doc::Doc,
tree::{SemaBranchId, SemaRoots, SemaTree},
vfs::{VPath, VPathBuf},
};
@ -63,17 +64,24 @@ impl File {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FileId(usize);
#[derive(Debug, Clone, Default)]
pub struct Tag {
pub files: Vec<FileId>,
}
/// Treehouse compilation context.
pub struct Treehouse {
pub files: Vec<File>,
pub files_by_tree_path: HashMap<VPathBuf, FileId>, // trees only
pub files_by_doc_path: HashMap<VPathBuf, FileId>, // docs only
pub feeds_by_name: HashMap<String, FileId>,
pub tags: HashMap<String, Tag>,
pub tree: SemaTree,
pub branches_by_named_id: HashMap<String, SemaBranchId>,
pub roots: HashMap<FileId, SemaRoots>,
pub docs: HashMap<FileId, Doc>,
pub branch_redirects: HashMap<String, SemaBranchId>,
pub missingno_generator: ulid::Generator,
@ -85,12 +93,14 @@ impl Treehouse {
files: vec![],
files_by_tree_path: HashMap::new(),
files_by_doc_path: HashMap::new(),
feeds_by_name: HashMap::new(),
tags: HashMap::new(),
tree: SemaTree::default(),
branches_by_named_id: HashMap::new(),
roots: HashMap::new(),
docs: HashMap::new(),
branch_redirects: HashMap::new(),
missingno_generator: ulid::Generator::new(),

View file

@ -1,6 +1,5 @@
pub mod ast;
pub mod attributes;
pub mod feed;
pub mod mini_template;
pub mod pull;
@ -12,7 +11,7 @@ use tracing::instrument;
use crate::{
config::Config,
state::{toml_error_to_diagnostic, FileId, Source, TomlError, Treehouse},
state::{FileId, Source, TomlError, Treehouse, toml_error_to_diagnostic},
tree::{
ast::{Branch, Roots},
attributes::{Attributes, Content},
@ -171,10 +170,6 @@ impl SemaRoots {
}
}
if let Some(feed_name) = &attributes.feed {
treehouse.feeds_by_name.insert(feed_name.clone(), file_id);
}
attributes
}
}

View file

@ -7,6 +7,11 @@ use crate::{state::FileId, vfs::VPathBuf};
/// Top-level `%%` root attributes.
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct RootAttributes {
/// Unique ID of this page.
/// Required for the page to be shown in feeds.
#[serde(default)]
pub id: Option<String>,
/// Template to use for generating the page.
/// Defaults to `_tree.hbs`.
#[serde(default)]
@ -46,12 +51,9 @@ pub struct RootAttributes {
#[serde(default)]
pub timestamps: Option<Timestamps>,
/// 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
/// Tags to assign to this page.
#[serde(default)]
pub feed: Option<String>,
pub tags: Vec<String>,
}
/// A picture reference.
@ -134,15 +136,19 @@ pub struct Attributes {
pub tags: Vec<String>,
}
/// Parses the timestamp out of a branch ID.
/// Returns `None` if the ID does not contain a timestamp.
pub fn timestamp_from_id(id: &str) -> Option<DateTime<Utc>> {
Ulid::from_string(id)
.ok()
.as_ref()
.map(Ulid::timestamp_ms)
.and_then(|ms| DateTime::from_timestamp_millis(ms as i64))
}
impl Attributes {
/// Parses the timestamp out of the branch's ID.
/// Returns `None` if the ID does not contain a timestamp.
pub fn timestamp(&self) -> Option<DateTime<Utc>> {
Ulid::from_string(&self.id)
.ok()
.as_ref()
.map(Ulid::timestamp_ms)
.and_then(|ms| DateTime::from_timestamp_millis(ms as i64))
timestamp_from_id(&self.id)
}
}

View file

@ -1,94 +0,0 @@
use crate::{
dirs::Dirs,
html::djot::{self, resolve_link},
sources::Sources,
state::FileId,
};
#[derive(Debug, Clone)]
pub struct ParsedEntry {
pub title: Option<String>,
pub link: Option<String>,
}
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 <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()
}

View file

@ -7,7 +7,7 @@ use dashmap::DashMap;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use tracing::{info_span, instrument};
use super::{query, walk_dir_rec, Content, Dir, Query, VPath, VPathBuf};
use super::{Content, Dir, Query, VPath, VPathBuf, query, walk_dir_rec};
pub struct ContentCache<T> {
inner: T,

View file

@ -3,6 +3,7 @@ use std::{borrow::Borrow, error::Error, fmt, ops::Deref, str::FromStr};
use serde::{Deserialize, Serialize};
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct VPath {
path: str,
}
@ -41,7 +42,8 @@ impl VPath {
}
const unsafe fn new_unchecked(s: &str) -> &Self {
std::mem::transmute::<_, &Self>(s)
// SAFETY: The representation of &str and &VPath is the same.
unsafe { std::mem::transmute::<_, &Self>(s) }
}
pub fn is_empty(&self) -> bool {
@ -117,11 +119,7 @@ impl VPath {
pub fn extension(&self) -> Option<&str> {
let file_name = self.file_name()?;
let (left, right) = file_name.rsplit_once('.')?;
if left.is_empty() {
None
} else {
Some(right)
}
if left.is_empty() { None } else { Some(right) }
}
pub fn with_extension(&self, extension: &str) -> VPathBuf {

View file

@ -6,7 +6,10 @@ main.doc {
align-items: start;
& .vertical-center {
--article-padding: 12rem;
min-height: 100vh;
padding: var(--article-padding) 0;
max-width: 100%; /* prevent from blowing up */
flex-grow: 1;
@ -85,7 +88,8 @@ main.doc {
}
& section.feed {
max-width: 40ch;
width: 40ch;
flex-shrink: 0;
padding: 0.8rem;
padding-top: 3.2rem;
}
@ -97,6 +101,7 @@ main.doc {
align-items: center;
& .vertical-center {
--article-padding: 3.2rem;
min-height: 0;
}
@ -105,7 +110,7 @@ main.doc {
}
& section.feed {
max-width: var(--doc-text-width);
width: var(--doc-text-width);
margin-top: 2.4em;
padding: 1.6rem;

View file

@ -439,17 +439,19 @@ section.feed {
/* Titles */
& a,
& a:visited {
color: var(--text-color);
}
& h2 {
& a,
& a:visited {
color: var(--text-color);
}
& a:visited {
color: color-mix(
in srgb,
var(--background-color),
var(--text-color) 60%
);
& a:visited {
color: color-mix(
in srgb,
var(--background-color),
var(--text-color) 60%
);
}
}
& h1 {
@ -496,12 +498,13 @@ section.feed {
margin: 0;
padding: 0;
& > *::before {
content: "#";
& > *:not(:first-child) {
padding-left: 1ch;
}
& > *:not(:first-child)::before {
padding-left: 0.4rem;
& a,
& a:visited {
color: var(--text-color);
}
}
}
@ -647,7 +650,7 @@ h1.page-title {
--recursive-wght: 900;
line-height: 1.2;
padding-top: 3lh;
padding-top: 0.5lh;
padding-bottom: 0.5lh;
& a {
@ -669,12 +672,6 @@ h1.page-title {
}
}
@media (max-width: 1280px) {
h1.page-title {
padding-top: 0.25lh;
}
}
@media (max-width: 700px) {
h1.page-title {
font-size: 4rem;

View file

@ -190,16 +190,6 @@ CommandLine.registerCommand({
},
});
CommandLine.registerCommand({
aliases: ["new", "n"],
description: "go to news feed",
immediate: true,
run() {
window.location = `${TREEHOUSE_SITE}/treehouse/new`;
},
});
CommandLine.registerCommand({
aliases: ["index", "i", "-w-"],
description: "go home",

View file

@ -24,7 +24,6 @@
{{~> components/_doc.hbs }}
{{~> components/_pink_space.hbs }}
<th-overlays></th-overlays>
<th-command-line></th-command-line>
</body>

View file

@ -2,21 +2,27 @@
<!--
%% title = "riki's house Atom feed"
title = "riki's house 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!
## Remarks
- 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.
This feed contains no content.
This is not only to save bandwidth as the feed grows larger and larger, but also so that you can appreciate my website in full.
I put a lot of thought into its design and typography, and surely reading my writing *as I write it* rather than _as your feed reader renders it_ will improve your experience.
If you don't like that, please feel free to not subscribe (or unsubscribe).
## Tags
If you'd like to filter by tag, you can do so by subscribing to /feed/{tag}.atom, e.g. /feed/programming.atom.
-->
<feed xmlns="http://www.w3.org/2005/Atom">
<id>{{ config.user.canonical_url }}</id>
<updated>{{ feed.updated }}</updated>
<updated>{{ updated }}</updated>
<title>{{ config.user.title }}</title>
<subtitle>{{ config.user.description }}</subtitle>
@ -29,17 +35,17 @@
<uri>{{ config.user.canonical_url }}</uri>
</author>
{{#each feed.entries}}
{{#each feed.entries as |entry|}}
<entry>
<id>{{ ../config.user.feed_id_prefix }}/b?{{ id }}</id>
<id>{{ ../config.user.feed_id_prefix }}/{{ 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 tags as |tag|}}
<category term="{{ tag }}"/>
{{/each}}
<summary type="html">{{ summary }}</summary>
<content type="text/html" src="{{ url }}"/>
</entry>
{{/each}}
</feed>

View file

@ -4,6 +4,14 @@
{{#if (ne page.tree_path 'index')}}
<header>
<h1 class="page-title"><a href="{{ config.site }}/{{ page.tree_path }}">{{ page.title }}</a></h1>
<ul class="feeds">
{{#each page.feeds as |feed|}}
<li>
<time datetime="{{ feed.updated }}">{{ iso_date feed.updated }}</time>
{{#if (gt (len page.feeds) 0)}}<span class="source-feed">[<a href="{{ feed.url }}">{{ feed.name }}</a>]</span>{{/if}}
</li>
{{/each}}
</ul>
</header>
{{/if}}

View file

@ -8,8 +8,8 @@
<div class="info">
<time datetime="{{ updated }}">{{ iso_date updated }}</time>
<ul class="categories">
{{#each categories as |category|}}
<li>{{ category }}</li>
{{#each tags as |tag|}}
<li><a href="{{ config.site }}/tag/{{ tag }}">#{{ tag }}</a></li>
{{/each}}
</ul>
</div>

View file

@ -72,6 +72,4 @@ clever to do while browser vendors figure that out, we'll just have to do a cach
<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}}
<link rel="alternate" type="application/atom+xml" title="{{ feed_name }}" href="{{ config.site }}/feed/all.atom">

View file

@ -58,6 +58,8 @@ feed_id_prefix = "https://liquidex.house"
[feed]
tags = [
"all",
# Hobby corners
"meow",
"programming",
@ -78,7 +80,6 @@ tags = [
# Projects
"treehouse",
"haku",
]
[redirects.path]
@ -115,6 +116,9 @@ tags = [
# 2025-07-15 furry v2
"philosophy/furry" = "furry"
# 2025-08-25 tags
"tag/all" = "index"
[emoji]
[pics]