newsfeed
This commit is contained in:
		
							parent
							
								
									d64cc3fbf2
								
							
						
					
					
						commit
						a1464bb865
					
				
					 20 changed files with 636 additions and 193 deletions
				
			
		
							
								
								
									
										11
									
								
								content/treehouse/new.tree
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								content/treehouse/new.tree
									
										
									
									
									
										Normal 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]
 | 
			
		||||
| 
						 | 
				
			
			@ -43,6 +43,40 @@ struct ParsedTree {
 | 
			
		|||
    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 {
 | 
			
		||||
    fn add_directory_rec(&mut self, directory: &Path) -> anyhow::Result<()> {
 | 
			
		||||
        for entry in WalkDir::new(directory) {
 | 
			
		||||
| 
						 | 
				
			
			@ -172,14 +206,14 @@ impl Generator {
 | 
			
		|||
        config: &Config,
 | 
			
		||||
        paths: &Paths<'_>,
 | 
			
		||||
        navigation_map: &NavigationMap,
 | 
			
		||||
        parsed_trees: impl IntoIterator<Item = ParsedTree>,
 | 
			
		||||
        parsed_trees: Vec<ParsedTree>,
 | 
			
		||||
    ) -> anyhow::Result<()> {
 | 
			
		||||
        let mut handlebars = Handlebars::new();
 | 
			
		||||
        let mut config_derived_data = ConfigDerivedData::default();
 | 
			
		||||
 | 
			
		||||
        let mut template_file_ids = HashMap::new();
 | 
			
		||||
        for entry in WalkDir::new(paths.template_dir) {
 | 
			
		||||
            let entry = entry?;
 | 
			
		||||
            let entry = entry.context("cannot read directory entry")?;
 | 
			
		||||
            let path = entry.path();
 | 
			
		||||
            if !entry.file_type().is_dir() && path.extension() == Some(OsStr::new("hbs")) {
 | 
			
		||||
                let relative_path = path
 | 
			
		||||
| 
						 | 
				
			
			@ -194,12 +228,8 @@ impl Generator {
 | 
			
		|||
 | 
			
		||||
        std::fs::create_dir_all(paths.template_target_dir)?;
 | 
			
		||||
        for (name, &file_id) in &template_file_ids {
 | 
			
		||||
            if !name.starts_with('_') {
 | 
			
		||||
                #[derive(Serialize)]
 | 
			
		||||
                struct StaticTemplateData<'a> {
 | 
			
		||||
                    config: &'a Config,
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            let filename = name.rsplit_once('/').unwrap_or(("", name)).1;
 | 
			
		||||
            if !filename.starts_with('_') {
 | 
			
		||||
                let templated_html = match handlebars.render(name, &StaticTemplateData { config }) {
 | 
			
		||||
                    Ok(html) => html,
 | 
			
		||||
                    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 {
 | 
			
		||||
            let breadcrumbs = breadcrumbs_to_html(config, navigation_map, &parsed_tree.tree_path);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -238,28 +286,6 @@ impl Generator {
 | 
			
		|||
                &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 {
 | 
			
		||||
                config,
 | 
			
		||||
                page: Page {
 | 
			
		||||
| 
						 | 
				
			
			@ -280,16 +306,22 @@ impl Generator {
 | 
			
		|||
                        .map(|s| s.to_owned()),
 | 
			
		||||
                    tree,
 | 
			
		||||
                },
 | 
			
		||||
                feeds: &feeds,
 | 
			
		||||
            };
 | 
			
		||||
            let template_name = roots
 | 
			
		||||
                .attributes
 | 
			
		||||
                .template
 | 
			
		||||
                .clone()
 | 
			
		||||
                .unwrap_or_else(|| "_tree.hbs".into());
 | 
			
		||||
 | 
			
		||||
            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,
 | 
			
		||||
                Err(error) => {
 | 
			
		||||
                    Self::wrangle_handlebars_error_into_diagnostic(
 | 
			
		||||
                        treehouse,
 | 
			
		||||
                        template_file_ids["_tree.hbs"],
 | 
			
		||||
                        template_file_ids[&template_name],
 | 
			
		||||
                        error.line_no,
 | 
			
		||||
                        error.column_no,
 | 
			
		||||
                        error.desc,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -114,15 +114,22 @@ pub fn branch_to_html(
 | 
			
		|||
                            .map(|&branch_id| {
 | 
			
		||||
                                (
 | 
			
		||||
                                    format!(
 | 
			
		||||
                                        "/b?{}",
 | 
			
		||||
                                        "{}/b?{}",
 | 
			
		||||
                                        config.site,
 | 
			
		||||
                                        treehouse.tree.branch(branch_id).attributes.id
 | 
			
		||||
                                    )
 | 
			
		||||
                                    .into(),
 | 
			
		||||
                                    "".into(),
 | 
			
		||||
                                )
 | 
			
		||||
                            }),
 | 
			
		||||
                        "page" => {
 | 
			
		||||
                            Some((format!("{}/{}.html", config.site, linked).into(), "".into()))
 | 
			
		||||
                        }
 | 
			
		||||
                        "pic" => config.pics.get(linked).map(|filename| {
 | 
			
		||||
                            (format!("/static/pic/{}", &filename).into(), "".into())
 | 
			
		||||
                            (
 | 
			
		||||
                                format!("{}/static/pic/{}", config.site, &filename).into(),
 | 
			
		||||
                                "".into(),
 | 
			
		||||
                            )
 | 
			
		||||
                        }),
 | 
			
		||||
                        _ => None,
 | 
			
		||||
                    })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,11 @@ use serde::{Deserialize, Serialize};
 | 
			
		|||
/// Top-level `%%` root attributes.
 | 
			
		||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
 | 
			
		||||
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.
 | 
			
		||||
    ///
 | 
			
		||||
    /// The page's tree path is used if empty.
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +31,11 @@ pub struct RootAttributes {
 | 
			
		|||
    /// These are relative to the /static/css directory.
 | 
			
		||||
    #[serde(default)]
 | 
			
		||||
    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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -369,6 +369,16 @@ th {
 | 
			
		|||
    --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. */
 | 
			
		||||
 | 
			
		||||
.noscript {
 | 
			
		||||
| 
						 | 
				
			
			@ -413,6 +423,7 @@ nav {
 | 
			
		|||
 | 
			
		||||
nav .nav-page {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -435,8 +446,49 @@ h1.page-title {
 | 
			
		|||
    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 {
 | 
			
		||||
    margin-top: 4rem;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
| 
						 | 
				
			
			@ -561,6 +613,9 @@ th-literate-program[data-mode="output"] {
 | 
			
		|||
        border-style: none;
 | 
			
		||||
        border-radius: 4px;
 | 
			
		||||
        display: block;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    & img.placeholder.js {
 | 
			
		||||
        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
									
								
							
							
						
						
									
										103
									
								
								static/css/new.css
									
										
									
									
									
										Normal 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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -184,6 +184,7 @@ class OutputMode {
 | 
			
		|||
        });
 | 
			
		||||
 | 
			
		||||
        if (this.frame.placeholderImage != null) {
 | 
			
		||||
            this.frame.placeholderImage.classList.add("js");
 | 
			
		||||
            this.frame.placeholderImage.classList.add("loading");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										70
									
								
								static/js/news.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								static/js/news.js
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										35
									
								
								static/js/settings.js
									
										
									
									
									
										Normal 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" });
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
// 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;
 | 
			
		||||
class WebkitMoment extends HTMLLIElement {
 | 
			
		||||
    constructor() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,12 +17,11 @@ function branchIsOpen(branchID) {
 | 
			
		|||
    return branchState[branchID];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Branch extends HTMLLIElement {
 | 
			
		||||
export class Branch extends HTMLLIElement {
 | 
			
		||||
    static branchesByNamedID = new Map();
 | 
			
		||||
    static onAdded = [];
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        this.isLeaf = this.classList.contains("leaf");
 | 
			
		||||
 | 
			
		||||
        this.details = this.childNodes[0];
 | 
			
		||||
| 
						 | 
				
			
			@ -48,16 +47,20 @@ class Branch extends HTMLLIElement {
 | 
			
		|||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let namedID = this.id.split(':')[1];
 | 
			
		||||
        Branch.branchesByNamedID.set(namedID, this);
 | 
			
		||||
        this.namedID = this.id.split(':')[1];
 | 
			
		||||
        Branch.branchesByNamedID.set(this.namedID, this);
 | 
			
		||||
 | 
			
		||||
        if (ulid.isCanonicalUlid(namedID)) {
 | 
			
		||||
            let timestamp = ulid.getTimestamp(namedID);
 | 
			
		||||
        if (ulid.isCanonicalUlid(this.namedID)) {
 | 
			
		||||
            let timestamp = ulid.getTimestamp(this.namedID);
 | 
			
		||||
            let date = document.createElement("span");
 | 
			
		||||
            date.classList.add("branch-date");
 | 
			
		||||
            date.innerText = timestamp.toLocaleDateString();
 | 
			
		||||
            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 {
 | 
			
		||||
    static byLink = new Map();
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
    connectedCallback() {
 | 
			
		||||
        super.connectedCallback();
 | 
			
		||||
 | 
			
		||||
        this.linkedTree = this.getAttribute("data-th-link");
 | 
			
		||||
        LinkedBranch.byLink.set(this.linkedTree, this);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										8
									
								
								template/README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								template/README.md
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										77
									
								
								template/_new.hbs
									
										
									
									
									
										Normal 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>
 | 
			
		||||
| 
						 | 
				
			
			@ -3,161 +3,31 @@
 | 
			
		|||
<html lang="en-US" prefix="og: https://ogp.me/ns#">
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
    <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 }}`;</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}}
 | 
			
		||||
    {{> components/_head.hbs }}
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
    <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>
 | 
			
		||||
    {{#> components/_nav.hbs }}
 | 
			
		||||
 | 
			
		||||
        <div class="nav-page">
 | 
			
		||||
            {{#if page.breadcrumbs}}
 | 
			
		||||
            <ol class="breadcrumbs">
 | 
			
		||||
                {{{ page.breadcrumbs }}}
 | 
			
		||||
            </ol>
 | 
			
		||||
            {{/if}}
 | 
			
		||||
    {{!-- 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}}
 | 
			
		||||
 | 
			
		||||
            {{#if (and (ne page.title config.user.title) (ne page.title page.tree_path))}}
 | 
			
		||||
            <h1 class="page-title">{{ page.title }}</h1>
 | 
			
		||||
            {{/if}}
 | 
			
		||||
        </div>
 | 
			
		||||
    </nav>
 | 
			
		||||
    {{/ components/_nav.hbs }}
 | 
			
		||||
 | 
			
		||||
    <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><details></code> element works.<br>
 | 
			
		||||
                (a <code><details></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>
 | 
			
		||||
    {{> components/_noscript.hbs }}
 | 
			
		||||
    {{> components/_webkit.hbs }}
 | 
			
		||||
 | 
			
		||||
            <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>
 | 
			
		||||
    {{!--
 | 
			
		||||
    NOTE: ~ because components/_tree.hbss must not include any extra indentation, because it may
 | 
			
		||||
    contain pre elements which shouldn't be indented.
 | 
			
		||||
    --}}
 | 
			
		||||
    {{~> components/_tree.hbs }}
 | 
			
		||||
 | 
			
		||||
    <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>
 | 
			
		||||
 | 
			
		||||
    <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>
 | 
			
		||||
    {{!-- For all pages except the one linked from the footer, include the footer icon. --}}
 | 
			
		||||
    {{#if (ne page.tree_path "treehouse")}}
 | 
			
		||||
    {{> components/_footer.hbs }}
 | 
			
		||||
    {{/if}}
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										49
									
								
								template/components/_footer.hbs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								template/components/_footer.hbs
									
										
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										37
									
								
								template/components/_head.hbs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								template/components/_head.hbs
									
										
									
									
									
										Normal 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}}
 | 
			
		||||
							
								
								
									
										23
									
								
								template/components/_nav.hbs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								template/components/_nav.hbs
									
										
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										20
									
								
								template/components/_noscript.hbs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								template/components/_noscript.hbs
									
										
									
									
									
										Normal 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><details></code> element works.<br>
 | 
			
		||||
            (a <code><details></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>
 | 
			
		||||
							
								
								
									
										17
									
								
								template/components/_tree.hbs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								template/components/_tree.hbs
									
										
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										7
									
								
								template/components/_webkit.hbs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								template/components/_webkit.hbs
									
										
									
									
									
										Normal 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>
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue