This commit is contained in:
liquidex 2024-02-21 23:17:19 +01:00
parent d64cc3fbf2
commit a1464bb865
20 changed files with 636 additions and 193 deletions

View file

@ -0,0 +1,11 @@
%% template = "_new.hbs"
title = "a curated feed of updates to the house"
styles = ["new.css"]
feed = "news"
% id = "01HQ6G30PTVT5H0Z04VVRHEZQF"
- ever wondered how Terraria renders its worlds? or how editors like Tiled manage to make painting tiles so easy?
### tairu - an interactive exploration of 2D autotiling techniques
[read][page:programming/blog/tairu]

View file

@ -43,6 +43,40 @@ struct ParsedTree {
target_path: PathBuf, target_path: PathBuf,
} }
#[derive(Serialize)]
struct Feed {
branches: Vec<String>,
}
#[derive(Serialize)]
pub struct Page {
pub title: String,
pub thumbnail: Option<Thumbnail>,
pub scripts: Vec<String>,
pub styles: Vec<String>,
pub breadcrumbs: String,
pub tree_path: Option<String>,
pub tree: String,
}
#[derive(Serialize)]
pub struct Thumbnail {
pub url: String,
pub alt: Option<String>,
}
#[derive(Serialize)]
struct StaticTemplateData<'a> {
config: &'a Config,
}
#[derive(Serialize)]
pub struct PageTemplateData<'a> {
pub config: &'a Config,
pub page: Page,
pub feeds: &'a HashMap<String, Feed>,
}
impl Generator { impl Generator {
fn add_directory_rec(&mut self, directory: &Path) -> anyhow::Result<()> { fn add_directory_rec(&mut self, directory: &Path) -> anyhow::Result<()> {
for entry in WalkDir::new(directory) { for entry in WalkDir::new(directory) {
@ -172,14 +206,14 @@ impl Generator {
config: &Config, config: &Config,
paths: &Paths<'_>, paths: &Paths<'_>,
navigation_map: &NavigationMap, navigation_map: &NavigationMap,
parsed_trees: impl IntoIterator<Item = ParsedTree>, parsed_trees: Vec<ParsedTree>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut handlebars = Handlebars::new(); let mut handlebars = Handlebars::new();
let mut config_derived_data = ConfigDerivedData::default(); let mut config_derived_data = ConfigDerivedData::default();
let mut template_file_ids = HashMap::new(); let mut template_file_ids = HashMap::new();
for entry in WalkDir::new(paths.template_dir) { for entry in WalkDir::new(paths.template_dir) {
let entry = entry?; let entry = entry.context("cannot read directory entry")?;
let path = entry.path(); let path = entry.path();
if !entry.file_type().is_dir() && path.extension() == Some(OsStr::new("hbs")) { if !entry.file_type().is_dir() && path.extension() == Some(OsStr::new("hbs")) {
let relative_path = path let relative_path = path
@ -194,12 +228,8 @@ impl Generator {
std::fs::create_dir_all(paths.template_target_dir)?; std::fs::create_dir_all(paths.template_target_dir)?;
for (name, &file_id) in &template_file_ids { for (name, &file_id) in &template_file_ids {
if !name.starts_with('_') { let filename = name.rsplit_once('/').unwrap_or(("", name)).1;
#[derive(Serialize)] if !filename.starts_with('_') {
struct StaticTemplateData<'a> {
config: &'a Config,
}
let templated_html = match handlebars.render(name, &StaticTemplateData { config }) { let templated_html = match handlebars.render(name, &StaticTemplateData { config }) {
Ok(html) => html, Ok(html) => html,
Err(error) => { Err(error) => {
@ -220,6 +250,24 @@ impl Generator {
} }
} }
let mut feeds = HashMap::new();
for parsed_tree in &parsed_trees {
let roots = &treehouse.roots[&parsed_tree.tree_path];
if let Some(feed_name) = &roots.attributes.feed {
let mut feed = Feed {
branches: Vec::new(),
};
for &root in &roots.branches {
let branch = treehouse.tree.branch(root);
feed.branches.push(branch.attributes.id.clone());
}
dbg!(&feed.branches);
feeds.insert(feed_name.to_owned(), feed);
}
}
for parsed_tree in parsed_trees { for parsed_tree in parsed_trees {
let breadcrumbs = breadcrumbs_to_html(config, navigation_map, &parsed_tree.tree_path); let breadcrumbs = breadcrumbs_to_html(config, navigation_map, &parsed_tree.tree_path);
@ -238,28 +286,6 @@ impl Generator {
&roots.branches, &roots.branches,
); );
#[derive(Serialize)]
pub struct Page {
pub title: String,
pub thumbnail: Option<Thumbnail>,
pub scripts: Vec<String>,
pub styles: Vec<String>,
pub breadcrumbs: String,
pub tree_path: Option<String>,
pub tree: String,
}
#[derive(Serialize)]
pub struct Thumbnail {
pub url: String,
pub alt: Option<String>,
}
#[derive(Serialize)]
pub struct PageTemplateData<'a> {
pub config: &'a Config,
pub page: Page,
}
let template_data = PageTemplateData { let template_data = PageTemplateData {
config, config,
page: Page { page: Page {
@ -280,16 +306,22 @@ impl Generator {
.map(|s| s.to_owned()), .map(|s| s.to_owned()),
tree, tree,
}, },
feeds: &feeds,
}; };
let template_name = roots
.attributes
.template
.clone()
.unwrap_or_else(|| "_tree.hbs".into());
treehouse.roots.insert(parsed_tree.tree_path, roots); treehouse.roots.insert(parsed_tree.tree_path, roots);
let templated_html = match handlebars.render("_tree.hbs", &template_data) { let templated_html = match handlebars.render(&template_name, &template_data) {
Ok(html) => html, Ok(html) => html,
Err(error) => { Err(error) => {
Self::wrangle_handlebars_error_into_diagnostic( Self::wrangle_handlebars_error_into_diagnostic(
treehouse, treehouse,
template_file_ids["_tree.hbs"], template_file_ids[&template_name],
error.line_no, error.line_no,
error.column_no, error.column_no,
error.desc, error.desc,

View file

@ -114,15 +114,22 @@ pub fn branch_to_html(
.map(|&branch_id| { .map(|&branch_id| {
( (
format!( format!(
"/b?{}", "{}/b?{}",
config.site,
treehouse.tree.branch(branch_id).attributes.id treehouse.tree.branch(branch_id).attributes.id
) )
.into(), .into(),
"".into(), "".into(),
) )
}), }),
"page" => {
Some((format!("{}/{}.html", config.site, linked).into(), "".into()))
}
"pic" => config.pics.get(linked).map(|filename| { "pic" => config.pics.get(linked).map(|filename| {
(format!("/static/pic/{}", &filename).into(), "".into()) (
format!("{}/static/pic/{}", config.site, &filename).into(),
"".into(),
)
}), }),
_ => None, _ => None,
}) })

View file

@ -3,6 +3,11 @@ use serde::{Deserialize, Serialize};
/// 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 {
/// Template to use for generating the page.
/// Defaults to `_tree.hbs`.
#[serde(default)]
pub template: Option<String>,
/// Title of the generated .html page. /// Title of the generated .html page.
/// ///
/// The page's tree path is used if empty. /// The page's tree path is used if empty.
@ -26,6 +31,11 @@ pub struct RootAttributes {
/// These are relative to the /static/css directory. /// These are relative to the /static/css directory.
#[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.
/// Feeds can be read by Handlebars templates to generate content based on them.
#[serde(default)]
pub feed: Option<String>,
} }
/// A picture reference. /// A picture reference.

View file

@ -369,6 +369,16 @@ th {
--recursive-casl: 0.5; --recursive-casl: 0.5;
} }
/* Horizontal rules */
hr {
width: 100%;
border: none;
border-top: 1px solid var(--border-1);
margin-top: 2em;
margin-bottom: 2em;
}
/* Style the noscript box a little more prettily. */ /* Style the noscript box a little more prettily. */
.noscript { .noscript {
@ -413,6 +423,7 @@ nav {
nav .nav-page { nav .nav-page {
display: flex; display: flex;
flex-grow: 1;
flex-direction: column; flex-direction: column;
} }
@ -435,8 +446,49 @@ h1.page-title {
font-size: 1.25rem; font-size: 1.25rem;
} }
/* Style the footer */ /* Style the `new` link on the homepage */
a[is="th-new"] {
flex-shrink: 0;
color: var(--text-color);
opacity: 50%;
&.has-news {
opacity: 100%;
text-decoration: none;
& .new-text {
text-decoration: underline;
}
}
& .badge {
margin-left: 8px;
text-decoration: none;
}
}
/* Style new badges */
span.badge {
--recursive-wght: 800;
--recursive-slnt: 0;
--recursive-mono: 1.0;
--recursive-casl: 0;
border-radius: 999px;
padding: 2px 6px;
font-size: 0.9em;
&.red {
color: white;
background-color: #d01243;
}
&.before-content {
margin-right: 6px;
}
}
/* Style the footer */
footer { footer {
margin-top: 4rem; margin-top: 4rem;
text-align: right; text-align: right;
@ -561,6 +613,9 @@ th-literate-program[data-mode="output"] {
border-style: none; border-style: none;
border-radius: 4px; border-radius: 4px;
display: block; display: block;
}
& img.placeholder.js {
transition: opacity var(--transition-duration); transition: opacity var(--transition-duration);
} }
@ -709,3 +764,11 @@ th-literate-program[data-mode="output"] {
} }
} }
} }
/* Style settings sections */
section[is="th-settings"] {
/* Don't display settings when JavaScript is disabled.
JS overrides this value on the element itself. */
display: none;
}

103
static/css/new.css Normal file
View file

@ -0,0 +1,103 @@
/* Give the intro and outro some breathing room. */
section {
padding: 1em 2em;
}
/* Style all links in the last paragraph as big buttons. */
.tree th-bc>p:last-child {
--transition-duration: 0.2s;
margin-top: 12px;
margin-bottom: 4px;
&>a {
padding: 0.5em 1.5em;
color: var(--text-color);
background-color: transparent;
border: 1px solid var(--border-1);
border-radius: 2em;
text-decoration: none;
transition:
color var(--transition-duration),
background-color var(--transition-duration),
border-color var(--transition-duration);
&:hover,
&:focus {
color: white;
background-color: #058ef0;
border-color: white;
}
}
}
section[is="th-settings"] {
& h3 {
display: inline;
}
& details>summary {
--recursive-wght: 700;
list-style: none;
cursor: pointer;
opacity: 50%;
transition: opacity var(--transition-duration);
&::-webkit-details-marker {
display: none;
}
&::before {
--recursive-casl: 0.0;
--recursive-mono: 1.0;
--recursive-slnt: 0.0;
content: '+';
margin-right: 0.3em;
opacity: 50%;
}
&:hover {
opacity: 100%;
}
}
& details[open]>summary {
opacity: 100%;
&::before {
content: '-';
}
}
& p {
margin-bottom: 8px;
}
& button {
border: 1px solid var(--border-1);
border-radius: 999px;
padding: 4px 12px;
background: none;
color: var(--text-color);
font-size: 1rem;
cursor: pointer;
transition:
color var(--transition-duration),
background-color var(--transition-duration),
border-color var(--transition-duration);
&:hover {
color: white;
background-color: #058ef0;
border-color: white;
}
}
}

View file

@ -184,6 +184,7 @@ class OutputMode {
}); });
if (this.frame.placeholderImage != null) { if (this.frame.placeholderImage != null) {
this.frame.placeholderImage.classList.add("js");
this.frame.placeholderImage.classList.add("loading"); this.frame.placeholderImage.classList.add("loading");
} }

70
static/js/news.js Normal file
View file

@ -0,0 +1,70 @@
// news.js because new.js makes the TypeScript language server flip out.
// Likely because `new` is a keyword, but also, what the fuck.
import { getSettingValue } from "./settings.js";
import { Branch } from "./tree.js";
const seenStatesKey = "treehouse.news.seenBranches";
const seenStates = new Set(JSON.parse(localStorage.getItem(seenStatesKey)) || []);
let seenCount = seenStates.size;
let unseenCount = TREEHOUSE_NEWS_COUNT - seenCount;
function saveSeenStates() {
localStorage.setItem(seenStatesKey, JSON.stringify(Array.from(seenStates)));
}
function markAsRead(branch) {
if (!seenStates.has(branch.namedID) && seenCount > 0) {
let badge = document.createElement("span");
badge.classList.add("badge", "red", "before-content");
badge.textContent = "new";
branch.branchContent.firstChild.insertBefore(badge, branch.branchContent.firstChild.firstChild);
}
seenStates.add(branch.namedID);
}
export function initNewsPage() {
for (let [_, branch] of Branch.branchesByNamedID) {
markAsRead(branch);
}
saveSeenStates();
// If any branches are added past the initial load, add them to the seen set too.
Branch.onAdded.push(branch => {
markAsRead(branch);
saveSeenStates();
})
}
export function markAllAsUnread() {
localStorage.removeItem(seenStatesKey);
}
class New extends HTMLAnchorElement {
connectedCallback() {
// Do not show the badge to people who have never seen any news.
// It's just annoying in that case.
// In case you do not wish to see the badge anymore, go to the news page and uncheck the
// checkbox at the bottom.
let userSawNews = seenCount > 0;
let userWantsToSeeNews = getSettingValue("showNewPostIndicator");
if (userSawNews && userWantsToSeeNews && unseenCount > 0) {
this.newText = document.createElement("span");
this.newText.classList.add("new-text");
this.newText.textContent = this.textContent;
this.textContent = "";
this.appendChild(this.newText);
this.badge = document.createElement("span");
this.badge.classList.add("badge", "red");
this.badge.textContent = unseenCount.toString();
this.appendChild(this.badge);
this.classList.add("has-news");
}
}
}
customElements.define("th-new", New, { extends: "a" });

35
static/js/settings.js Normal file
View file

@ -0,0 +1,35 @@
const settingsKey = "treehouse.settings";
const settings = JSON.parse(localStorage.getItem(settingsKey)) || {};
const defaultSettingValues = {
showNewPostIndicator: true,
};
function saveSettings() {
localStorage.setItem(settingsKey, JSON.stringify(settings));
}
export function getSettingValue(setting) {
return settings[setting] ?? defaultSettingValues[setting];
}
class SettingCheckbox extends HTMLInputElement {
connectedCallback() {
this.checked = getSettingValue(this.id);
this.addEventListener("change", () => {
settings[this.id] = this.checked;
saveSettings();
});
}
}
customElements.define("th-setting-checkbox", SettingCheckbox, { extends: "input" });
class Settings extends HTMLElement {
connectedCallback() {
this.style.display = "block";
}
}
customElements.define("th-settings", Settings, { extends: "section" });

View file

@ -1,5 +1,5 @@
// Detect if we can have crucial functionality (ie. custom elements call constructors). // Detect if we can have crucial functionality (ie. custom elements call constructors).
// This doesn't seem to happen in Epiphany, and possibly also other Webkit-based browsers. // This doesn't seem to happen in Epiphany, and also other Webkit-based browsers.
let works = false; let works = false;
class WebkitMoment extends HTMLLIElement { class WebkitMoment extends HTMLLIElement {
constructor() { constructor() {

View file

@ -17,12 +17,11 @@ function branchIsOpen(branchID) {
return branchState[branchID]; return branchState[branchID];
} }
class Branch extends HTMLLIElement { export class Branch extends HTMLLIElement {
static branchesByNamedID = new Map(); static branchesByNamedID = new Map();
static onAdded = [];
constructor() { connectedCallback() {
super();
this.isLeaf = this.classList.contains("leaf"); this.isLeaf = this.classList.contains("leaf");
this.details = this.childNodes[0]; this.details = this.childNodes[0];
@ -48,16 +47,20 @@ class Branch extends HTMLLIElement {
}); });
} }
let namedID = this.id.split(':')[1]; this.namedID = this.id.split(':')[1];
Branch.branchesByNamedID.set(namedID, this); Branch.branchesByNamedID.set(this.namedID, this);
if (ulid.isCanonicalUlid(namedID)) { if (ulid.isCanonicalUlid(this.namedID)) {
let timestamp = ulid.getTimestamp(namedID); let timestamp = ulid.getTimestamp(this.namedID);
let date = document.createElement("span"); let date = document.createElement("span");
date.classList.add("branch-date"); date.classList.add("branch-date");
date.innerText = timestamp.toLocaleDateString(); date.innerText = timestamp.toLocaleDateString();
this.buttonBar.insertBefore(date, this.buttonBar.firstChild); this.buttonBar.insertBefore(date, this.buttonBar.firstChild);
} }
for (let callback of Branch.onAdded) {
callback(this);
}
} }
} }
@ -68,8 +71,8 @@ customElements.define("th-b", Branch, { extends: "li" });
class LinkedBranch extends Branch { class LinkedBranch extends Branch {
static byLink = new Map(); static byLink = new Map();
constructor() { connectedCallback() {
super(); super.connectedCallback();
this.linkedTree = this.getAttribute("data-th-link"); this.linkedTree = this.getAttribute("data-th-link");
LinkedBranch.byLink.set(this.linkedTree, this); LinkedBranch.byLink.set(this.linkedTree, this);

8
template/README.md Normal file
View file

@ -0,0 +1,8 @@
# Templates
This directory houses Handlebars templates, which are mostly used for reusable bits of the house.
Files that are not prefixed with a `_` are generated into their own `.html` files.
All other files are only loaded into Handlebars for use by other templates (or the generator itself.)
In particular, `_tree.hbs` is used as the default page template. This can be changed by including a `%% template = "_whatever.hbs"` at the top of your .tree file.

77
template/_new.hbs Normal file
View file

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en-US" prefix="og: https://ogp.me/ns#">
<head>
{{> components/_head.hbs }}
</head>
<body>
{{#> components/_nav.hbs }}
{{!-- For /index, include a "new" link that goes to the curated news feed page. --}}
{{#if (eq page.tree_path "index")}}
<a href="{{ config.site }}/treehouse/new.html" is="th-new">new</a>
{{/if}}
{{/ components/_nav.hbs }}
{{> components/_noscript.hbs }}
{{> components/_webkit.hbs }}
<section>
<p>welcome!</p>
<p>since you clicked here, you must be curious as to what's been going on since your last visit to the house. so
here's a recap just for you - enjoy!</p>
</section>
<hr>
{{> components/_tree.hbs }}
<hr>
<section>
<p>note that this page does not include any updates that were made to the website itself - for that, you can
visit <a href="{{ config.site }}/treehouse/changelog.html">the changelog</a>.
</p>
</section>
<section is="th-settings">
<details>
<summary>
settings
</summary>
<section>
<p>if you find the newsfeed annoying, you can customize some aspects of it.</p>
<p>
<input type="checkbox" is="th-setting-checkbox" id="showNewPostIndicator">
<label for="showNewPostIndicator">show the <span class="badge red">1</span> badge on the homepage
for
new posts you haven't read yet</label>
</p>
<p>
<button id="mark-all-as-unread"
title="Mostly useful for debugging purposes, but it's there if you really wanna do it.">
mark all as unread</button>
</p>
</section>
</details>
</section>
{{!-- For all pages except the one linked from the footer, include the footer icon. --}}
{{#if (ne page.tree_path "treehouse")}}
{{> components/_footer.hbs }}
{{/if}}
<script type="module" defer>
import { initNewsPage, markAllAsUnread } from "{{ config.site }}/static/js/news.js";
initNewsPage();
document.getElementById("mark-all-as-unread").addEventListener("click", () => {
markAllAsUnread();
alert("congration! you done it");
});
</script>
</body>
</html>

View file

@ -3,161 +3,31 @@
<html lang="en-US" prefix="og: https://ogp.me/ns#"> <html lang="en-US" prefix="og: https://ogp.me/ns#">
<head> <head>
<meta charset="UTF-8"> {{> components/_head.hbs }}
<title>{{#if (ne page.title config.user.title)}}{{ page.title }} · {{/if}}{{ config.user.title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preload" href="{{ config.site }}/static/font/Recursive_VF_1.085.woff2" as="font" type="font/woff2"
crossorigin="anonymous">
<link rel="stylesheet" href="{{ config.site }}/static/css/main.css">
<link rel="stylesheet" href="{{ config.site }}/static/css/tree.css">
<script>const TREEHOUSE_SITE = `{{ config.site }}`;</script>
<script type="module" src="{{ config.site }}/navmap.js"></script>
<script type="module" src="{{ config.site }}/static/js/ulid.js"></script>
<script type="module" src="{{ config.site }}/static/js/usability.js"></script>
<script type="module" src="{{ config.site }}/static/js/tree.js"></script>
<script type="module" src="{{ config.site }}/static/js/emoji.js"></script>
<script type="module" src="{{ config.site }}/static/js/thanks-webkit.js"></script>
<meta property="og:site_name" content="{{ config.user.title }}">
<meta property="og:title" content="{{ page.title }}">
{{!--
This is a bit of a hack to quickly insert metadata into generated pages without going through Handlebars, which
would involve registering, parsing, and generating a page from a template.
Yes it would be more flexible that way, but it doesn't need to be.
It just needs to be a string replacement.
--}}
<!-- treehouse-ca37057a-cff5-45b3-8415-3b02dbf6c799-per-branch-metadata -->
{{#if page.thumbnail}}
<meta property="og:image" content="{{ page.thumbnail.url }}">
<meta property="og:image:alt" content="{{ page.thumbnail.alt }}">
{{/if}}
</head> </head>
<body> <body>
<nav> {{#> components/_nav.hbs }}
<a href="{{ config.site }}/" title="Back to homepage">
<svg class="logo" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M8 3H7H6V4V5H4V6H6V9V10H7H10V12H11V10H12H13V9V8V7H12H11H10V8V9H7V6H8H9V5V4V3H8ZM12 9H11V8H12V9ZM7 5V4H8V5H7ZM3 5H2V6H3V5ZM10 13H11V14H10V13Z"
fill="currentColor" />
</svg>
</a>
<div class="nav-page"> {{!-- For /index, include a "new" link that goes to the curated news feed page. --}}
{{#if page.breadcrumbs}} {{#if (eq page.tree_path "index")}}
<ol class="breadcrumbs"> <a href="{{ config.site }}/treehouse/new.html" is="th-new">new</a>
{{{ page.breadcrumbs }}} {{/if}}
</ol>
{{/if}}
{{#if (and (ne page.title config.user.title) (ne page.title page.tree_path))}} {{/ components/_nav.hbs }}
<h1 class="page-title">{{ page.title }}</h1>
{{/if}}
</div>
</nav>
<noscript> {{> components/_noscript.hbs }}
<div class="noscript" role="note"> {{> components/_webkit.hbs }}
<p>hey! looks like you have <strong>JavaScript disabled.</strong><br>
I respect that decision, but you may find the experience of browsing the treehouse… not great.<br>
for example, links to branches may not work properly. I cannot do anything about this; it's due to how
the <code>&lt;details&gt;</code> element works.<br>
(a <code>&lt;details&gt;</code> will not expand itself automatically to reveal the linked element to
you.)<br>
I did my best to at least keep the site readable in this state, but you can only do so much with plain
HTML and CSS.</p>
<p><strong>Pinky promise this website does not contain any malicious code such as trackers or cryptocurrency {{!--
miners.</strong><br> NOTE: ~ because components/_tree.hbss must not include any extra indentation, because it may
if you don't believe me, you're free to inspect the source yourself! all the scripts are written contain pre elements which shouldn't be indented.
lovingly in vanilla JS (not minified!) by yours truly ❤️</p> --}}
<small>and if this box is annoying, feel free to block it with uBlock Origin or something. I have no {{~> components/_tree.hbs }}
way of remembering you closed it, and don't wanna add a database to this website. simplicity
rules!</small>
</div>
</noscript>
<div id="webkit-makes-me-go-insane" class="noscript" role="note"> {{!-- For all pages except the one linked from the footer, include the footer icon. --}}
<p>hey! looks like you're using a weird or otherwise quirky web browser. this basically means, the website will {{#if (ne page.tree_path "treehouse")}}
not work for you correctly. I might fix it in the future but I have very limited time to work on this {{> components/_footer.hbs }}
website and so don't have an estimate on when that might happen.</p>
<p>in the meantime I suggest switching to <a href="https://firefox.com">something more modern.</a></p>
<p>sorry for the inconvenience!</p>
</div>
<main class="tree">
{{!-- Append page styles and scripts into the main content, such that they can be inlined
into linked branches when those are loaded in. Putting them in the page's head would make
extracting them way more painful than it needs to be. --}}
{{#each page.styles}}
<link rel="stylesheet" href="{{ ../config.site }}/static/css/{{ this }}">
{{/each}}
{{#each page.scripts}}
<script type="module" src="{{ ../config.site }}/static/js/{{ this }}"></script>
{{/each}}
{{{ page.tree }}}
</main>
<th-emoji-tooltips></th-emoji-tooltips>
{{#if (ne page.tree_path 'treehouse')}}
<footer>
<a href="{{ config.site }}/treehouse.html">
<svg id="footer-icon" width="32" height="32" viewBox="0 0 32 32" fill="none"
xmlns="http://www.w3.org/2000/svg">
<g id="all">
<mask id="mask">
<rect width="32" height="32" fill="black" />
<clipPath id="treehouse">
<path fill-rule="evenodd" clip-rule="evenodd" fill="white" transform="translate(0 12)"
d="M2.95266 3.95816C2.74074 1.83892 4.40494 0 6.53475 0C8.68036 0 10.3496 1.86501 10.1127 3.9975L10.0568 4.5L10.352 4.37352C11.7717 3.76506 13.316 4.92718 13.1244 6.45988L13.0568 7C14.1537 6.56127 15.3084 7.4907 15.1142 8.65595L15.0449 9.07153C14.7633 10.7614 13.3012 12 11.588 12H4.05892C2.0541 12 0.358966 10.5159 0.0940032 8.52866L0.0241185 8.00452C-0.210422 6.24546 1.30006 4.74903 3.05685 5L2.95266 3.95816ZM4.55685 7H2.55685V8H4.55685V7ZM4.55685 9H2.55685V10H4.55685V9ZM5.55685 7H7.55685V8H5.55685V7ZM7.55685 9H5.55685V10H7.55685V9ZM5.55685 13H7.55685L8.05685 16L9.55685 13H10.5569L9.49201 16.5495C9.21835 17.4617 9.39407 18.4496 9.96549 19.2115L10.5569 20H7.55685V18H6.55685V20H4.55685L5.35542 18.9352C5.80652 18.3338 6.01534 17.5848 5.94053 16.8367L5.55685 13Z" />
</clipPath>
<clipPath id="rectangleClip">
<rect id="rectangle1" width="16" height="16" />
</clipPath>
<clipPath id="rectangleTreehouseClip" clip-path="url(#treehouse)">
<rect id="rectangle2" width="16" height="16" />
</clipPath>
<g transform="translate(3 0)">
<rect width="32" height="32" fill="white" clip-path="url(#treehouse)" />
<rect width="32" height="32" fill="white" clip-path="url(#rectangleClip)" />
<rect width="32" height="32" fill="black" clip-path="url(#rectangleTreehouseClip)" />
</g>
</mask>
<rect width="32" height="32" fill="currentColor" mask="url(#mask)" />
</g>
<style>
#rectangle1,
#rectangle2 {
transform: translate(16px, 12px) rotate(15deg) translate(-8px, -8px);
rx: 0px;
transition: all 1s;
}
#all:hover #rectangle1,
#all:hover #rectangle2 {
transform: translate(22px, 24px) rotate(360deg) translate(-2px, -2px);
width: 4px;
height: 4px;
rx: 4px;
}
</style>
</svg>
</a>
</footer>
{{/if}} {{/if}}
</body> </body>

View file

@ -0,0 +1,49 @@
<footer>
<a href="{{ config.site }}/treehouse.html">
<svg id="footer-icon" width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="all">
<mask id="mask">
<rect width="32" height="32" fill="black" />
<clipPath id="treehouse">
<path fill-rule="evenodd" clip-rule="evenodd" fill="white" transform="translate(0 12)"
d="M2.95266 3.95816C2.74074 1.83892 4.40494 0 6.53475 0C8.68036 0 10.3496 1.86501 10.1127 3.9975L10.0568 4.5L10.352 4.37352C11.7717 3.76506 13.316 4.92718 13.1244 6.45988L13.0568 7C14.1537 6.56127 15.3084 7.4907 15.1142 8.65595L15.0449 9.07153C14.7633 10.7614 13.3012 12 11.588 12H4.05892C2.0541 12 0.358966 10.5159 0.0940032 8.52866L0.0241185 8.00452C-0.210422 6.24546 1.30006 4.74903 3.05685 5L2.95266 3.95816ZM4.55685 7H2.55685V8H4.55685V7ZM4.55685 9H2.55685V10H4.55685V9ZM5.55685 7H7.55685V8H5.55685V7ZM7.55685 9H5.55685V10H7.55685V9ZM5.55685 13H7.55685L8.05685 16L9.55685 13H10.5569L9.49201 16.5495C9.21835 17.4617 9.39407 18.4496 9.96549 19.2115L10.5569 20H7.55685V18H6.55685V20H4.55685L5.35542 18.9352C5.80652 18.3338 6.01534 17.5848 5.94053 16.8367L5.55685 13Z" />
</clipPath>
<clipPath id="rectangleClip">
<rect id="rectangle1" width="16" height="16" />
</clipPath>
<clipPath id="rectangleTreehouseClip" clip-path="url(#treehouse)">
<rect id="rectangle2" width="16" height="16" />
</clipPath>
<g transform="translate(3 0)">
<rect width="32" height="32" fill="white" clip-path="url(#treehouse)" />
<rect width="32" height="32" fill="white" clip-path="url(#rectangleClip)" />
<rect width="32" height="32" fill="black" clip-path="url(#rectangleTreehouseClip)" />
</g>
</mask>
<rect width="32" height="32" fill="currentColor" mask="url(#mask)" />
</g>
<style>
#rectangle1,
#rectangle2 {
transform: translate(16px, 12px) rotate(15deg) translate(-8px, -8px);
rx: 0px;
transition: all 1s;
}
#all:hover #rectangle1,
#all:hover #rectangle2 {
transform: translate(22px, 24px) rotate(360deg) translate(-2px, -2px);
width: 4px;
height: 4px;
rx: 4px;
}
</style>
</svg>
</a>
</footer>

View file

@ -0,0 +1,37 @@
<meta charset="UTF-8">
<title>{{#if (ne page.title config.user.title)}}{{ page.title }} · {{/if}}{{ config.user.title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preload" href="{{ config.site }}/static/font/Recursive_VF_1.085.woff2" as="font" type="font/woff2"
crossorigin="anonymous">
<link rel="stylesheet" href="{{ config.site }}/static/css/main.css">
<link rel="stylesheet" href="{{ config.site }}/static/css/tree.css">
<script>
const TREEHOUSE_SITE = `{{ config.site }}`;
const TREEHOUSE_NEWS_COUNT = {{ len feeds.news.branches }};
</script>
<script type="module" src="{{ config.site }}/navmap.js"></script>
<script type="module" src="{{ config.site }}/static/js/ulid.js"></script>
<script type="module" src="{{ config.site }}/static/js/usability.js"></script>
<script type="module" src="{{ config.site }}/static/js/settings.js"></script>
<script type="module" src="{{ config.site }}/static/js/tree.js"></script>
<script type="module" src="{{ config.site }}/static/js/emoji.js"></script>
<script type="module" src="{{ config.site }}/static/js/thanks-webkit.js"></script>
<script type="module" src="{{ config.site }}/static/js/news.js"></script>
<meta property="og:site_name" content="{{ config.user.title }}">
<meta property="og:title" content="{{ page.title }}">
{{!--
This is a bit of a hack to quickly insert metadata into generated pages without going through Handlebars, which
would involve registering, parsing, and generating a page from a template.
Yes it would be more flexible that way, but it doesn't need to be.
It just needs to be a string replacement.
--}}
<!-- treehouse-ca37057a-cff5-45b3-8415-3b02dbf6c799-per-branch-metadata -->
{{#if page.thumbnail}}
<meta property="og:image" content="{{ page.thumbnail.url }}">
<meta property="og:image:alt" content="{{ page.thumbnail.alt }}">
{{/if}}

View file

@ -0,0 +1,23 @@
<nav>
<a href="{{ config.site }}/" title="Back to homepage">
<svg class="logo" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M8 3H7H6V4V5H4V6H6V9V10H7H10V12H11V10H12H13V9V8V7H12H11H10V8V9H7V6H8H9V5V4V3H8ZM12 9H11V8H12V9ZM7 5V4H8V5H7ZM3 5H2V6H3V5ZM10 13H11V14H10V13Z"
fill="currentColor" />
</svg>
</a>
<div class="nav-page">
{{#if page.breadcrumbs}}
<ol class="breadcrumbs">
{{{ page.breadcrumbs }}}
</ol>
{{/if}}
{{#if (and (ne page.title config.user.title) (ne page.title page.tree_path))}}
<h1 class="page-title">{{ page.title }}</h1>
{{/if}}
</div>
{{> @partial-block }}
</nav>

View file

@ -0,0 +1,20 @@
<noscript>
<div class="noscript" role="note">
<p>hey! looks like you have <strong>JavaScript disabled.</strong><br>
I respect that decision, but you may find the experience of browsing the treehouse… not great.<br>
for example, links to branches may not work properly. I cannot do anything about this; it's due to how
the <code>&lt;details&gt;</code> element works.<br>
(a <code>&lt;details&gt;</code> will not expand itself automatically to reveal the linked element to
you.)<br>
I did my best to at least keep the site readable in this state, but you can only do so much with plain
HTML and CSS.</p>
<p><strong>Pinky promise this website does not contain any malicious code such as trackers or cryptocurrency
miners.</strong><br>
if you don't believe me, you're free to inspect the source yourself! all the scripts are written
lovingly in vanilla JS (not minified!) by yours truly ❤️</p>
<small>and if this box is annoying, feel free to block it with uBlock Origin or something. I have no
way of remembering you closed it, and don't wanna add a database to this website. simplicity
rules!</small>
</div>
</noscript>

View file

@ -0,0 +1,17 @@
<main class="tree">
{{!-- Append page styles and scripts into the main content, such that they can be inlined
into linked branches when those are loaded in. Putting them in the page's head would make
extracting them way more painful than it needs to be. --}}
{{#each page.styles}}
<link rel="stylesheet" href="{{ ../config.site }}/static/css/{{ this }}">
{{/each}}
{{#each page.scripts}}
<script type="module" src="{{ ../config.site }}/static/js/{{ this }}"></script>
{{/each}}
{{{ page.tree }}}
</main>
<th-emoji-tooltips></th-emoji-tooltips>

View file

@ -0,0 +1,7 @@
<div id="webkit-makes-me-go-insane" class="noscript" role="note">
<p>hey! looks like you're using a weird or otherwise quirky web browser. this basically means, the website will
not work for you correctly. I might fix it in the future but I have very limited time to work on this
website and so don't have an estimate on when that might happen.</p>
<p>in the meantime I suggest switching to <a href="https://firefox.com">something more modern.</a></p>
<p>sorry for the inconvenience!</p>
</div>