Use tera templates, incorporate templates into the graph

This commit is contained in:
Shadowfacts 2025-01-01 18:31:30 -05:00
parent 1253999961
commit 60858bde24
16 changed files with 734 additions and 359 deletions

429
Cargo.lock generated
View File

@ -96,50 +96,6 @@ version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
[[package]]
name = "askama"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
dependencies = [
"askama_derive",
"askama_escape",
"humansize",
"num-traits",
"percent-encoding",
]
[[package]]
name = "askama_derive"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
dependencies = [
"askama_parser",
"basic-toml",
"mime",
"mime_guess",
"proc-macro2",
"quote",
"serde",
"syn",
]
[[package]]
name = "askama_escape"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
[[package]]
name = "askama_parser"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
dependencies = [
"nom",
]
[[package]]
name = "autocfg"
version = "1.4.0"
@ -161,15 +117,6 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "basic-toml"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8"
dependencies = [
"serde",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@ -182,6 +129,25 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bstr"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
@ -230,6 +196,28 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "chrono-tz"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
dependencies = [
"chrono",
"chrono-tz-build",
"phf",
]
[[package]]
name = "chrono-tz-build"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1"
dependencies = [
"parse-zoneinfo",
"phf",
"phf_codegen",
]
[[package]]
name = "clap"
version = "4.5.23"
@ -289,6 +277,50 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
dependencies = [
"libc",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "debounced"
version = "0.2.0"
@ -306,6 +338,22 @@ dependencies = [
"compute_graph",
]
[[package]]
name = "deunicode"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00"
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@ -487,6 +535,16 @@ dependencies = [
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getopts"
version = "0.2.21"
@ -513,6 +571,30 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "globset"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19"
dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "globwalk"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
dependencies = [
"bitflags 2.6.0",
"ignore",
"walkdir",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
@ -710,6 +792,22 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "ignore"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
dependencies = [
"crossbeam-deque",
"globset",
"log",
"memchr",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
]
[[package]]
name = "indexmap"
version = "2.5.0"
@ -791,6 +889,12 @@ dependencies = [
"libc",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.169"
@ -874,28 +978,6 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.2"
@ -923,16 +1005,6 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "notify"
version = "7.0.0"
@ -1008,12 +1080,66 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "parse-zoneinfo"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
dependencies = [
"regex",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
version = "2.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
dependencies = [
"memchr",
"thiserror",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea"
dependencies = [
"once_cell",
"pest",
"sha2",
]
[[package]]
name = "petgraph"
version = "0.6.6"
@ -1281,6 +1407,17 @@ dependencies = [
"serde",
]
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
@ -1311,6 +1448,16 @@ dependencies = [
"autocfg",
]
[[package]]
name = "slug"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724"
dependencies = [
"deunicode",
"wasm-bindgen",
]
[[package]]
name = "smallvec"
version = "1.13.2"
@ -1398,6 +1545,48 @@ dependencies = [
"utf-8",
]
[[package]]
name = "tera"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee"
dependencies = [
"chrono",
"chrono-tz",
"globwalk",
"humansize",
"lazy_static",
"percent-encoding",
"pest",
"pest_derive",
"rand",
"regex",
"serde",
"serde_json",
"slug",
"unic-segment",
]
[[package]]
name = "thiserror"
version = "2.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinystr"
version = "0.7.6"
@ -1497,6 +1686,68 @@ dependencies = [
"winnow",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unic-char-property"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
dependencies = [
"unic-char-range",
]
[[package]]
name = "unic-char-range"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
[[package]]
name = "unic-common"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
[[package]]
name = "unic-segment"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
dependencies = [
"unic-ucd-segment",
]
[[package]]
name = "unic-ucd-segment"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
dependencies = [
"unic-char-property",
"unic-char-range",
"unic-ucd-version",
]
[[package]]
name = "unic-ucd-version"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
dependencies = [
"unic-common",
]
[[package]]
name = "unicase"
version = "2.8.1"
@ -1564,7 +1815,6 @@ name = "v7"
version = "0.1.0"
dependencies = [
"anyhow",
"askama",
"chrono",
"clap",
"compute_graph",
@ -1580,6 +1830,7 @@ dependencies = [
"regex",
"serde",
"serde_json",
"tera",
"tokio",
"tokio-stream",
"toml",
@ -1587,6 +1838,12 @@ dependencies = [
"url",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"

View File

@ -17,7 +17,6 @@ serde_json = "1.0"
[dependencies]
anyhow = "1.0.95"
askama = "0.12.1"
chrono = { version = "0.4.39", features = ["serde"] }
clap = { version = "4.5.23", features = ["cargo"] }
compute_graph = { path = "crates/compute_graph" }
@ -32,6 +31,7 @@ once_cell = "1.20.2"
pulldown-cmark = "0.12.2"
regex = "1.11.1"
serde = { version = "1.0", features = ["derive"] }
tera = "1.20.0"
tokio = { version = "1.42.0", features = ["full"] }
tokio-stream = "0.1.17"
toml = "0.8.19"

View File

@ -1,2 +0,0 @@
[general]
dirs = ["site_test"]

View File

@ -1,13 +1,15 @@
{% extends "layout/default.html" %}
{% extends "default" %}
{% block title %}Archive{% endblock %}
{% block titlevariable %}
{% set title = "Archive" %}
{% endblock %}
{% block content -%}
{% for year in self.years() %}
{% for year in years %}
<h2>{{ year }}</h2>
<ul>
{% for entry in self.posts_for_year(year) %}
{% for entry in posts_by_year[year] %}
<li>
<a href="{{ entry.permalink }}">
{{ entry.title }}

View File

@ -1,30 +1,36 @@
{% extends "layout/default.html" %}
{% extends "default" %}
{% block titlevariable %}
{% set title = metadata.title %}
{% endblock %}
{% block head -%}
<meta property="og:type" content="article">
{% match post.metadata.short_desc %}
{% when Some with (val) %}
<meta property="og:description" content="{{ val }}">
{% when None %}
<meta property="og:description" content="The outer part of a shadow is called the penumbra.">
{% endmatch %}
{% if metadata.short_desc %}
<meta property="og:description" content="{{ metadata.short_desc }}">
{% else %}
<meta property="og:description" content="The outer part of a shadow is called the penumbra.">
{% endif %}
{%- endblock %}
{% block image %}
{% match post.metadata.card_image_path %}
{% when Some with (path) %}
<meta property="twitter:image" content="https://{{ Self::domain() }}{{ path }}">
<meta property="og:image" content="https://{{ Self::domain() }}{{ path }}">
{% when None %}
<meta property="twitter:image" content="https://{{ Self::domain() }}/shadowfacts.png">
<meta property="og:image" content="https://{{ Self::domain() }}/shadowfacts.png">
{% endmatch %}
{% if metadata.card_image_path %}
<meta property="twitter:image" content="https://{{ _domain }}{{ metadata.card_image_path }}">
<meta property="og:image" content="https://{{ _domain }}{{ metadata.card_image_path }}">
{% else %}
<meta property="twitter:image" content="https://{{ _domain }}/shadowfacts.png">
<meta property="og:image" content="https://{{ _domain }}/shadowfacts.png">
{% endif %}
{% endblock %}
{% block title %}{{ post.metadata.title }}{% endblock %}
{% block content -%}
<article itemprop="blogPost" itemscope itemtype="https://schema.org/BlogPosting">
<meta itemprop="mainEntityOfPage" content="https://{{ Self::domain() }}{{ self.permalink() }}">
<meta itemprop="mainEntityOfPage" content="https://{{ _domain }}{{ _permalink }}">
</article>
{%- endblock %}

View File

@ -5,10 +5,13 @@
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{% block title %}Shadowfacts{% endblock %}</title>
{% block titlevariable %}
{% set title = "Shadowfacts" %}
{% endblock %}
<title>{{ title }}</title>
<link rel="cannonical" href="https://{{ Self::domain() }}{{ self.permalink() }}">
<link rel="alternate" type="application/rss+xml" title="Shadowfacts" href="https://{{ Self::domain() }}/feed.xml">
<link rel="cannonical" href="https://{{ _domain }}{{ _permalink }}">
<link rel="alternate" type="application/rss+xml" title="Shadowfacts" href="https://{{ _domain }}/feed.xml">
<link rel="icon" href="/favicon.ico">
<link rel="apple-touch-icon-precomposed" href="/favicon-152.png">
@ -16,17 +19,17 @@
<meta name="msapplication-TileImage" content="/favicon-152.png">
<meta name="twitter:card" content="summary">
<meta property="og:title" content="{% block title %}{% endblock %}">
<meta property="og:title" content="{{ title }}">
{% block image %}
<meta property="twitter:image" content="https://{{ Self::domain() }}/shadowfacts.png">
<meta property="og:image" content="https://{{ Self::domain() }}/shadowfacts.png">
<meta property="twitter:image" content="https://{{ _domain }}/shadowfacts.png">
<meta property="og:image" content="https://{{ _domain }}/shadowfacts.png">
{% endblock %}
<meta property="og:url" content="https://{{ Self::domain() }}{{ self.permalink() }}">
<meta property="og:url" content="https://{{ _domain }}{{ _permalink }}">
<meta property="og:site_name" content="Shadowfacts">
{% block head %}{% endblock %}
<link rel="stylesheet" href="/css/main.css?{{ Self::stylesheet_cache_buster() }}">
<link rel="stylesheet" href="/css/main.css?{{ _stylesheet_cache_buster }}">
</head>
<body itemscope itemtype="https://schema.org/Blog">

View File

@ -1,9 +1,21 @@
{% extends "layout/default.html" %}
{% extends "default" %}
{% block title %}{{ tag.name }} posts{% endblock %}
{% block titlevariable %}
{% set title = tag_name ~ " posts" %}
{% endblock %}
{% block content -%}
<h1>{{ tag.name }} posts</h1>
<h1>{{ tag_name }} posts</h1>
<ul>
{% for entry in posts %}
<li>
<a href="{{ entry.permalink }}">
{{ entry.title }}
</a>
</li>
{% endfor %}
</ul>
{%- endblock %}

View File

@ -1,25 +1,54 @@
use std::collections::HashMap;
use askama::Template;
use chrono::Datelike;
use compute_graph::{
builder::GraphBuilder,
rule::{Input, InputVisitable, Rule},
synchronicity::Asynchronous,
};
use serde::Serialize;
use crate::generator::templates::{BuildTemplateContext, RenderTemplate};
use super::{
FileWatcher,
posts::content::{HtmlContent, Post},
util::{output_rendered_template, templates::TemplateCommon},
templates::{AddTemplate, Templates},
util::content_path,
};
pub fn make_graph(
builder: &mut GraphBuilder<(), Asynchronous>,
posts: Input<Vec<Post<HtmlContent>>>,
default_template: Input<Templates>,
watcher: &mut FileWatcher,
) -> Input<()> {
let entries = builder.add_rule(Entries(posts));
let posts_by_year = builder.add_rule(PostsByYear(entries));
builder.add_rule(Archive(posts_by_year))
let archive_path = content_path("archive.html");
let (archive_template, invalidate_template) = builder.add_invalidatable_rule(AddTemplate::new(
"archive",
archive_path.clone(),
default_template,
));
watcher.watch(archive_path, move || invalidate_template.invalidate());
let context = builder.add_rule(BuildTemplateContext::new(
"/archive/".into(),
posts_by_year,
|map, ctx| {
ctx.insert("years", &map.years());
ctx.insert("posts_by_year", &map.0);
},
));
builder.add_rule(RenderTemplate {
name: "archive",
output_path: "/archive/index.html".into(),
templates: archive_template,
context,
})
}
#[derive(InputVisitable)]
@ -54,53 +83,18 @@ impl Rule for PostsByYear {
#[derive(PartialEq)]
struct PostsYearMap(HashMap<i32, Vec<Entry>>);
#[derive(PartialEq, Clone)]
impl PostsYearMap {
fn years(&self) -> Vec<i32> {
let mut years = self.0.keys().cloned().collect::<Vec<_>>();
years.sort();
years.reverse();
years
}
}
#[derive(PartialEq, Clone, Serialize)]
struct Entry {
permalink: String,
title: String,
year: i32,
}
#[derive(InputVisitable)]
struct Archive(Input<PostsYearMap>);
impl Rule for Archive {
type Output = ();
fn evaluate(&mut self) -> Self::Output {
output_rendered_template(
&ArchiveTemplate {
map: &*self.input_0(),
},
"archive/index.html",
)
.expect("writing archive")
}
}
#[derive(Template)]
#[template(path = "archive.html")]
struct ArchiveTemplate<'a> {
map: &'a PostsYearMap,
}
impl<'a> TemplateCommon for ArchiveTemplate<'a> {}
impl<'a> ArchiveTemplate<'a> {
fn permalink(&self) -> &'static str {
"/archive/"
}
fn years(&self) -> Vec<i32> {
let mut years = self.map.0.keys().cloned().collect::<Vec<_>>();
years.sort();
years.reverse();
years
}
fn posts_for_year(&self, year: &i32) -> &[Entry] {
self.map
.0
.get(year)
.map(|vec| vec.as_slice())
.unwrap_or(&[])
}
}

View File

@ -2,6 +2,7 @@ mod archive;
mod markdown;
mod posts;
mod tags;
mod templates;
mod util;
use std::cell::RefCell;
@ -28,11 +29,24 @@ pub async fn generate(watcher: Rc<RefCell<FileWatcher>>) -> anyhow::Result<Async
fn make_graph(watcher: Rc<RefCell<FileWatcher>>) -> anyhow::Result<AsyncGraph<()>> {
let mut builder = GraphBuilder::new_async();
let (void_outputs, posts, all_posts, post_metadatas) = posts::make_graph(&mut builder, watcher);
let default_template = templates::make_graph(&mut builder, &mut *watcher.borrow_mut());
let archive = archive::make_graph(&mut builder, all_posts);
let (void_outputs, posts, all_posts, post_metadatas) =
posts::make_graph(&mut builder, default_template.clone(), Rc::clone(&watcher));
let tag_output = tags::make_graph(&mut builder, posts);
let archive = archive::make_graph(
&mut builder,
all_posts,
default_template.clone(),
&mut *watcher.borrow_mut(),
);
let tag_output = tags::make_graph(
&mut builder,
posts,
default_template,
&mut *watcher.borrow_mut(),
);
let post_metadatas_voided = builder.add_rule(MapToVoid(post_metadatas));
let output = Combine::make(&mut builder, &[

View File

@ -5,7 +5,6 @@ use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
use askama::Template;
use compute_graph::{
builder::GraphBuilder,
rule::{
@ -17,16 +16,17 @@ use compute_graph::{
use content::{HtmlContent, Post};
use log::error;
use metadata::PostMetadata;
use crate::generator::util::output_rendered_template;
use tera::Context;
use super::{
FileWatcher,
util::{MapDynamicToVoid, content_path, templates::TemplateCommon},
templates::{AddTemplate, BuildTemplateContext, RenderTemplate, Templates},
util::{MapDynamicToVoid, content_path},
};
pub fn make_graph(
builder: &mut GraphBuilder<(), Asynchronous>,
default_template: Input<Templates>,
watcher: Rc<RefCell<FileWatcher>>,
) -> (
Input<()>,
@ -39,11 +39,22 @@ pub fn make_graph(
invalidate_posts.invalidate();
});
let posts = builder.add_dynamic_rule(MakeReadNodes::new(post_files, watcher));
let posts = builder.add_dynamic_rule(MakeReadNodes::new(post_files, Rc::clone(&watcher)));
let extract_metadatas = builder.add_dynamic_rule(MakeExtractMetadatas::new(posts.clone()));
let write_posts = builder.add_dynamic_rule(MakeWritePosts::new(posts.clone()));
let article_path = content_path("layout/article.html");
let (article_template, invalidate_template) = builder.add_invalidatable_rule(AddTemplate::new(
"article",
article_path.clone(),
default_template,
));
watcher
.borrow_mut()
.watch(article_path, move || invalidate_template.invalidate());
let write_posts =
builder.add_dynamic_rule(MakeWritePosts::new(posts.clone(), article_template));
(
builder.add_rule(MapDynamicToVoid(write_posts)),
@ -105,7 +116,7 @@ impl DynamicRule for MakeReadNodes {
type ChildOutput = ReadPostOutput;
fn evaluate(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec<Input<Self::ChildOutput>> {
for file in self.files.value().iter() {
self.node_factory.add_rule(ctx, file.clone(), |ctx| {
self.node_factory.add_node(ctx, file.clone(), |ctx| {
let (input, signal) = ctx.add_invalidatable_rule(ReadPost { path: file.clone() });
self.watcher
.borrow_mut()
@ -170,7 +181,7 @@ impl DynamicRule for MakeExtractMetadatas {
for post_input in self.posts.value().inputs.iter() {
let post_ = post_input.value();
let post = post_.as_ref().unwrap();
self.node_factory.add_rule(ctx, post.path.clone(), |ctx| {
self.node_factory.add_node(ctx, post.path.clone(), |ctx| {
ctx.add_rule(ExtractMetadata(post_input.clone()))
});
}
@ -201,13 +212,19 @@ impl Rule for ExtractMetadata {
#[derive(InputVisitable)]
struct MakeWritePosts {
posts: DynamicInput<ReadPostOutput>,
node_factory: DynamicNodeFactory<PathBuf, ()>,
article_template: Input<Templates>,
unwrapped_factory: DynamicNodeFactory<PathBuf, Post<HtmlContent>>,
build_context_factory: DynamicNodeFactory<PathBuf, Context>,
render_factory: DynamicNodeFactory<PathBuf, ()>,
}
impl MakeWritePosts {
fn new(posts: DynamicInput<ReadPostOutput>) -> Self {
fn new(posts: DynamicInput<ReadPostOutput>, templates: Input<Templates>) -> Self {
Self {
posts,
node_factory: DynamicNodeFactory::new(),
article_template: templates,
unwrapped_factory: DynamicNodeFactory::new(),
build_context_factory: DynamicNodeFactory::new(),
render_factory: DynamicNodeFactory::new(),
}
}
}
@ -216,44 +233,35 @@ impl DynamicRule for MakeWritePosts {
fn evaluate(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec<Input<Self::ChildOutput>> {
for post_input in self.posts.value().inputs.iter() {
if let Some(post) = post_input.value().as_ref() {
self.node_factory.add_rule(ctx, post.path.clone(), |ctx| {
ctx.add_rule(WritePost(post_input.clone()))
let context = self
.build_context_factory
.add_node(ctx, post.path.clone(), |ctx| {
ctx.add_rule(BuildTemplateContext::new(
post.permalink().into(),
post_input.clone(),
|post_opt, ctx| {
let post = post_opt.as_ref().unwrap();
ctx.insert("metadata", &post.metadata);
ctx.insert("content", post.content.html());
},
))
});
let mut output_path = PathBuf::from(post.permalink());
output_path.push("index.html");
self.render_factory.add_node(ctx, post.path.clone(), |ctx| {
ctx.add_rule(RenderTemplate {
name: "article",
output_path,
templates: self.article_template.clone(),
context,
})
});
}
}
self.node_factory.all_nodes(ctx)
}
}
#[derive(InputVisitable)]
struct WritePost(Input<ReadPostOutput>);
impl Rule for WritePost {
type Output = ();
fn evaluate(&mut self) -> Self::Output {
let post_ = &self.input_0();
let post = post_.as_ref().unwrap();
let mut path = PathBuf::from(post.permalink());
path.push("index.html");
output_rendered_template(&ArticleTemplate { post }, path).expect("writing post");
}
fn node_label(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.input_0().as_ref().unwrap().slug)
}
}
#[derive(Template)]
#[template(path = "layout/article.html")]
struct ArticleTemplate<'a> {
post: &'a Post<HtmlContent>,
}
impl<'a> TemplateCommon for ArticleTemplate<'a> {}
impl<'a> ArticleTemplate<'a> {
fn permalink(&self) -> String {
self.post.permalink()
self.unwrapped_factory.finalize_nodes(ctx);
self.build_context_factory.finalize_nodes(ctx);
self.render_factory.all_nodes(ctx)
}
}

View File

@ -1,12 +1,12 @@
use std::hash::Hash;
use chrono::{DateTime, FixedOffset};
use serde::Deserialize;
use serde::de::{SeqAccess, Visitor};
use serde::{Deserialize, Serialize};
use crate::generator::util::slugify::slugify;
#[derive(Debug, Deserialize, PartialEq, Clone)]
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
#[serde(deny_unknown_fields)]
pub struct PostMetadata {
pub title: String,
@ -68,7 +68,7 @@ where
deserializer.deserialize_any(StringOrVec)
}
#[derive(Debug, Eq, Clone)]
#[derive(Debug, Eq, Clone, Serialize)]
pub struct Tag {
pub name: String,
pub slug: String,

View File

@ -1,26 +1,37 @@
use std::{collections::HashMap, path::PathBuf};
use std::collections::HashMap;
use askama::Template;
use compute_graph::{
builder::GraphBuilder,
rule::{DynamicInput, DynamicNodeFactory, DynamicRule, Input, InputVisitable, Rule},
synchronicity::Asynchronous,
};
use serde::Serialize;
use tera::Context;
use super::{
posts::{
ReadPostOutput,
metadata::{PostMetadata, Tag},
},
util::{MapDynamicToVoid, output_rendered_template, templates::TemplateCommon},
FileWatcher,
posts::{ReadPostOutput, metadata::Tag},
templates::{AddTemplate, BuildTemplateContext, RenderTemplate, Templates},
util::{MapDynamicToVoid, content_path},
};
pub fn make_graph(
builder: &mut GraphBuilder<(), Asynchronous>,
posts: DynamicInput<ReadPostOutput>,
default_template: Input<Templates>,
watcher: &mut FileWatcher,
) -> Input<()> {
let by_tags = builder.add_dynamic_rule(MakePostsByTags::new(posts));
let write_tags = builder.add_dynamic_rule(MakeWriteTagPages::new(by_tags));
let template_path = content_path("tag.html");
let (tag_template, invalidate_template) = builder.add_invalidatable_rule(AddTemplate::new(
"tag",
template_path.clone(),
default_template,
));
watcher.watch(template_path, move || invalidate_template.invalidate());
let write_tags = builder.add_dynamic_rule(MakeWriteTagPages::new(by_tags, tag_template));
builder.add_rule(MapDynamicToVoid(write_tags))
}
@ -51,7 +62,7 @@ impl DynamicRule for MakePostsByTags {
}
}
for (slug, name) in all_tags {
self.node_factory.add_rule(ctx, slug.clone(), |ctx| {
self.node_factory.add_node(ctx, slug.clone(), |ctx| {
ctx.add_rule(PostsByTag {
posts: self.posts.clone(),
tag: Tag { slug, name },
@ -82,7 +93,7 @@ impl Rule for PostsByTag {
if tags.any(|t| t.slug == self.tag.slug) {
Some(Entry {
permalink: post.permalink(),
metadata: post.metadata.clone(),
title: post.metadata.title.clone(),
})
} else {
None
@ -106,22 +117,27 @@ struct TagAndPosts {
entries: Vec<Entry>,
}
#[derive(PartialEq, Clone)]
#[derive(PartialEq, Clone, Serialize)]
struct Entry {
permalink: String,
metadata: PostMetadata,
title: String,
}
#[derive(InputVisitable)]
struct MakeWriteTagPages {
tags: DynamicInput<TagAndPosts>,
node_factory: DynamicNodeFactory<String, ()>,
#[ignore_input]
templates: Input<Templates>,
build_context_factory: DynamicNodeFactory<String, Context>,
render_factory: DynamicNodeFactory<String, ()>,
}
impl MakeWriteTagPages {
fn new(tags: DynamicInput<TagAndPosts>) -> Self {
fn new(tags: DynamicInput<TagAndPosts>, templates: Input<Templates>) -> Self {
Self {
tags,
node_factory: DynamicNodeFactory::new(),
templates,
build_context_factory: DynamicNodeFactory::new(),
render_factory: DynamicNodeFactory::new(),
}
}
}
@ -133,50 +149,29 @@ impl DynamicRule for MakeWriteTagPages {
) -> Vec<Input<Self::ChildOutput>> {
for tag_input in self.tags.value().inputs.iter() {
let tag_and_posts = tag_input.value();
self.node_factory
.add_rule(ctx, tag_and_posts.tag.slug.clone(), |ctx| {
ctx.add_rule(WriteTag(tag_input.clone()))
let slug = &tag_and_posts.tag.slug;
let template_context = self
.build_context_factory
.add_node(ctx, slug.clone(), |ctx| {
ctx.add_rule(BuildTemplateContext::new(
format!("/{slug}/").into(),
tag_input.clone(),
|tag_and_posts, ctx| {
ctx.insert("tag_name", &tag_and_posts.tag.name);
ctx.insert("posts", &tag_and_posts.entries);
},
))
});
self.render_factory.add_node(ctx, slug.clone(), |ctx| {
ctx.add_rule(RenderTemplate {
name: "tag",
output_path: format!("/{slug}/index.html").into(),
templates: self.templates.clone(),
context: template_context,
})
});
}
self.node_factory.all_nodes(ctx)
}
}
#[derive(InputVisitable)]
struct WriteTag(Input<TagAndPosts>);
impl Rule for WriteTag {
type Output = ();
fn evaluate(&mut self) -> Self::Output {
let tag_and_posts = self.input_0();
let mut path = PathBuf::from(&tag_and_posts.tag.slug);
path.push("index.html");
output_rendered_template(
&TagTemplate {
tag: &tag_and_posts.tag,
posts: &tag_and_posts.entries,
},
path,
)
.expect("writing tag page");
}
fn node_label(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.input_0().tag.slug)
}
}
#[derive(Template)]
#[template(path = "tag.html")]
struct TagTemplate<'a> {
tag: &'a Tag,
posts: &'a [Entry],
}
impl<'a> TemplateCommon for TagTemplate<'a> {}
impl<'a> TagTemplate<'a> {
fn permalink(&self) -> String {
format!("/{}/", self.tag.slug)
self.build_context_factory.finalize_nodes(ctx);
self.render_factory.all_nodes(ctx)
}
}

131
src/generator/templates.rs Normal file
View File

@ -0,0 +1,131 @@
use std::{borrow::Cow, path::PathBuf, time::SystemTime};
use compute_graph::{
builder::GraphBuilder,
node::NodeValue,
rule::{Input, InputVisitable, Rule},
synchronicity::Asynchronous,
};
use log::error;
use once_cell::sync::Lazy;
use tera::{Context, Tera};
use crate::generator::util::output_writer;
use super::{FileWatcher, util::content_path};
pub fn make_graph(
builder: &mut GraphBuilder<(), Asynchronous>,
watcher: &mut FileWatcher,
) -> Input<Templates> {
let empty_templates = builder.add_value(Templates::default());
let default_path = content_path("layout/default.html");
let (default, invalidate_default) = builder.add_invalidatable_rule(AddTemplate::new(
"default",
default_path.clone(),
empty_templates,
));
watcher.watch(default_path, move || invalidate_default.invalidate());
default
}
#[derive(InputVisitable)]
pub struct AddTemplate(String, PathBuf, Input<Templates>);
impl AddTemplate {
pub fn new(name: &str, path: PathBuf, base: Input<Templates>) -> Self {
Self(name.into(), path, base)
}
}
impl Rule for AddTemplate {
type Output = Templates;
fn evaluate(&mut self) -> Self::Output {
let mut templates = self.input_2().clone();
let content = std::fs::read_to_string(&self.1).expect("reading template");
let result = templates.tera.add_raw_template(&self.0, &content);
if let Err(e) = result {
error!("Error adding template {:?}: {:?}", &self.1, e)
}
templates.templates.push(content);
templates
}
}
#[derive(Default, Clone)]
pub struct Templates {
templates: Vec<String>,
tera: Tera,
}
impl NodeValue for Templates {
fn node_value_eq(&self, other: &Self) -> bool {
self.templates == other.templates
}
}
static DOMAIN: Lazy<String> =
Lazy::new(|| std::env::var("DOMAIN").unwrap_or("shadowfacts.net".to_owned()));
static CB: Lazy<u64> = Lazy::new(|| {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs()
});
#[derive(InputVisitable)]
pub struct BuildTemplateContext<T, F> {
permalink: Cow<'static, str>,
input: Input<T>,
func: F,
}
impl<T, F: Fn(&T, &mut Context) -> ()> BuildTemplateContext<T, F> {
pub fn new(permalink: Cow<'static, str>, input: Input<T>, func: F) -> Self {
Self {
permalink,
input,
func,
}
}
}
impl<T: 'static, F: Fn(&T, &mut Context) -> () + 'static> Rule for BuildTemplateContext<T, F> {
type Output = Context;
fn evaluate(&mut self) -> Self::Output {
let mut context = Context::new();
(self.func)(&*self.input(), &mut context);
context.insert("_domain", &*DOMAIN);
context.insert("_permalink", &self.permalink);
context.insert("_stylesheet_cache_buster", &*CB);
context
}
}
#[derive(InputVisitable)]
pub struct RenderTemplate {
pub name: &'static str,
pub output_path: PathBuf,
pub templates: Input<Templates>,
pub context: Input<Context>,
}
impl Rule for RenderTemplate {
type Output = ();
fn evaluate(&mut self) -> Self::Output {
let templates = self.templates();
assert!(templates.tera.get_template_names().any(|n| n == self.name));
let writer = output_writer(&self.output_path).expect("output writer");
let result = templates
.tera
.render_to(&self.name, &*self.context(), writer);
if let Err(e) = result {
error!(
"Error rendering template to {:?}: {:?}",
&self.output_path, e
);
}
}
fn node_label(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.output_path.display())
}
}

View File

@ -8,7 +8,6 @@ use std::io::{BufWriter, Write};
use std::path::{Component, Path, PathBuf};
use anyhow::anyhow;
use askama::Template;
use compute_graph::builder::GraphBuilder;
use compute_graph::rule::{DynamicInput, Input, InputVisitable, Rule};
use compute_graph::synchronicity::Synchronicity;
@ -23,38 +22,6 @@ pub fn output_writer(path: impl AsRef<Path>) -> Result<impl Write, std::io::Erro
Ok(BufWriter::new(file))
}
pub fn output_rendered_template(
template: &impl Template,
file: impl AsRef<Path>,
) -> Result<(), std::io::Error> {
let path = file.as_ref();
let writer = output_writer(path)?;
template.render_into(&mut FmtWriter(writer)).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("writing template {}: {}", path.display(), e),
)
})
}
struct FmtWriter<W: Write>(W);
impl<W: Write> std::fmt::Write for FmtWriter<W> {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
self.0.write_all(s.as_bytes()).map_err(|_| std::fmt::Error)
}
fn write_char(&mut self, c: char) -> std::fmt::Result {
let mut buf = [0u8; 4];
c.encode_utf8(&mut buf);
self.0.write_all(&buf).map_err(|_| std::fmt::Error)
}
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::fmt::Result {
self.0.write_fmt(args).map_err(|_| std::fmt::Error)
}
}
pub fn from_frontmatter<D: DeserializeOwned>(contents: &str) -> anyhow::Result<(D, &str)> {
let mut chars = contents.char_indices();
for i in 0..=2 {

View File

@ -1,59 +1,53 @@
use chrono::{DateTime, Local};
use once_cell::sync::Lazy;
use std::time::SystemTime;
// static DOMAIN: Lazy<String> =
// Lazy::new(|| std::env::var("DOMAIN").unwrap_or("shadowfacts.net".to_owned()));
static DOMAIN: Lazy<String> =
Lazy::new(|| std::env::var("DOMAIN").unwrap_or("shadowfacts.net".to_owned()));
// static CB: Lazy<u64> = Lazy::new(|| {
// SystemTime::now()
// .duration_since(SystemTime::UNIX_EPOCH)
// .unwrap()
// .as_secs()
// });
static CB: Lazy<u64> = Lazy::new(|| {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs()
});
// static GENERATED_AT: Lazy<DateTime<Local>> = Lazy::new(|| Local::now());
static GENERATED_AT: Lazy<DateTime<Local>> = Lazy::new(|| Local::now());
// pub trait TemplateCommon {
// fn domain() -> String {
// DOMAIN.to_owned()
// }
pub trait TemplateCommon {
fn domain() -> String {
DOMAIN.to_owned()
}
// fn stylesheet_cache_buster() -> u64 {
// *CB
// }
fn stylesheet_cache_buster() -> u64 {
*CB
}
fn generated_at() -> &'static DateTime<Local> {
&*GENERATED_AT
}
}
// fn generated_at() -> &'static DateTime<Local> {
// &*GENERATED_AT
// }
// }
pub mod filters {
use std::fmt::Display;
use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc};
pub fn iso_date(date: &NaiveDate) -> askama::Result<String> {
Ok(
Utc::from_utc_datetime(&Utc, &date.and_hms_opt(12, 0, 0).unwrap())
.format("%+:0")
.to_string(),
)
pub fn iso_date(date: &NaiveDate) -> String {
Utc::from_utc_datetime(&Utc, &date.and_hms_opt(12, 0, 0).unwrap())
.format("%+:0")
.to_string()
}
pub fn iso_datetime<Tz>(datetime: &DateTime<Tz>) -> askama::Result<String>
pub fn iso_datetime<Tz>(datetime: &DateTime<Tz>) -> String
where
Tz: TimeZone,
Tz::Offset: Display,
{
Ok(datetime.format("%+:0").to_string())
datetime.format("%+:0").to_string()
}
const MONTHS: &[&str; 12] = &[
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
pub fn pretty_date(date: &impl Datelike) -> askama::Result<String> {
pub fn pretty_date(date: &impl Datelike) -> String {
let month = MONTHS[date.month0() as usize];
let suffix = match date.day() {
1 | 21 | 31 => "st",
@ -61,29 +55,23 @@ pub mod filters {
3 | 23 => "rd",
_ => "th",
};
Ok(format!(
"{} {}{}, {}",
month,
date.day(),
suffix,
date.year()
))
format!("{} {}{}, {}", month, date.day(), suffix, date.year())
}
pub fn pretty_datetime<Tz>(datetime: &DateTime<Tz>) -> askama::Result<String>
pub fn pretty_datetime<Tz>(datetime: &DateTime<Tz>) -> String
where
Tz: TimeZone,
Tz::Offset: Display,
{
Ok(format!(
format!(
"{} {}",
datetime.format("%-I:%M:%S %p"),
pretty_date(datetime)?
))
pretty_date(datetime)
)
}
pub fn reading_time(words: &u32) -> askama::Result<u32> {
pub fn reading_time(words: &u32) -> u32 {
let wpm = 225.0;
return Ok((*words as f32 / wpm).max(1.0) as u32);
(*words as f32 / wpm).max(1.0) as u32
}
}