Output stuff
This commit is contained in:
parent
9ff658f719
commit
6bb51638cc
101
Cargo.lock
generated
101
Cargo.lock
generated
@ -96,6 +96,50 @@ 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"
|
||||
@ -117,6 +161,15 @@ 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 = "2.6.0"
|
||||
@ -348,6 +401,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
@ -554,6 +616,12 @@ version = "0.2.169"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.7.4"
|
||||
@ -614,6 +682,28 @@ 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"
|
||||
@ -640,6 +730,16 @@ 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 = "num-traits"
|
||||
version = "0.2.19"
|
||||
@ -1208,6 +1308,7 @@ name = "v7"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"askama",
|
||||
"chrono",
|
||||
"clap",
|
||||
"compute_graph",
|
||||
|
@ -17,6 +17,7 @@ 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" }
|
||||
|
@ -1,2 +1,2 @@
|
||||
[general]
|
||||
dirs = ["site"]
|
||||
dirs = ["site_test"]
|
||||
|
@ -57,6 +57,11 @@ impl<O: 'static, S: Synchronicity> GraphBuilder<O, S> {
|
||||
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.
|
||||
pub fn with_output<R: Rule>(mut self, rule: R) -> GraphBuilder<R::Output, S> {
|
||||
let input = self.add_rule(rule);
|
||||
|
@ -26,7 +26,8 @@
|
||||
//! ```
|
||||
//!
|
||||
//! 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:
|
||||
//!
|
||||
//! ```rust
|
||||
@ -165,7 +166,7 @@ impl<O: 'static, S: Synchronicity> Graph<O, S> {
|
||||
let target = graph.to_index(edge.target());
|
||||
if !old_edges
|
||||
.get(&source)
|
||||
.map_or(false, |old| !old.contains(&target))
|
||||
.map_or(false, |old| old.contains(&target))
|
||||
{
|
||||
to_invalidate.push_back(edge.target());
|
||||
}
|
||||
@ -926,5 +927,6 @@ mod tests {
|
||||
assert_eq!(*graph.evaluate_async().await, 3);
|
||||
set_count.set_value(4);
|
||||
assert_eq!(*graph.evaluate_async().await, 10);
|
||||
println!("{}", graph.as_dot_string());
|
||||
}
|
||||
}
|
||||
|
@ -260,6 +260,8 @@ fn visit_inputs<V: InputVisitable>(visitable: &V, visitor: &mut dyn FnMut(NodeId
|
||||
// And visit all the nodes it produces
|
||||
let maybe_dynamic_output = input.input.value.borrow();
|
||||
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() {
|
||||
self.visit(input);
|
||||
}
|
||||
@ -686,8 +688,9 @@ impl<R: AsyncDynamicRule> std::fmt::Debug for AsyncDynamicRuleNode<R, R::ChildOu
|
||||
}
|
||||
|
||||
fn pretty_type_name<T>() -> String {
|
||||
let s = std::any::type_name::<T>();
|
||||
let ty = syn::parse_str::<syn::Type>(s).unwrap();
|
||||
// idk where the {{closure}} comes from in one of the tests, just do this to avoid panicking
|
||||
let s = std::any::type_name::<T>().replace("{{closure}}", "__closure__");
|
||||
let ty = syn::parse_str::<syn::Type>(&s).unwrap();
|
||||
pretty_type_name_type(ty)
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,9 @@ use crate::node::{DynamicRuleOutput, NodeValue};
|
||||
use crate::NodeId;
|
||||
pub use compute_graph_macros::InputVisitable;
|
||||
use std::cell::{Cell, Ref, RefCell};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::future::Future;
|
||||
use std::hash::Hash;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
|
||||
@ -88,6 +90,8 @@ pub trait DynamicRule: InputVisitable + 'static {
|
||||
/// Evaluates this rule, producing additional nodes.
|
||||
///
|
||||
/// 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>>;
|
||||
|
||||
#[allow(unused_variables)]
|
||||
@ -114,6 +118,79 @@ pub trait DynamicRuleContext {
|
||||
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.
|
||||
///
|
||||
/// See [`DynamicRule`].
|
||||
@ -124,6 +201,8 @@ pub trait AsyncDynamicRule: InputVisitable + 'static {
|
||||
/// Evaluates this rule asynchronously, producing additional nodes.
|
||||
///
|
||||
/// 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>(
|
||||
&'a mut self,
|
||||
ctx: &'a mut impl AsyncDynamicRuleContext,
|
||||
|
20
site_test/archive.html
Normal file
20
site_test/archive.html
Normal 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 %}
|
30
site_test/layout/article.html
Normal file
30
site_test/layout/article.html
Normal 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>
|
37
site_test/layout/default.html
Normal file
37
site_test/layout/default.html
Normal 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
9
site_test/tag.html
Normal 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
106
src/generator/archive.rs
Normal 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(&[])
|
||||
}
|
||||
}
|
@ -1,13 +1,11 @@
|
||||
mod archive;
|
||||
mod markdown;
|
||||
mod posts;
|
||||
mod tags;
|
||||
mod util;
|
||||
|
||||
use compute_graph::{
|
||||
AsyncGraph,
|
||||
builder::GraphBuilder,
|
||||
rule::{Input, InputVisitable, Rule},
|
||||
};
|
||||
use util::MapToVoid;
|
||||
use compute_graph::{AsyncGraph, builder::GraphBuilder};
|
||||
use util::{Combine, MapToVoid};
|
||||
|
||||
pub async fn generate() -> anyhow::Result<()> {
|
||||
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<()>> {
|
||||
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));
|
||||
builder.set_output(Output {
|
||||
posts: posts_output,
|
||||
});
|
||||
let archive = archive::make_graph(&mut builder, all_posts);
|
||||
|
||||
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()?)
|
||||
}
|
||||
|
||||
#[derive(InputVisitable)]
|
||||
struct Output {
|
||||
posts: Input<()>,
|
||||
}
|
||||
impl Rule for Output {
|
||||
type Output = ();
|
||||
fn evaluate(&mut self) -> Self::Output {}
|
||||
}
|
||||
// #[derive(InputVisitable)]
|
||||
// struct Output {
|
||||
// archive: Input<()>,
|
||||
// posts: Input<()>,
|
||||
// }
|
||||
// impl Rule for Output {
|
||||
// type Output = ();
|
||||
// fn evaluate(&mut self) -> Self::Output {}
|
||||
// }
|
||||
|
@ -1,29 +1,44 @@
|
||||
mod content;
|
||||
mod metadata;
|
||||
pub mod content;
|
||||
pub mod metadata;
|
||||
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use askama::Template;
|
||||
use compute_graph::{
|
||||
builder::GraphBuilder,
|
||||
rule::{DynamicInput, DynamicRule, DynamicRuleContext, Input, InputVisitable, Rule},
|
||||
rule::{
|
||||
DynamicInput, DynamicNodeFactory, DynamicRule, DynamicRuleContext, Input, InputVisitable,
|
||||
Rule,
|
||||
},
|
||||
synchronicity::Asynchronous,
|
||||
};
|
||||
use content::{HtmlContent, Post};
|
||||
use log::error;
|
||||
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(
|
||||
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
|
||||
let post_files = builder.add_rule(ListPostFiles);
|
||||
let posts = builder.add_dynamic_rule(MakeReadNodes::new(post_files));
|
||||
|
||||
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(AllMetadatas(extract_metadatas)),
|
||||
)
|
||||
@ -65,13 +80,13 @@ fn find_index(path: PathBuf) -> Option<PathBuf> {
|
||||
#[derive(InputVisitable)]
|
||||
struct MakeReadNodes {
|
||||
files: Input<Vec<PathBuf>>,
|
||||
existing_nodes: HashMap<PathBuf, Input<ReadPostOutput>>,
|
||||
node_factory: DynamicNodeFactory<PathBuf, ReadPostOutput>,
|
||||
}
|
||||
impl MakeReadNodes {
|
||||
fn new(files: Input<Vec<PathBuf>>) -> Self {
|
||||
Self {
|
||||
files,
|
||||
existing_nodes: HashMap::new(),
|
||||
node_factory: DynamicNodeFactory::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -79,27 +94,14 @@ 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() {
|
||||
if !self.existing_nodes.contains_key(file) {
|
||||
let input = ctx.add_rule(ReadPost { path: file.clone() });
|
||||
self.existing_nodes.insert(file.clone(), input);
|
||||
}
|
||||
self.node_factory
|
||||
.add_rule(ctx, file.clone(), || ReadPost { path: file.clone() });
|
||||
}
|
||||
// collect everything up front so we can mutably borrow existing_nodes
|
||||
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()
|
||||
self.node_factory.all_nodes(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
type ReadPostOutput = Option<Post<HtmlContent>>;
|
||||
pub type ReadPostOutput = Option<Post<HtmlContent>>;
|
||||
|
||||
#[derive(InputVisitable)]
|
||||
struct ReadPost {
|
||||
@ -136,41 +138,27 @@ impl Rule for ReadPost {
|
||||
#[derive(InputVisitable)]
|
||||
struct MakeExtractMetadatas {
|
||||
posts: DynamicInput<ReadPostOutput>,
|
||||
existing_nodes: HashMap<PathBuf, Input<ExtractMetadataOutput>>,
|
||||
node_factory: DynamicNodeFactory<PathBuf, ExtractMetadataOutput>,
|
||||
}
|
||||
impl MakeExtractMetadatas {
|
||||
fn new(posts: DynamicInput<ReadPostOutput>) -> Self {
|
||||
Self {
|
||||
posts,
|
||||
existing_nodes: HashMap::new(),
|
||||
node_factory: DynamicNodeFactory::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl DynamicRule for MakeExtractMetadatas {
|
||||
type ChildOutput = ExtractMetadataOutput;
|
||||
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() {
|
||||
let post_ = post_input.value();
|
||||
let post = post_.as_ref().unwrap();
|
||||
all_posts.push(post.path.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);
|
||||
}
|
||||
self.node_factory.add_rule(ctx, post.path.clone(), || {
|
||||
ExtractMetadata(post_input.clone())
|
||||
});
|
||||
}
|
||||
let keys = self
|
||||
.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()
|
||||
self.node_factory.all_nodes(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>>
|
||||
#[derive(InputVisitable)]
|
||||
struct AllPosts(DynamicInput<ReadPostOutput>);
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use chrono::Datelike;
|
||||
|
||||
use crate::generator::markdown;
|
||||
use crate::generator::posts::metadata::PostMetadata;
|
||||
@ -58,6 +59,10 @@ impl<C: PostContent> Post<C> {
|
||||
content: self.content.to_html(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn permalink(&self) -> String {
|
||||
format!("/{}/{}/", self.metadata.date.year(), self.slug)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
|
181
src/generator/tags.rs
Normal file
181
src/generator/tags.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -1,13 +1,59 @@
|
||||
pub mod one_more;
|
||||
pub mod slugify;
|
||||
pub mod templates;
|
||||
pub mod word_count;
|
||||
|
||||
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;
|
||||
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)> {
|
||||
let mut chars = contents.char_indices();
|
||||
for i in 0..=2 {
|
||||
@ -74,6 +120,40 @@ impl<T> Rule for MapDynamicToVoid<T> {
|
||||
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)]
|
||||
mod tests {
|
||||
use super::join_abs;
|
||||
|
@ -1,8 +1,10 @@
|
||||
use crate::activitypub::DOMAIN;
|
||||
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 CB: Lazy<u64> = Lazy::new(|| {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
@ -21,17 +23,6 @@ pub trait TemplateCommon {
|
||||
*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> {
|
||||
&*GENERATED_AT
|
||||
}
|
||||
@ -43,9 +34,11 @@ pub mod filters {
|
||||
use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc};
|
||||
|
||||
pub fn iso_date(date: &NaiveDate) -> askama::Result<String> {
|
||||
Ok(DateTime::<Utc>::from_utc(date.and_hms(12, 0, 0), Utc)
|
||||
.format("%+:0")
|
||||
.to_string())
|
||||
Ok(
|
||||
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>
|
Loading…
x
Reference in New Issue
Block a user