Misc changes

This commit is contained in:
Shadowfacts 2025-02-17 00:51:13 -05:00
parent ea05eb2aa7
commit c53dd56d0f
27 changed files with 312 additions and 192 deletions

View File

@ -1,6 +1,6 @@
The two parts of this work are covered by separate licenses. The two parts of this work are covered by separate licenses.
The 'src/' directory (static site generator and backend code) is licensed under the Creative Commons BY-NC-SA 4.0 license. The 'src/' and `crates/` directories (static site generator and backend code) is licensed under the Creative Commons BY-NC-SA 4.0 license.
A copy of the license is available in the 'src/LICENSE' file. A copy of the license is available in the 'src/LICENSE' file.
The 'site/' directory (the contents of the website and the frontend code) is not publicly licensed and all rights are reserved. The 'site/' directory (the contents of the website and the frontend code) is not publicly licensed and all rights are reserved.

View File

@ -1,7 +0,0 @@
digraph {
0 [label ="ConstNode<i32> (id=0)"]
1 [label ="ConstNode<i32> (id=1)"]
2 [label ="RuleNode<compute_graph::tests::Add> (id=2)"]
0 -> 2 []
1 -> 2 []
}

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 10.0.1 (20240210.2158)
-->
<!-- Pages: 1 -->
<svg width="432pt" height="116pt"
viewBox="0.00 0.00 432.02 116.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 112)">
<polygon fill="white" stroke="none" points="-4,4 -4,-112 428.02,-112 428.02,4 -4,4"/>
<!-- 0 -->
<g id="node1" class="node">
<title>0</title>
<ellipse fill="none" stroke="black" cx="101.51" cy="-90" rx="101.51" ry="18"/>
<text text-anchor="middle" x="101.51" y="-84.95" font-family="Times,serif" font-size="14.00">ConstNode&lt;i32&gt; (id=0)</text>
</g>
<!-- 2 -->
<g id="node3" class="node">
<title>2</title>
<ellipse fill="none" stroke="black" cx="211.51" cy="-18" rx="185.96" ry="18"/>
<text text-anchor="middle" x="211.51" y="-12.95" font-family="Times,serif" font-size="14.00">RuleNode&lt;compute_graph::tests::Add&gt; (id=2)</text>
</g>
<!-- 0&#45;&gt;2 -->
<g id="edge1" class="edge">
<title>0&#45;&gt;2</title>
<path fill="none" stroke="black" d="M127.86,-72.23C142.07,-63.19 159.82,-51.89 175.31,-42.03"/>
<polygon fill="black" stroke="black" points="176.78,-45.25 183.33,-36.93 173.02,-39.34 176.78,-45.25"/>
</g>
<!-- 1 -->
<g id="node2" class="node">
<title>1</title>
<ellipse fill="none" stroke="black" cx="322.51" cy="-90" rx="101.51" ry="18"/>
<text text-anchor="middle" x="322.51" y="-84.95" font-family="Times,serif" font-size="14.00">ConstNode&lt;i32&gt; (id=1)</text>
</g>
<!-- 1&#45;&gt;2 -->
<g id="edge2" class="edge">
<title>1&#45;&gt;2</title>
<path fill="none" stroke="black" d="M295.92,-72.23C281.58,-63.19 263.67,-51.89 248.04,-42.03"/>
<polygon fill="black" stroke="black" points="250.26,-39.3 239.93,-36.92 246.52,-45.22 250.26,-39.3"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -6,17 +6,19 @@
{% block content -%} {% block content -%}
<h1>Archive</h1> <h1 class="headline">Archive</h1>
{% for year in years %} {% for year in years %}
<div class="archive-list"> <div class="archive-list">
{% for entry in posts_by_year[year] %} {% for entry in posts_by_year[year] %}
<div class="archive-entry"> <code>
<code><time datetime="{{ entry.date | iso_datetime }}">{{ entry.date | iso_date }}</time></code> <time datetime="{{ entry.date | iso_datetime }}">
<a href="{{ entry.permalink }}"> {{ entry.date | iso_date }}
{{ entry.title }} </time>
</a> </code>
</div> <a href="{{ entry.permalink }}">
{{ entry.title }}
</a>
{% endfor %} {% endfor %}
</div> </div>
{% if not loop.last %} {% if not loop.last %}

52
site_test/colophon.html Normal file
View File

