Output stuff

This commit is contained in:
Shadowfacts 2024-12-31 14:29:11 -05:00
parent 9ff658f719
commit 6bb51638cc
18 changed files with 789 additions and 84 deletions

101
Cargo.lock generated
View File

@ -96,6 +96,50 @@ version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" 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]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.4.0" version = "1.4.0"
@ -117,6 +161,15 @@ dependencies = [
"windows-targets", "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]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.6.0" version = "2.6.0"
@ -348,6 +401,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "humansize"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
dependencies = [
"libm",
]
[[package]] [[package]]
name = "humantime" name = "humantime"
version = "2.1.0" version = "2.1.0"
@ -554,6 +616,12 @@ version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libm"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.7.4" version = "0.7.4"
@ -614,6 +682,28 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 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]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.2" version = "0.8.2"
@ -640,6 +730,16 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" 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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@ -1208,6 +1308,7 @@ name = "v7"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"askama",
"chrono", "chrono",
"clap", "clap",
"compute_graph", "compute_graph",

View File

@ -17,6 +17,7 @@ serde_json = "1.0"
[dependencies] [dependencies]
anyhow = "1.0.95" anyhow = "1.0.95"
askama = "0.12.1"
chrono = { version = "0.4.39", features = ["serde"] } chrono = { version = "0.4.39", features = ["serde"] }
clap = { version = "4.5.23", features = ["cargo"] } clap = { version = "4.5.23", features = ["cargo"] }
compute_graph = { path = "crates/compute_graph" } compute_graph = { path = "crates/compute_graph" }

View File

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

View File

@ -57,6 +57,11 @@ impl<O: 'static, S: Synchronicity> GraphBuilder<O, S> {
self.output = Some(input); self.output = Some(input);
} }
/// Sets an existing node represented by the given input to be the output node.
pub fn set_existing_output(&mut self, input: Input<O>) {
self.output = Some(input);
}
/// Replaces the current output rule with a new one, changing the output value type. /// Replaces the current output rule with a new one, changing the output value type.
pub fn with_output<R: Rule>(mut self, rule: R) -> GraphBuilder<R::Output, S> { pub fn with_output<R: Rule>(mut self, rule: R) -> GraphBuilder<R::Output, S> {
let input = self.add_rule(rule); let input = self.add_rule(rule);

View File

@ -26,7 +26,8 @@
//! ``` //! ```
//! //!
//! Here, `a` and `b` are placeholders representing the values of the two constant nodes in the graph. //! Here, `a` and `b` are placeholders representing the values of the two constant nodes in the graph.
//! The `Add` struct implements the [`Rule`] trait and defines how to combine those two values by addition. //! The `Add` struct implements the [`Rule`](`crate::rule::Rule`) trait and defines how to combine
//! those two values by addition.
//! The `Add` rule is implemented as follows: //! The `Add` rule is implemented as follows:
//! //!
//! ```rust //! ```rust
@ -165,7 +166,7 @@ impl<O: 'static, S: Synchronicity> Graph<O, S> {
let target = graph.to_index(edge.target()); let target = graph.to_index(edge.target());
if !old_edges if !old_edges
.get(&source) .get(&source)
.map_or(false, |old| !old.contains(&target)) .map_or(false, |old| old.contains(&target))
{ {
to_invalidate.push_back(edge.target()); to_invalidate.push_back(edge.target());
} }
@ -926,5 +927,6 @@ mod tests {
assert_eq!(*graph.evaluate_async().await, 3); assert_eq!(*graph.evaluate_async().await, 3);
set_count.set_value(4); set_count.set_value(4);
assert_eq!(*graph.evaluate_async().await, 10); assert_eq!(*graph.evaluate_async().await, 10);
println!("{}", graph.as_dot_string());
} }
} }

View File

