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

@ -7,7 +7,7 @@ slug = "swiftui-lifecycle"
When SwiftUI was announced in 2019 (oh boy, more than 5 years ago), one of the big things that the Apple engineers emphasized was that a SwiftUI `View` is not like a UIKit/AppKit view. As Apple was at pains to note, SwiftUI may evaluate the `body` property of a `View` arbitrarily often. It's easy to miss an important consequence this has, though: your `View` will also be initilaized arbitrarily often. This is why SwiftUI views are supposed to be structs which are simple value types and can be stack-allocated: constructing a `View` needs to be cheap.
More precisely, what I mean by the title of this post is that the lifetime of a struct that conforms to `View` is unmoored from that of the conceptual thing representing a piece of your user interface.
More precisely, what I mean by the title of this post is that the lifetime of a struct that conforms to `View` is unmoored from that of the conceptual thing representing a piece of your user interface.
<!-- excerpt-end -->
@ -19,7 +19,7 @@ A note on definitions: in this post, I'm using the word 'lifecycle' and 'lifetim
This comes up on social media with some regularity. Every few months there'll be questions about (either directly, or in the form of questions that boil down to) why some view's initializer is being called more than expected. One particularly common case is people migrating from Combine/`ObservableObject` to the iOS 17+ `@Observable`.
This comes up on social media with some regularity. Every few months there'll be questions about (either directly, or in the form of questions that boil down to) why some view's initializer is being called more than expected. One particularly common case is people migrating from Combine/`ObservableObject` to the iOS 17+ `@Observable`.
If you were not paying attention to SwiftUI in the early days, or have only started learning it more recently, there are some important details about the nature of the framework you might have missed. One of the things the SwiftUI engineers emphasized when it was announced was that the `body` property can be called at any time by the framework and may be called as often as the framework likes. What follows from this—that's not entirely obvious or always explicitly stated—is that `View` initializers run with the same frequency. Consider the minimal possible case:

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
}
}