@ -0,0 +1,52 @@
{% extends "default" %}
{% block titlevariable %}
{% set title = "Colophon" %}
{% endblock %}
{% block content -%}
<h1 class="headline">Colophon</h1>
<div class="body-content">
<p>
This website is produced using a custom static site generator called v7, which is <a href="https://git.shadowfacts.net/shadowfacts/v7/">open source</a>.
It comprises some 7,000 lines of Rust code, and took too much time to develop.
<a href="/2025/version-7/">Read more</a> about the architecture.
</p>
<p>
Body text is typeset in Matthew Butterick&rsquo;s <a href="https://mbtype.com/fonts/valkyrie/">Valkyrie</a>.
Butterick&rsquo;s <a href="https://practicaltypography.com/"><em>Practical Typography</em></a> also informed many of the typographic choices of this website.
Code is set in <a href="https://mass-driver.com/typefaces/md-io/"><code>MD IO</code></a> by Mass Driver.
</p>
<p>
For analytics, I use <a href="https://www.goatcounter.com/">GoatCounter</a> (<a href="https://www.goatcounter.com/help/privacy">privacy policy</a>) to get a rough sense of what people are reading.
No personally identifiable information is stored, and no cookies are used.
The <a href="https://shadowfacts.goatcounter.com/" rel="nofollow">statistics</a> gathered are public.
</p>
<p>
All of the posts are written by yours truly.
No generative artificial intelligence tools were used to create this website.
</p>
<p>
All content on this website is under copyright and may not be republished without permission.
</p>
<hr>
<blockquote>
<p>
Now, there&rsquo;s this about cynicism, Sergeant.
It&rsquo;s the universe&rsquo;s most supine moral position.
Real comfortable.
If nothing can be done, then you&rsquo;re not some kind of shit for not doing it, and you can lie there and stink to yourself in perfect peace.
</p>
<p>&mdash; Lois McMaster Bujold, <em>Borders of Infinity</em></p>
</blockquote>
</div>
{%- endblock %}

View File

@ -1,4 +1,3 @@
<!--<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>-->
<svg <svg
width="13.333401" width="13.333401"
height="13.3334" height="13.3334"

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 772 B

View File

@ -31,13 +31,14 @@ a:visited {
color: var(--link-color); color: var(--link-color);
text-decoration-thickness: 2px; text-decoration-thickness: 2px;
text-underline-offset: 2px; text-underline-offset: 2px;
text-decoration-color: currentColor;
} }
a:hover { a:hover {
text-decoration-thickness: 3px; text-decoration-thickness: 3px;
} }
a[href^="https://"]:not(.no-external-link-decoration)::after,
a[href^="http://"]:not(.no-external-link-decoration)::after, a[href^="http://"]:not(.no-external-link-decoration)::after,
a[href^="https://"]:not(.no-external-link-decoration)::after a[href^="mailto:"]:not(.no-external-link-decoration)::after {
{
background-color: currentColor; background-color: currentColor;
content: ""; content: "";
width: calc(max(0.667em, 12px)); width: calc(max(0.667em, 12px));
@ -67,6 +68,7 @@ blockquote {
&::before { &::before {
content: open-quote; content: open-quote;
font-size: 2em; font-size: 2em;
font-family: Charter, serif;
position: absolute; position: absolute;
left: -25px; left: -25px;
top: -10px; top: -10px;
@ -128,6 +130,21 @@ hr {
font-style: italic; font-style: italic;
} }
::selection {
background-color: var(--link-color);
color: white;
}
code {
word-break: break-all;
}
@media (min-width: $mobile-breakpoint) {
h1 {
font-size: 3em;
}
}
html { html {
font-family: "Valkyrie A", Charter, serif; font-family: "Valkyrie A", Charter, serif;
font-size: 16px; font-size: 16px;
@ -154,11 +171,9 @@ header {
} }
} }
} }
p { #epigraph {
margin-top: 0.25rem; margin-top: 0.25rem;
font-size: 1.5rem; font-size: 1.5rem;
font-weight: lighter;
text-wrap: balance;
} }
} }
@ -378,22 +393,20 @@ footer {
} }
.archive-list { .archive-list {
display: flex; display: grid;
flex-direction: column; grid-template-columns: min-content 1fr;
gap: 4px; align-items: baseline;
row-gap: 4px;
font-size: 1.25rem; font-size: 1.25rem;
margin: 1rem 0; margin: 1rem 0;
.archive-entry { code {
display: flex; text-wrap: nowrap;
flex-direction: row; color: var(--secondary-text-color);
align-items: start; padding-right: 8px;
gap: 8px;
time { &:hover,
color: var(--secondary-text-color); &:has(+ a:hover) {
}
&:hover time {
color: var(--text-color); color: var(--text-color);
} }
} }
@ -450,5 +463,10 @@ footer {
margin-bottom: 0; margin-bottom: 0;
} }
} }
// links in masto replies are almost always bare urls, so let them break anywhere to prevent overflow
a {
word-break: break-all;
}
} }
} }