@ -260,6 +260,8 @@ fn visit_inputs<V: InputVisitable>(visitable: &V, visitor: &mut dyn FnMut(NodeId
// And visit all the nodes it produces // And visit all the nodes it produces
let maybe_dynamic_output = input.input.value.borrow(); let maybe_dynamic_output = input.input.value.borrow();
if let Some(dynamic_output) = maybe_dynamic_output.as_ref() { if let Some(dynamic_output) = maybe_dynamic_output.as_ref() {
// This might be slightly overzealous: it is possible for a node to only depend on the
// dynamic node itself, and not directly depend on any of the nodes the dynamic node produces.
for input in dynamic_output.inputs.iter() { for input in dynamic_output.inputs.iter() {
self.visit(input); self.visit(input);
} }
@ -686,8 +688,9 @@ impl<R: AsyncDynamicRule> std::fmt::Debug for AsyncDynamicRuleNode<R, R::ChildOu
} }
fn pretty_type_name<T>() -> String { fn pretty_type_name<T>() -> String {
let s = std::any::type_name::<T>(); // idk where the {{closure}} comes from in one of the tests, just do this to avoid panicking
let ty = syn::parse_str::<syn::Type>(s).unwrap(); let s = std::any::type_name::<T>().replace("{{closure}}", "__closure__");
let ty = syn::parse_str::<syn::Type>(&s).unwrap();
pretty_type_name_type(ty) pretty_type_name_type(ty)
} }

View File

@ -2,7 +2,9 @@ use crate::node::{DynamicRuleOutput, NodeValue};
use crate::NodeId; use crate::NodeId;
pub use compute_graph_macros::InputVisitable; pub use compute_graph_macros::InputVisitable;
use std::cell::{Cell, Ref, RefCell}; use std::cell::{Cell, Ref, RefCell};
use std::collections::{HashMap, HashSet};
use std::future::Future; use std::future::Future;
use std::hash::Hash;
use std::ops::Deref; use std::ops::Deref;
use std::rc::Rc; use std::rc::Rc;
@ -88,6 +90,8 @@ pub trait DynamicRule: InputVisitable + 'static {
/// Evaluates this rule, producing additional nodes. /// Evaluates this rule, producing additional nodes.
/// ///
/// Use the methods on [`DynamicRuleContext`] to add or remove nodes from the graph. /// Use the methods on [`DynamicRuleContext`] to add or remove nodes from the graph.
/// Or, use [`DynamicNodeFactory`] to always register all nodes and allow that type to track
/// the specific additions/removals.
fn evaluate(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec<Input<Self::ChildOutput>>; fn evaluate(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec<Input<Self::ChildOutput>>;
#[allow(unused_variables)] #[allow(unused_variables)]
@ -114,6 +118,79 @@ pub trait DynamicRuleContext {
R: Rule; R: Rule;
} }
/// Helper type for working with [`DynamicRule`]s.
///
/// When implementing [`DynamicRule::evaluate`], call the [`DynamicNodeFactory::add_rule`] method for
/// each of the nodes that should be in the output. Then, call [`DynamicNodeFactory::all_nodes`] to produce
/// the output (i.e., `Vec` of [`Input`]s) of the dynamic node.
///
/// This type keeps track of which nodes need to be added and removed from the [`DynamicRuleContext`].
pub struct DynamicNodeFactory<ID, ChildOutput> {
existing_nodes: HashMap<ID, Input<ChildOutput>>,
ids_added_this_evaluation: HashSet<ID>,
}
impl<ID: Hash + Eq + Clone, ChildOutput> DynamicNodeFactory<ID, ChildOutput> {
/// Constructs a new dynamic node factory.
pub fn new() -> Self {
Self {
existing_nodes: HashMap::new(),
ids_added_this_evaluation: HashSet::new(),
}
}
/// Registers a node that is part of the output.
///
/// This method must be called for every node that is part of the output. The `build` function
/// will only be called for nodes that have not previously been built.
pub fn add_rule<F, R>(&mut self, ctx: &mut impl DynamicRuleContext, id: ID, build: F)
where
F: FnOnce() -> R,
R: Rule<Output = ChildOutput>,
{
if !self.existing_nodes.contains_key(&id) {
let input = ctx.add_rule(build());
self.existing_nodes.insert(id.clone(), input);
}
self.ids_added_this_evaluation.insert(id);
}
/// Registers a node that is part of the output.
///
/// See [`DynamicNodeFactory::add_rule`].
pub fn add_async_rule<F, R>(&mut self, ctx: &mut impl AsyncDynamicRuleContext, id: ID, build: F)
where
F: FnOnce() -> R,
R: AsyncRule<Output = ChildOutput>,
{
if !self.existing_nodes.contains_key(&id) {
let input = ctx.add_async_rule(build());
self.existing_nodes.insert(id.clone(), input);
}
self.ids_added_this_evaluation.insert(id);
}
/// Builds the final list of all nodes currently present in the output.
///
/// Removes any nodes that were previously output but which have not had [`DynamicNodeFactory::add_rule`]
/// called during this evaluation.
pub fn all_nodes(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec<Input<ChildOutput>> {
// collect everything up front so we can mutably borrow existing_nodes
let to_remove = self
.existing_nodes
.keys()
.filter(|k| !self.ids_added_this_evaluation.contains(*k))
.cloned()
.collect::<Vec<_>>();
for key in to_remove {
let input = self.existing_nodes.remove(&key).unwrap();
ctx.remove_node(input.node_id());
}
self.ids_added_this_evaluation.clear();
self.existing_nodes.values().cloned().collect()
}
}
/// An asynchronous rule whose output is further nodes in the graph. /// An asynchronous rule whose output is further nodes in the graph.
/// ///
/// See [`DynamicRule`]. /// See [`DynamicRule`].
@ -124,6 +201,8 @@ pub trait AsyncDynamicRule: InputVisitable + 'static {
/// Evaluates this rule asynchronously, producing additional nodes. /// Evaluates this rule asynchronously, producing additional nodes.
/// ///
/// Use the methods on [`AsyncDynamicRuleContext`] to add or remove nodes from the graph. /// Use the methods on [`AsyncDynamicRuleContext`] to add or remove nodes from the graph.
/// Or, use [`DynamicNodeFactory`] to always register all nodes and allow that type to track
/// the specific additions/removals.
fn evaluate<'a>( fn evaluate<'a>(
&'a mut self, &'a mut self,
ctx: &'a mut impl AsyncDynamicRuleContext, ctx: &'a mut impl AsyncDynamicRuleContext,

20
site_test/archive.html Normal file
View File

@ -0,0 +1,20 @@
{% extends "layout/default.html" %}
{% block title %}Archive{% endblock %}
{% block content -%}
{% for year in self.years() %}
<h2>{{ year }}</h2>
<ul>
{% for entry in self.posts_for_year(year) %}
<li>
<a href="{{ entry.permalink }}">
{{ entry.title }}
</a>
</li>
{% endfor %}
</ul>
{% endfor %}
{%- endblock %}

View File

@ -0,0 +1,30 @@
{% extends "layout/default.html" %}
{% 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 %}
{%- 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 %}
{% endblock %}
{% block title %}{{ post.metadata.title }}{% endblock %}
<article itemprop="blogPost" itemscope itemtype="https://schema.org/BlogPosting">
<meta itemprop="mainEntityOfPage" content="https://{{ Self::domain() }}{{ self.permalink() }}">
</article>

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{% block title %}Shadowfacts{% endblock %}</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="icon" href="/favicon.ico">
<link rel="apple-touch-icon-precomposed" href="/favicon-152.png">
<meta name="msapplication-TileColor" content="#F9C72F">
<meta name="msapplication-TileImage" content="/favicon-152.png">
<meta name="twitter:card" content="summary">
<meta property="og:title" content="{% block title %}{% endblock %}">
{% block image %}
<meta property="twitter:image" content="https://{{ Self::domain() }}/shadowfacts.png">
<meta property="og:image" content="https://{{ Self::domain() }}/shadowfacts.png">
{% endblock %}
<meta property="og:url" content="https://{{ Self::domain() }}{{ self.permalink() }}">
<meta property="og:site_name" content="Shadowfacts">
{% block head %}{% endblock %}
<link rel="stylesheet" href="/css/main.css?{{ Self::stylesheet_cache_buster() }}">
</head>
<body itemscope itemtype="https://schema.org/Blog">
{% block content %}{% endblock %}
<script data-goatcounter="https://shadowfacts.goatcounter.com/count" async src="//gc.zgo.at/count.v3.js" crossorigin="anonymous"></script>
</body>
</html>

9
site_test/tag.html Normal file
View File

@ -0,0 +1,9 @@
{% extends "layout/default.html" %}
{% block title %}{{ tag.name }} posts{% endblock %}
{% block content -%}
<h1>{{ tag.name }} posts</h1>
{%- endblock %}

106
src/generator/archive.rs Normal file
View File

@ -0,0 +1,106 @@
use std::collections::HashMap;
use askama::Template;
use chrono::Datelike;
use compute_graph::{
builder::GraphBuilder,
rule::{Input, InputVisitable, Rule},
synchronicity::Asynchronous,
};
use super::{
posts::content::{HtmlContent, Post},
util::{output_rendered_template, templates::TemplateCommon},
};
pub fn make_graph(
builder: &mut GraphBuilder<(), Asynchronous>,
posts: Input<Vec<Post<HtmlContent>>>,
) -> Input<()> {
let entries = builder.add_rule(Entries(posts));
let posts_by_year = builder.add_rule(PostsByYear(entries));
builder.add_rule(Archive(posts_by_year))
}
#[derive(InputVisitable)]
struct Entries(Input<Vec<Post<HtmlContent>>>);
impl Rule for Entries {
type Output = Vec<Entry>;
fn evaluate(&mut self) -> Self::Output {
self.input_0()
.iter()
.map(|post| Entry {
permalink: post.permalink(),
title: post.metadata.title.clone(),
year: post.metadata.date.year(),
})
.collect()
}
}
#[derive(InputVisitable)]
struct PostsByYear(Input<Vec<Entry>>);
impl Rule for PostsByYear {
type Output = PostsYearMap;
fn evaluate(&mut self) -> Self::Output {
let mut map = HashMap::new();
for entry in self.input_0().iter().cloned() {
map.entry(entry.year).or_insert(vec![]).push(entry);
}
PostsYearMap(map)
}
}
#[derive(PartialEq)]
struct PostsYearMap(HashMap<i32, Vec<Entry>>);
#[derive(PartialEq, Clone)]
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

@ -1,13 +1,11 @@
mod archive;
mod markdown; mod markdown;
mod posts; mod posts;
mod tags;
mod util; mod util;
use compute_graph::{ use compute_graph::{AsyncGraph, builder::GraphBuilder};
AsyncGraph, use util::{Combine, MapToVoid};
builder::GraphBuilder,
rule::{Input, InputVisitable, Rule},
};
use util::MapToVoid;
pub async fn generate() -> anyhow::Result<()> { pub async fn generate() -> anyhow::Result<()> {
std::fs::create_dir_all("out").expect("creating output dir"); std::fs::create_dir_all("out").expect("creating output dir");
@ -26,20 +24,29 @@ pub async fn generate() -> anyhow::Result<()> {
fn make_graph() -> anyhow::Result<AsyncGraph<()>> { fn make_graph() -> anyhow::Result<AsyncGraph<()>> {
let mut builder = GraphBuilder::new_async(); let mut builder = GraphBuilder::new_async();
let (posts, post_metadatas) = posts::make_graph(&mut builder); let (void_outputs, posts, all_posts, post_metadatas) = posts::make_graph(&mut builder);
let posts_output = builder.add_rule(MapToVoid(post_metadatas)); let archive = archive::make_graph(&mut builder, all_posts);
builder.set_output(Output {
posts: posts_output, let tag_output = tags::make_graph(&mut builder, posts);
});
let post_metadatas_voided = builder.add_rule(MapToVoid(post_metadatas));
let output = Combine::make(&mut builder, &[
void_outputs,
archive,
tag_output,
post_metadatas_voided,
]);
builder.set_existing_output(output);
Ok(builder.build()?) Ok(builder.build()?)
} }
#[derive(InputVisitable)] // #[derive(InputVisitable)]
struct Output { // struct Output {
posts: Input<()>, // archive: Input<()>,
} // posts: Input<()>,
impl Rule for Output { // }
type Output = (); // impl Rule for Output {
fn evaluate(&mut self) -> Self::Output {} // type Output = ();
} // fn evaluate(&mut self) -> Self::Output {}
// }

View File

@ -1,29 +1,44 @@
mod content; pub mod content;
mod metadata; pub mod metadata;
use std::{collections::HashMap, path::PathBuf}; use std::path::PathBuf;
use askama::Template;
use compute_graph::{ use compute_graph::{
builder::GraphBuilder, builder::GraphBuilder,
rule::{DynamicInput, DynamicRule, DynamicRuleContext, Input, InputVisitable, Rule}, rule::{
DynamicInput, DynamicNodeFactory, DynamicRule, DynamicRuleContext, Input, InputVisitable,
Rule,
},
synchronicity::Asynchronous, synchronicity::Asynchronous,
}; };
use content::{HtmlContent, Post}; use content::{HtmlContent, Post};
use log::error; use log::error;
use metadata::PostMetadata; use metadata::PostMetadata;
use super::util::content_path; use crate::generator::util::output_rendered_template;
use super::util::{MapDynamicToVoid, content_path, templates::TemplateCommon};
pub fn make_graph( pub fn make_graph(
builder: &mut GraphBuilder<(), Asynchronous>, builder: &mut GraphBuilder<(), Asynchronous>,
) -> (Input<Vec<Post<HtmlContent>>>, Input<Vec<PostMetadata>>) { ) -> (
Input<()>,
DynamicInput<ReadPostOutput>,
Input<Vec<Post<HtmlContent>>>,
Input<Vec<PostMetadata>>,
) {
// todo: make this invalidatable, watch files // todo: make this invalidatable, watch files
let post_files = builder.add_rule(ListPostFiles); let post_files = builder.add_rule(ListPostFiles);
let posts = builder.add_dynamic_rule(MakeReadNodes::new(post_files)); let posts = builder.add_dynamic_rule(MakeReadNodes::new(post_files));
let extract_metadatas = builder.add_dynamic_rule(MakeExtractMetadatas::new(posts.clone())); let extract_metadatas = builder.add_dynamic_rule(MakeExtractMetadatas::new(posts.clone()));
let write_posts = builder.add_dynamic_rule(MakeWritePosts::new(posts.clone()));
( (
builder.add_rule(MapDynamicToVoid(write_posts)),
posts.clone(),
builder.add_rule(AllPosts(posts)), builder.add_rule(AllPosts(posts)),
builder.add_rule(AllMetadatas(extract_metadatas)), builder.add_rule(AllMetadatas(extract_metadatas)),
) )
@ -65,13 +80,13 @@ fn find_index(path: PathBuf) -> Option<PathBuf> {
#[derive(InputVisitable)] #[derive(InputVisitable)]
struct MakeReadNodes { struct MakeReadNodes {
files: Input<Vec<PathBuf>>, files: Input<Vec<PathBuf>>,
existing_nodes: HashMap<PathBuf, Input<ReadPostOutput>>, node_factory: DynamicNodeFactory<PathBuf, ReadPostOutput>,
} }
impl MakeReadNodes { impl MakeReadNodes {
fn new(files: Input<Vec<PathBuf>>) -> Self { fn new(files: Input<Vec<PathBuf>>) -> Self {
Self { Self {
files, files,
existing_nodes: HashMap::new(), node_factory: DynamicNodeFactory::new(),
} }
} }
} }
@ -79,27 +94,14 @@ impl DynamicRule for MakeReadNodes {
type ChildOutput = ReadPostOutput; type ChildOutput = ReadPostOutput;
fn evaluate(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec<Input<Self::ChildOutput>> { fn evaluate(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec<Input<Self::ChildOutput>> {
for file in self.files.value().iter() { for file in self.files.value().iter() {
if !self.existing_nodes.contains_key(file) { self.node_factory
let input = ctx.add_rule(ReadPost { path: file.clone() }); .add_rule(ctx, file.clone(), || ReadPost { path: file.clone() });
self.existing_nodes.insert(file.clone(), input);
}
} }
// collect everything up front so we can mutably borrow existing_nodes self.node_factory.all_nodes(ctx)
let to_remove = self
.existing_nodes
.keys()
.filter(|key| !self.files.value().contains(key))
.cloned()
.collect::<Vec<_>>();
for key in to_remove {
ctx.remove_node(self.existing_nodes[&key].node_id());
self.existing_nodes.remove(&key);
}
self.existing_nodes.values().cloned().collect()
} }
} }
type ReadPostOutput = Option<Post<HtmlContent>>; pub type ReadPostOutput = Option<Post<HtmlContent>>;
#[derive(InputVisitable)] #[derive(InputVisitable)]
struct ReadPost { struct ReadPost {
@ -136,41 +138,27 @@ impl Rule for ReadPost {
#[derive(InputVisitable)] #[derive(InputVisitable)]
struct MakeExtractMetadatas { struct MakeExtractMetadatas {
posts: DynamicInput<ReadPostOutput>, posts: DynamicInput<ReadPostOutput>,
existing_nodes: HashMap<PathBuf, Input<ExtractMetadataOutput>>, node_factory: DynamicNodeFactory<PathBuf, ExtractMetadataOutput>,
} }
impl MakeExtractMetadatas { impl MakeExtractMetadatas {
fn new(posts: DynamicInput<ReadPostOutput>) -> Self { fn new(posts: DynamicInput<ReadPostOutput>) -> Self {
Self { Self {
posts, posts,
existing_nodes: HashMap::new(), node_factory: DynamicNodeFactory::new(),
} }
} }
} }
impl DynamicRule for MakeExtractMetadatas { impl DynamicRule for MakeExtractMetadatas {
type ChildOutput = ExtractMetadataOutput; type ChildOutput = ExtractMetadataOutput;
fn evaluate(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec<Input<Self::ChildOutput>> { fn evaluate(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec<Input<Self::ChildOutput>> {
let mut all_posts = vec![];
for post_input in self.posts.value().inputs.iter() { for post_input in self.posts.value().inputs.iter() {
let post_ = post_input.value(); let post_ = post_input.value();
let post = post_.as_ref().unwrap(); let post = post_.as_ref().unwrap();
all_posts.push(post.path.clone()); self.node_factory.add_rule(ctx, post.path.clone(), || {
ExtractMetadata(post_input.clone())
if !self.existing_nodes.contains_key(&post.path) { });
let input = ctx.add_rule(ExtractMetadata(post_input.clone()));
self.existing_nodes.insert(post.path.clone(), input);
}
} }
let keys = self self.node_factory.all_nodes(ctx)
.existing_nodes
.keys()
.filter(|key| !all_posts.contains(key))
.cloned()
.collect::<Vec<_>>();
for key in keys {
ctx.remove_node(self.existing_nodes[&key].node_id());
self.existing_nodes.remove(&key);
}
self.existing_nodes.values().cloned().collect()
} }
} }
@ -194,6 +182,64 @@ impl Rule for ExtractMetadata {
} }
} }
#[derive(InputVisitable)]
struct MakeWritePosts {
posts: DynamicInput<ReadPostOutput>,
node_factory: DynamicNodeFactory<PathBuf, ()>,
}
impl MakeWritePosts {
fn new(posts: DynamicInput<ReadPostOutput>) -> Self {
Self {
posts,
node_factory: DynamicNodeFactory::new(),
}
}
}
impl DynamicRule for MakeWritePosts {
type ChildOutput = ();
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(), || WritePost(post_input.clone()));
}
}
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()
}
}
/// Flattens Vec<Option<Post<HtmlContent>>> into Vec<Post<HtmlContent>> /// Flattens Vec<Option<Post<HtmlContent>>> into Vec<Post<HtmlContent>>
#[derive(InputVisitable)] #[derive(InputVisitable)]
struct AllPosts(DynamicInput<ReadPostOutput>); struct AllPosts(DynamicInput<ReadPostOutput>);

View File

@ -1,6 +1,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::anyhow; use anyhow::anyhow;
use chrono::Datelike;
use crate::generator::markdown; use crate::generator::markdown;
use crate::generator::posts::metadata::PostMetadata; use crate::generator::posts::metadata::PostMetadata;
@ -58,6 +59,10 @@ impl<C: PostContent> Post<C> {
content: self.content.to_html(), content: self.content.to_html(),
} }
} }
pub fn permalink(&self) -> String {
format!("/{}/{}/", self.metadata.date.year(), self.slug)
}
} }
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]

181
src/generator/tags.rs Normal file
View File

@ -0,0 +1,181 @@
use std::{collections::HashMap, path::PathBuf};
use askama::Template;
use compute_graph::{
builder::GraphBuilder,
rule::{DynamicInput, DynamicNodeFactory, DynamicRule, Input, InputVisitable, Rule},
synchronicity::Asynchronous,
};
use super::{
posts::{
ReadPostOutput,
metadata::{PostMetadata, Tag},
},
util::{MapDynamicToVoid, output_rendered_template, templates::TemplateCommon},
};
pub fn make_graph(
builder: &mut GraphBuilder<(), Asynchronous>,
posts: DynamicInput<ReadPostOutput>,
) -> Input<()> {
let by_tags = builder.add_dynamic_rule(MakePostsByTags::new(posts));
let write_tags = builder.add_dynamic_rule(MakeWriteTagPages::new(by_tags));
builder.add_rule(MapDynamicToVoid(write_tags))
}
#[derive(InputVisitable)]
struct MakePostsByTags {
posts: DynamicInput<ReadPostOutput>,
node_factory: DynamicNodeFactory<String, TagAndPosts>,
}
impl MakePostsByTags {
fn new(posts: DynamicInput<ReadPostOutput>) -> Self {
Self {
posts,
node_factory: DynamicNodeFactory::new(),
}
}
}
impl DynamicRule for MakePostsByTags {
type ChildOutput = TagAndPosts;
fn evaluate(
&mut self,
ctx: &mut impl compute_graph::rule::DynamicRuleContext,
) -> Vec<Input<Self::ChildOutput>> {
let mut all_tags = HashMap::new();
for post_input in self.posts().inputs.iter() {
let post = post_input.value();
for tag in post.as_ref().unwrap().metadata.tags.iter().flatten() {
all_tags.insert(tag.slug.clone(), tag.name.clone());
}
}
for (slug, name) in all_tags {
self.node_factory
.add_rule(ctx, slug.clone(), || PostsByTag {
posts: self.posts.clone(),
tag: Tag { slug, name },
});
}
self.node_factory.all_nodes(ctx)
}
}
#[derive(InputVisitable)]
struct PostsByTag {
posts: DynamicInput<ReadPostOutput>,
tag: Tag,
}
impl Rule for PostsByTag {
type Output = TagAndPosts;
fn evaluate(&mut self) -> Self::Output {
let entries = self
.posts()
.inputs
.iter()
.flat_map(|post_input| {
let post_ = post_input.value();
let post = post_.as_ref().unwrap();
let mut tags = post.metadata.tags.iter().flatten();
if tags.any(|t| t.slug == self.tag.slug) {
Some(Entry {
permalink: post.permalink(),
metadata: post.metadata.clone(),
})
} else {
None
}
})
.collect();
TagAndPosts {
tag: self.tag.clone(),
entries,
}
}
fn node_label(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.tag.slug)
}
}
#[derive(PartialEq, Clone)]
struct TagAndPosts {
tag: Tag,
entries: Vec<Entry>,
}
#[derive(PartialEq, Clone)]
struct Entry {
permalink: String,
metadata: PostMetadata,
}
#[derive(InputVisitable)]
struct MakeWriteTagPages {
tags: DynamicInput<TagAndPosts>,
node_factory: DynamicNodeFactory<String, ()>,
}
impl MakeWriteTagPages {
fn new(tags: DynamicInput<TagAndPosts>) -> Self {
Self {
tags,
node_factory: DynamicNodeFactory::new(),
}
}
}
impl DynamicRule for MakeWriteTagPages {
type ChildOutput = ();
fn evaluate(
&mut self,
ctx: &mut impl compute_graph::rule::DynamicRuleContext,
) -> 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(), || {
WriteTag(tag_input.clone())
});
}
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)
}
}