37
site_test/elsewhere.html Normal file
View File

@ -0,0 +1,37 @@
{% extends "default" %}
{% block titlevariable %}
{% set title = "Elsewhere" %}
{% endblock %}
{% block content -%}
<h1 class="headline">Elsewhere</h1>
<div class="body-content">
<p>A non-exhaustive list of other places I can be found on the internet.</p>
<ul>
<li><a href="https://bsky.app/profile/shadowfacts.net">Bluesky</a> (read-only)</li>
<li><a href="https://legacy.curseforge.com/members/shadowfactsdev/projects">CurseForge</a> (inactive)</li>
<li><a href="mailto:me@shadowfacts.net">Email</a></li>
<li><a href="https://github.com/shadowfacts">GitHub</a></li>
<li><a href="https://git.shadowfacts.net/shadowfacts">Gitea</a></li>
<li><a href="https://news.ycombinator.com/user?id=shadowfacts">&ldquo;Hacker&rdquo; &ldquo;News&rdquo;</a></li>
<li>
<a href="https://letterboxd.com/shadowfacts/">Letterboxd</a>
(pronounced like it's a <a href="https://en.wikipedia.org/wiki/Daemon_(computing)">daemon</a>)
</li>
<li>
<a href="https://social.shadowfacts.net/users/shadowfacts" rel="me">Mastodon</a>
(technically the fediverse)
</li>
<li>
<a href="https://social.shadowfacts.net/users/tusker" rel="me">Mastodon</a>
(for <a href="https://vaccor.space/tusker/">Tusker</a> announcements)
</li>
<li><a href="https://www.reddit.com/user/shadowfactsdev">Reddit</a></li>
</ul>
</div>
{% endblock %}

View File

@ -55,7 +55,6 @@
{% block footer_links %} {% block footer_links %}
{% set additional_links = [ {% set additional_links = [
"Book Log", "/books/",
"TV Commentary", "/tv/", "TV Commentary", "/tv/",
"Modding Tutorials", "/tutorials/", "Modding Tutorials", "/tutorials/",
] %} ] %}

View File

@ -68,10 +68,15 @@ const commentsPostID = "{{ metadata.comments_post_id }}";
<summary><h2>Comments</h2></summary> <summary><h2>Comments</h2></summary>
<p class="italic">Reply to this post <a href="https://social.shadowfacts.net/notice/{{ metadata.comments_post_id }}" target="_blank">via the Fediverse</a>.</p> <p class="italic">Reply to this post <a href="https://social.shadowfacts.net/notice/{{ metadata.comments_post_id }}" target="_blank">via the Fediverse</a>.</p>
<div id="comments-list"></div> <div id="comments-list"></div>
<noscript>
<aside class="inline">
<p>
Comments cannot be shown inline since you have JavaScript disabled.
</p>
</aside>
</noscript>
</details> </details>
{% endif %}
<script src="/js/comments.js?{{ _stylesheet_cache_buster }}" async></script> <script src="/js/comments.js?{{ _stylesheet_cache_buster }}" async></script>
{% endif %}
<hr>
{%- endblock %} {%- endblock %}

View File

@ -37,13 +37,17 @@
<header> <header>
<div class="container"> <div class="container">
<h1><a href="/">Shadowfacts</a></h1> <h1><a href="/">Shadowfacts</a></h1>
<p>The outer part of a shadow is called the penumbra.</p> <p id="epigraph">The outer part of a shadow is called the penumbra.</p>
</div> </div>
</header> </header>
<main> <main>
<div class="container"> <div class="container">
{% block content %}{% endblock %} {% block content %}{% endblock %}
{% if _permalink != "/" %}
<hr>
{% endif %}
</div> </div>
</main> </main>
@ -69,7 +73,7 @@
</div> </div>
</footer> </footer>
<script data-goatcounter="https://shadowfacts.goatcounter.com/count" async src="//gc.zgo.at/count.v3.js" crossorigin="anonymous"></script> <script data-goatcounter="https://shadowfacts.goatcounter.com/count" defer src="//gc.zgo.at/count.v3.js" crossorigin="anonymous"></script>
{% if _development %} {% if _development %}
<script> <script>

View File

@ -11,12 +11,14 @@
{% for year in years %} {% for year in years %}
<div class="archive-list"> <div class="archive-list">
{% for entry in posts_by_year[year] %} {% for entry in posts_by_year[year] %}
<div class="archive-entry"> <code>
<code><time datetime="{{ entry.date | iso_datetime }}">{{ entry.date | iso_date }}</time></code> <time datetime="{{ entry.date | iso_datetime }}">
<a href="{{ entry.permalink }}"> {{ entry.date | iso_date }}
{{ entry.title }} </time>
</a> </code>
</div> <a href="{{ entry.permalink }}">
{{ entry.title }}
</a>
{% endfor %} {% endfor %}
</div> </div>
{% if not loop.last %} {% if not loop.last %}

View File

@ -42,12 +42,10 @@ The site remains almost entirely JavaScript free. There are two places where cli
## The Backend ## The Backend
The previous version of my website used [Jekyll](https://jekyllrb.com/) (and WordPress before that, and Jekyll again before that). In what may become a pattern, I've once more switched away from Jekyll. Version Five uses something completely custom. It has been a work-in-progress in one form or another for about a year now. It started out as a Node.js project that was going to be a general-purpose static site generator. Then, around the time I was learning Elixir (which I love, and will be the subject of another blog post), I attempted to rewrite it in that[^3]. Then we finally arrive at the current iteration of the current iteration of my website. In spite of my distaste for the ecosystem[^4], I returned to Node.js. This time, however, the project took a bit of a different direction than the previous two attempts at a rewrite. It has two main parts: the static site generator and the ActivityPub integration. The previous version of my website used [Jekyll](https://jekyllrb.com/) (and WordPress before that, and Jekyll again before that). In what may become a pattern, I've once more switched away from Jekyll. Version Five uses something completely custom. It has been a work-in-progress in one form or another for about a year now. It started out as a Node.js project that was going to be a general-purpose static site generator. Then, around the time I was learning Elixir (which I love, and will be the subject of another blog post), I attempted to rewrite it in that[^3]. Then we finally arrive at the current iteration of the current iteration of my website. In spite of my distaste for the ecosystem (the `package.json` lists 30 dependencies, 13 of which are TypeScript type definitions, yet there are 311 packages in my `node_modules` folder), I returned to Node.js. This time, however, the project took a bit of a different direction than the previous two attempts at a rewrite. It has two main parts: the static site generator and the ActivityPub integration.
[^3]: Unfortunately, this attempt ran into some issues fairly quickly. Elixir itself is wonderful, but the package ecosystem for web-related things such as Sass, Markdown rendering, and syntax highlighting, is lackluster. [^3]: Unfortunately, this attempt ran into some issues fairly quickly. Elixir itself is wonderful, but the package ecosystem for web-related things such as Sass, Markdown rendering, and syntax highlighting, is lackluster.
[^4]: The `package.json` for the project explicitly lists 30 dependencies, 13 of which are TypeScript type definitions. There are 311 packages in my `node_modules` folder. Enough said.
### Static Site Generator ### Static Site Generator
The static site generator is by far the most important piece. Without it, there would be no website. I once again went with an SSG for a couple reasons, starting and ending with performance. When it comes down to it, nothing is generated at request time. Everything exists as static files on disk that are generated when the service starts up. The basic architecture isn't all that special: there are posts written in Markdown, gathered into various collections, rendered to HTML using various page layouts, and then gathered together in various indexes (the main index, category-specific ones, and RSS feeds). The static site generator is by far the most important piece. Without it, there would be no website. I once again went with an SSG for a couple reasons, starting and ending with performance. When it comes down to it, nothing is generated at request time. Everything exists as static files on disk that are generated when the service starts up. The basic architecture isn't all that special: there are posts written in Markdown, gathered into various collections, rendered to HTML using various page layouts, and then gathered together in various indexes (the main index, category-specific ones, and RSS feeds).

View File

@ -6,7 +6,7 @@ slug = "swiftui-lifecycle"
comments_post_id = "Aoah5r3C5rOsOYe6lc" comments_post_id = "Aoah5r3C5rOsOYe6lc"
``` ```
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. 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 initialized 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.

View File

@ -14,7 +14,12 @@ async function fetchComments() {
); );
const json = await res.json(); const json = await res.json();
const comments = json.descendants.map(makeCommentHTML).join(""); const comments = json.descendants.map(makeCommentHTML).join("");
document.getElementById("comments-list").innerHTML = comments; const list = document.getElementById("comments-list");
list.innerHTML = comments;
list.querySelectorAll(".comment-body a").forEach((a) => {
a.target = "_blank";
a.rel = "nofollow";
});
} }
} }