View File

@ -1,13 +1,59 @@
pub mod one_more; pub mod one_more;
pub mod slugify; pub mod slugify;
pub mod templates;
pub mod word_count; pub mod word_count;
use std::io::{BufWriter, Write};
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use anyhow::anyhow; use anyhow::anyhow;
use askama::Template;
use compute_graph::builder::GraphBuilder;
use compute_graph::rule::{DynamicInput, Input, InputVisitable, Rule}; use compute_graph::rule::{DynamicInput, Input, InputVisitable, Rule};
use compute_graph::synchronicity::Synchronicity;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
pub fn output_writer(path: impl AsRef<Path>) -> Result<impl Write, std::io::Error> {
let path = output_path(path);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::File::create(path)?;
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)> { pub fn from_frontmatter<D: DeserializeOwned>(contents: &str) -> anyhow::Result<(D, &str)> {
let mut chars = contents.char_indices(); let mut chars = contents.char_indices();
for i in 0..=2 { for i in 0..=2 {
@ -74,6 +120,40 @@ impl<T> Rule for MapDynamicToVoid<T> {
fn evaluate(&mut self) -> Self::Output {} fn evaluate(&mut self) -> Self::Output {}
} }
#[derive(InputVisitable)]
pub struct Combine(pub Input<()>, pub Input<()>);
impl Combine {
pub fn make<O: 'static, S: Synchronicity>(
builder: &mut GraphBuilder<O, S>,
inputs: &[Input<()>],
) -> Input<()> {
if inputs.is_empty() {
panic!("can only create a combine rule with one or more inputs")
} else if inputs.len() == 1 {
inputs[0].clone()
} else {
let input = builder.add_rule(Combine(inputs[0].clone(), inputs[1].clone()));
Self::make_(builder, input, &inputs[2..])
}
}
fn make_<O: 'static, S: Synchronicity>(
builder: &mut GraphBuilder<O, S>,
acc: Input<()>,
rest: &[Input<()>],
) -> Input<()> {
if rest.is_empty() {
acc
} else {
let input = builder.add_rule(Combine(acc, rest[0].clone()));
Self::make_(builder, input, &rest[1..])
}
}
}
impl Rule for Combine {
type Output = ();
fn evaluate(&mut self) -> Self::Output {}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::join_abs; use super::join_abs;

View File

@ -1,8 +1,10 @@
use crate::activitypub::DOMAIN;
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::time::SystemTime; use std::time::SystemTime;
static DOMAIN: Lazy<String> =
Lazy::new(|| std::env::var("DOMAIN").unwrap_or("shadowfacts.net".to_owned()));
static CB: Lazy<u64> = Lazy::new(|| { static CB: Lazy<u64> = Lazy::new(|| {
SystemTime::now() SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
@ -21,17 +23,6 @@ pub trait TemplateCommon {
*CB *CB
} }
fn fancy_link(text: &str, href: &str, meta: Option<&str>) -> String {
format!(
r#"
<a href="{}" class="fancy-link" {}><span aria-hidden="true">{{</span>{}<span aria-hidden="true">}}</span></a>
"#,
href,
meta.unwrap_or(""),
text
)
}
fn generated_at() -> &'static DateTime<Local> { fn generated_at() -> &'static DateTime<Local> {
&*GENERATED_AT &*GENERATED_AT
} }
@ -43,9 +34,11 @@ pub mod filters {
use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc}; use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc};
pub fn iso_date(date: &NaiveDate) -> askama::Result<String> { pub fn iso_date(date: &NaiveDate) -> askama::Result<String> {
Ok(DateTime::<Utc>::from_utc(date.and_hms(12, 0, 0), Utc) Ok(
.format("%+:0") Utc::from_utc_datetime(&Utc, &date.and_hms_opt(12, 0, 0).unwrap())
.to_string()) .format("%+:0")
.to_string(),
)
} }
pub fn iso_datetime<Tz>(datetime: &DateTime<Tz>) -> askama::Result<String> pub fn iso_datetime<Tz>(datetime: &DateTime<Tz>) -> askama::Result<String>