View File

@ -14,7 +14,7 @@ use crate::generator::templates::{BuildTemplateContext, RenderTemplate};
use super::{ use super::{
FileWatcher, FileWatcher,
posts::content::{HtmlContent, Post}, posts::content::{HtmlContent, Post},
templates::{AddTemplate, Templates}, templates::{Templates, make_invalidatable_template},
util::content_path, util::content_path,
}; };
@ -27,13 +27,13 @@ pub fn make_graph(
let entries = builder.add_rule(Entries(posts)); let entries = builder.add_rule(Entries(posts));
let posts_by_year = builder.add_rule(PostsByYear(entries)); let posts_by_year = builder.add_rule(PostsByYear(entries));
let archive_path = content_path("archive.html"); let archive_template = make_invalidatable_template(
let (archive_template, invalidate_template) = builder.add_invalidatable_rule(AddTemplate::new( builder,
"archive", watcher,
archive_path.clone(),
default_template, default_template,
)); "archive",
watcher.watch(archive_path, move || invalidate_template.invalidate()); content_path("archive.html"),
);
let context = builder.add_rule(BuildTemplateContext::new( let context = builder.add_rule(BuildTemplateContext::new(
"/archive/".into(), "/archive/".into(),

View File

@ -70,18 +70,18 @@ pub fn make_graph(
FontKey::BOLD.union(FontKey::ITALIC), FontKey::BOLD.union(FontKey::ITALIC),
), ),
( (
"berkeley-mono-regular", "md-io-regular",
content_path("css/fonts/BerkeleyMono-Regular.woff2"), content_path("css/fonts/MDIOTrial-Regular.otf"),
FontKey::MONOSPACE, FontKey::MONOSPACE,
), ),
( (
"berkeley-mono-italic", "md-io-italic",
content_path("css/fonts/BerkeleyMono-Oblique.woff2"), content_path("css/fonts/MDIOTrial-Italic.otf"),
FontKey::MONOSPACE.union(FontKey::ITALIC), FontKey::MONOSPACE.union(FontKey::ITALIC),
), ),
( (
"berkeley-mono-bold", "md-io-bold",
content_path("css/fonts/BerkeleyMono-Bold.woff2"), content_path("css/fonts/MDIOTrial-Bold.otf"),
FontKey::MONOSPACE.union(FontKey::BOLD), FontKey::MONOSPACE.union(FontKey::BOLD),
), ),
]; ];

View File

@ -3,7 +3,7 @@ use bitflags::bitflags;
use compute_graph::{ use compute_graph::{
NodeId, NodeId,
builder::GraphBuilder, builder::GraphBuilder,
input::{DynamicInput, Input, InputVisitable}, input::{DynamicInput, Input, InputVisitable, InputVisitor},
rule::{DynamicNodeFactory, DynamicRule, DynamicRuleContext, Rule}, rule::{DynamicNodeFactory, DynamicRule, DynamicRuleContext, Rule},
synchronicity::Asynchronous, synchronicity::Asynchronous,
}; };
@ -53,7 +53,7 @@ impl MakeBuildDynamicCharacterSets {
} }
} }
impl InputVisitable for MakeBuildDynamicCharacterSets { impl InputVisitable for MakeBuildDynamicCharacterSets {
fn visit_inputs(&self, visitor: &mut impl compute_graph::input::InputVisitor) { fn visit_inputs(&self, visitor: &mut impl InputVisitor) {
for input in self.strings.iter() { for input in self.strings.iter() {
input.visit_inputs(visitor); input.visit_inputs(visitor);
} }
@ -75,7 +75,7 @@ impl DynamicRule for MakeBuildDynamicCharacterSets {
struct UnionCharacterSets(Vec<Input<CharacterSets>>); struct UnionCharacterSets(Vec<Input<CharacterSets>>);
impl InputVisitable for UnionCharacterSets { impl InputVisitable for UnionCharacterSets {
fn visit_inputs(&self, visitor: &mut impl compute_graph::input::InputVisitor) { fn visit_inputs(&self, visitor: &mut impl InputVisitor) {
for input in self.0.iter() { for input in self.0.iter() {
input.visit_inputs(visitor); input.visit_inputs(visitor);
} }

View File

@ -34,9 +34,14 @@ struct GetCharacterSet {
} }
impl Rule for GetCharacterSet { impl Rule for GetCharacterSet {
type Output = ahash::HashSet<char>; type Output = ahash::HashSet<char>;
fn evaluate(&mut self) -> Self::Output { fn evaluate(&mut self) -> Self::Output {
self.sets().get(self.key).clone() self.sets().get(self.key).clone()
} }
fn node_label(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self.key)
}
} }
#[derive(InputVisitable)] #[derive(InputVisitable)]
@ -48,6 +53,7 @@ struct SubsetFont {
} }
impl Rule for SubsetFont { impl Rule for SubsetFont {
type Output = Vec<u8>; type Output = Vec<u8>;
fn evaluate(&mut self) -> Self::Output { fn evaluate(&mut self) -> Self::Output {
debug!("Subsetting font {:?}", self.key); debug!("Subsetting font {:?}", self.key);
let unicodes = self let unicodes = self
@ -57,4 +63,8 @@ impl Rule for SubsetFont {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
subset(self.font().as_ref(), &unicodes) subset(self.font().as_ref(), &unicodes)
} }
fn node_label(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self.key)
}
} }

View File

@ -6,14 +6,14 @@ use compute_graph::{
}; };
use crate::generator::{ use crate::generator::{
templates::{AddTemplate, BuildTemplateContext}, templates::{BuildTemplateContext, make_invalidatable_template},
util::content_path, util::content_path,
}; };
use super::{ use super::{
FileWatcher, FileWatcher,
posts::content::{HtmlContent, Post}, posts::content::{HtmlContent, Post},
templates::{RenderTemplate, Templates}, templates::{RenderTemplate, Templates, make_template_context},
}; };
pub fn make_graph( pub fn make_graph(
@ -21,16 +21,16 @@ pub fn make_graph(
posts: Input<Vec<Post<HtmlContent>>>, posts: Input<Vec<Post<HtmlContent>>>,
default_template: Input<Templates>, default_template: Input<Templates>,
watcher: &mut FileWatcher, watcher: &mut FileWatcher,
) -> Input<String> { ) -> Vec<Input<String>> {
let latest_post = builder.add_rule(LatestPost(posts)); let latest_post = builder.add_rule(LatestPost(posts));
let template_path = content_path("index.html"); let home_template = make_invalidatable_template(
let (home_template, invalidate_template) = builder.add_invalidatable_rule(AddTemplate::new( builder,
watcher,
default_template.clone(),
"home", "home",
template_path.clone(), content_path("index.html"),
default_template, );
));
watcher.watch(template_path, move || invalidate_template.invalidate());
let context = builder.add_rule(BuildTemplateContext::new( let context = builder.add_rule(BuildTemplateContext::new(
"/".into(), "/".into(),
@ -42,12 +42,43 @@ pub fn make_graph(
}, },
)); ));
builder.add_rule(RenderTemplate { let home = builder.add_rule(RenderTemplate {
name: "home", name: "home",
output_path: "index.html".into(), output_path: "index.html".into(),
templates: home_template, templates: home_template,
context: context.into(), context: context.into(),
}) });
let colophon_template = make_invalidatable_template(
builder,
watcher,
default_template.clone(),
"colophon",
content_path("colophon.html"),
);
let colophon = builder.add_rule(RenderTemplate {
name: "colophon",
output_path: "colophon/index.html".into(),
templates: colophon_template,
context: make_template_context(&"/colophon/".into()).into(),
});
let elsewhere_template = make_invalidatable_template(
builder,
watcher,
default_template,
"elsewhere",
content_path("elsewhere.html"),
);
let elsewhere = builder.add_rule(RenderTemplate {
name: "elsewhere",
output_path: "elsewhere/index.html".into(),
templates: elsewhere_template,
context: make_template_context(&"/elsewhere/".into()).into(),
});
vec![home, colophon, elsewhere]
} }
#[derive(InputVisitable)] #[derive(InputVisitable)]

View File

@ -60,13 +60,13 @@ fn make_graph(watcher: Rc<RefCell<FileWatcher>>) -> anyhow::Result<AsyncGraph<()
); );
render_dynamic_inputs.push(render_tags); render_dynamic_inputs.push(render_tags);
let render_home = home::make_graph( let render_home_inputs = home::make_graph(
&mut builder, &mut builder,
all_posts, all_posts,
default_template.clone(), default_template.clone(),
&mut *watcher.borrow_mut(), &mut *watcher.borrow_mut(),
); );
render_inputs.push(render_home); render_inputs.extend(render_home_inputs);
let statics = static_files::make_graph(&mut builder, Rc::clone(&watcher)); let statics = static_files::make_graph(&mut builder, Rc::clone(&watcher));

View File

@ -1,13 +1,9 @@
use compute_graph::{builder::GraphBuilder, input::Input, synchronicity::Asynchronous}; use compute_graph::{builder::GraphBuilder, input::Input, synchronicity::Asynchronous};
use crate::generator::{
templates::{AddTemplate, RenderTemplate},
util::content_path,
};
use super::{ use super::{
FileWatcher, FileWatcher,
templates::{Templates, make_template_context}, templates::{RenderTemplate, Templates, make_invalidatable_template, make_template_context},
util::content_path,
}; };
pub fn make_graph( pub fn make_graph(
@ -15,10 +11,13 @@ pub fn make_graph(
default_template: Input<Templates>, default_template: Input<Templates>,
watcher: &mut FileWatcher, watcher: &mut FileWatcher,
) -> Input<String> { ) -> Input<String> {
let path = content_path("404.html"); let templates = make_invalidatable_template(
let (templates, invalidate) = builder,
builder.add_invalidatable_rule(AddTemplate::new("404", path.clone(), default_template)); watcher,
watcher.watch(path, move || invalidate.invalidate()); default_template,
"404",
content_path("404.html"),
);
builder.add_rule(RenderTemplate { builder.add_rule(RenderTemplate {
name: "404", name: "404",

View File

@ -20,7 +20,7 @@ use tera::Context;
use super::{ use super::{
FileWatcher, FileWatcher,
templates::{AddTemplate, BuildTemplateContext, RenderTemplate, Templates}, templates::{BuildTemplateContext, RenderTemplate, Templates, make_invalidatable_template},
util::content_path, util::content_path,
}; };
@ -44,15 +44,13 @@ pub fn make_graph(
// let extract_metadatas = builder.add_dynamic_rule(MakeExtractMetadatas::new(posts.clone())); // let extract_metadatas = builder.add_dynamic_rule(MakeExtractMetadatas::new(posts.clone()));
let article_path = content_path("layout/article.html"); let article_template = make_invalidatable_template(
let (article_template, invalidate_template) = builder.add_invalidatable_rule(AddTemplate::new( builder,
"article", &mut *watcher.borrow_mut(),
article_path.clone(),
default_template, default_template,
)); "article",
watcher content_path("layout/article.html"),
.borrow_mut() );
.watch(article_path, move || invalidate_template.invalidate());
let write_posts = let write_posts =
builder.add_dynamic_rule(MakeWritePosts::new(html_posts.clone(), article_template)); builder.add_dynamic_rule(MakeWritePosts::new(html_posts.clone(), article_template));

View File

@ -14,7 +14,7 @@ use super::{
FileWatcher, FileWatcher,
archive::{Entry, PostsYearMap}, archive::{Entry, PostsYearMap},
posts::{ReadPostOutput, metadata::Tag}, posts::{ReadPostOutput, metadata::Tag},
templates::{AddTemplate, BuildTemplateContext, RenderTemplate, Templates}, templates::{BuildTemplateContext, RenderTemplate, Templates, make_invalidatable_template},
util::content_path, util::content_path,
}; };
@ -26,13 +26,13 @@ pub fn make_graph(
) -> DynamicInput<String> { ) -> DynamicInput<String> {
let by_tags = builder.add_dynamic_rule(MakePostsByTags::new(posts)); let by_tags = builder.add_dynamic_rule(MakePostsByTags::new(posts));
let template_path = content_path("layout/tag.html"); let tag_template = make_invalidatable_template(
let (tag_template, invalidate_template) = builder.add_invalidatable_rule(AddTemplate::new( builder,
"tag", watcher,
template_path.clone(),
default_template, default_template,
)); "tag",
watcher.watch(template_path, move || invalidate_template.invalidate()); content_path("layout/tag.html"),
);
builder.add_dynamic_rule(MakeWriteTagPages::new(by_tags, tag_template)) builder.add_dynamic_rule(MakeWriteTagPages::new(by_tags, tag_template))
} }

View File

@ -33,17 +33,30 @@ pub fn make_graph(
empty_templates.register_filter("zip", filters::zip); empty_templates.register_filter("zip", filters::zip);
let empty_templates = builder.add_value(empty_templates); let empty_templates = builder.add_value(empty_templates);
let default_path = content_path("layout/default.html"); let default = make_invalidatable_template(
let (default, invalidate_default) = builder.add_invalidatable_rule(AddTemplate::new( builder,
"default", watcher,
default_path.clone(),
empty_templates, empty_templates,
)); "default",
watcher.watch(default_path, move || invalidate_default.invalidate()); content_path("layout/default.html"),
);
default default
} }
pub fn make_invalidatable_template(
builder: &mut GraphBuilder<(), Asynchronous>,
watcher: &mut FileWatcher,
base: Input<Templates>,
name: &'static str,
path: PathBuf,
) -> Input<Templates> {
let (templates, invalidate) =
builder.add_invalidatable_rule(AddTemplate::new(name, path.clone(), base));
watcher.watch(path, move || invalidate.invalidate());
templates
}
#[derive(InputVisitable)] #[derive(InputVisitable)]
pub struct AddTemplate( pub struct AddTemplate(
#[skip_visit] &'static str, #[skip_visit] &'static str,

View File

@ -4,7 +4,9 @@ use compute_graph::rule::{DynamicNodeFactory, DynamicRule, DynamicRuleContext};
use compute_graph::{builder::GraphBuilder, input::Input, synchronicity::Asynchronous}; use compute_graph::{builder::GraphBuilder, input::Input, synchronicity::Asynchronous};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::generator::templates::{AddTemplate, RenderTemplate, make_template_context}; use crate::generator::templates::{
RenderTemplate, make_invalidatable_template, make_template_context,
};
use crate::generator::util::word_count; use crate::generator::util::word_count;
use super::markdown; use super::markdown;
@ -17,29 +19,29 @@ pub fn make_graph(
default_template: Input<Templates>, default_template: Input<Templates>,
watcher: &mut FileWatcher, watcher: &mut FileWatcher,
) -> (Vec<Input<String>>, Vec<DynamicInput<String>>) { ) -> (Vec<Input<String>>, Vec<DynamicInput<String>>) {
let post_path = content_path("layout/tutorial_post.html"); let post_template = make_invalidatable_template(
let (post_template, invalidate) = builder.add_invalidatable_rule(AddTemplate::new( builder,
watcher,
default_template.clone(),
"tutorial_post", "tutorial_post",
post_path.clone(), content_path("layout/tutorial_post.html"),
default_template.clone(), );
));
watcher.watch(post_path, move || invalidate.invalidate());
let series_path = content_path("layout/tutorial_series.html"); let series_template = make_invalidatable_template(
let (series_template, invalidate) = builder.add_invalidatable_rule(AddTemplate::new( builder,
watcher,
default_template.clone(),
"tutorial_series", "tutorial_series",
series_path.clone(), content_path("layout/tutorial_series.html"),
default_template.clone(), );
));
watcher.watch(series_path, move || invalidate.invalidate());
let index_path = content_path("tutorials.html"); let index_template = make_invalidatable_template(
let (index_template, invalidate) = builder.add_invalidatable_rule(AddTemplate::new( builder,
"tutorials", watcher,
index_path.clone(),
default_template, default_template,
)); "tutorials",
watcher.watch(index_path, move || invalidate.invalidate()); content_path("tutorials.html"),
);
let serieses = vec![ let serieses = vec![
read_series("forge-modding-1102", "Forge Mods for 1.10.2"), read_series("forge-modding-1102", "Forge Mods for 1.10.2"),

View File

@ -19,14 +19,10 @@ use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tera::Context; use tera::Context;
use crate::generator::{
templates::AddTemplate,
util::{content_path, from_frontmatter},
};
use super::{ use super::{
FileWatcher, markdown, FileWatcher, markdown,
templates::{BuildTemplateContext, RenderTemplate, Templates}, templates::{BuildTemplateContext, RenderTemplate, Templates, make_invalidatable_template},
util::{content_path, from_frontmatter},
}; };
pub fn make_graph( pub fn make_graph(
@ -41,24 +37,23 @@ pub fn make_graph(
.borrow_mut() .borrow_mut()
.watch(tv_path, move || invalidate.invalidate()); .watch(tv_path, move || invalidate.invalidate());
let show_path = content_path("layout/show.html"); let show_template = make_invalidatable_template(
let (show_template, invalidate) = builder.add_invalidatable_rule(AddTemplate::new( builder,
"show", &mut *watcher.borrow_mut(),
show_path.clone(),
default_template.clone(), default_template.clone(),
)); "show",
watcher content_path("layout/show.html"),
.borrow_mut() );
.watch(show_path, move || invalidate.invalidate());
let render_shows = builder.add_dynamic_rule(MakeRenderShows::new(shows.clone(), show_template)); let render_shows = builder.add_dynamic_rule(MakeRenderShows::new(shows.clone(), show_template));
let tv_path = content_path("tv.html"); let index_template = make_invalidatable_template(
let (index_template, invalidate) = builder,
builder.add_invalidatable_rule(AddTemplate::new("tv", tv_path.clone(), default_template)); &mut *watcher.borrow_mut(),
watcher default_template,
.borrow_mut() "tv",
.watch(tv_path, move || invalidate.invalidate()); content_path("tv.html"),
);
let index = builder.add_rule(ShowIndex(shows)); let index = builder.add_rule(ShowIndex(shows));
let index_context = builder.add_rule(BuildTemplateContext::new( let index_context = builder.add_rule(BuildTemplateContext::new(