diff --git a/Cargo.lock b/Cargo.lock
index c2a5d8a..dea8d6a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -54,9 +54,9 @@ checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "ahash"
-version = "0.7.6"
+version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [
"getrandom",
"once_cell",
@@ -362,6 +362,8 @@ version = "0.1.0"
dependencies = [
"compute_graph_macros",
"petgraph",
+ "quote",
+ "syn 2.0.85",
"tokio",
]
@@ -2687,6 +2689,7 @@ dependencies = [
"base64",
"chrono",
"clap",
+ "compute_graph",
"env_logger",
"futures",
"html5ever",
diff --git a/Cargo.toml b/Cargo.toml
index ceaaea9..ee94354 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,4 +1,8 @@
-workspace = { members = ["crates/compute_graph", "crates/compute_graph_macros", "crates/derive_test"] }
+workspace = { members = [
+ "crates/compute_graph",
+ "crates/compute_graph_macros",
+ "crates/derive_test",
+] }
[package]
name = "v6"
@@ -21,6 +25,7 @@ axum = "0.5.6"
base64 = "0.13"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "3.1", features = ["cargo"] }
+compute_graph = { path = "crates/compute_graph" }
env_logger = "0.9"
futures = "0.3"
html5ever = "0.26"
diff --git a/site/archive.html b/site/archive.html
index f568a6e..778356f 100644
--- a/site/archive.html
+++ b/site/archive.html
@@ -9,8 +9,8 @@
{% for post in posts %}
-
-
- {{ post.metadata.title }}
+
+ {{ post.title }}
{% endfor %}
diff --git a/src/generator/archive.rs b/src/generator/archive.rs
index e8a2513..a5b5331 100644
--- a/src/generator/archive.rs
+++ b/src/generator/archive.rs
@@ -19,8 +19,27 @@ pub fn generate(posts: &[Post]) {
years.sort_by_key(|(year, _)| *year);
years.reverse();
- output_rendered_template(&ArchiveTemplate { years: &years }, "archive/index.html")
- .expect("generating archive");
+ output_rendered_template(
+ &ArchiveTemplate {
+ years: &(years
+ .iter()
+ .map(|(year, posts)| {
+ (
+ *year,
+ posts
+ .iter()
+ .map(|post| ArchivePost {
+ permalink: post.permalink(),
+ title: post.metadata.title.clone(),
+ })
+ .collect::>(),
+ )
+ })
+ .collect::>()),
+ },
+ "archive/index.html",
+ )
+ .expect("generating archive");
for (year, posts) in years {
let template = YearTemplate {
@@ -32,10 +51,16 @@ pub fn generate(posts: &[Post]) {
}
}
+#[derive(PartialEq)]
+pub struct ArchivePost {
+ pub permalink: String,
+ pub title: String,
+}
+
#[derive(Template)]
#[template(path = "archive.html")]
struct ArchiveTemplate<'a> {
- years: &'a Vec<(i32, Vec<&'a Post>)>,
+ years: &'a [(i32, Vec)],
}
impl<'a> TemplateCommon for ArchiveTemplate<'a> {}
diff --git a/src/generator/mod.rs b/src/generator/mod.rs
index ee77bee..528db97 100644
--- a/src/generator/mod.rs
+++ b/src/generator/mod.rs
@@ -1,16 +1,16 @@
-mod archive;
+pub mod archive;
mod copy;
mod css;
mod highlight;
mod home;
mod markdown;
mod pagination;
-mod posts;
+pub mod posts;
mod rss;
mod tags;
mod tutorials;
mod tv;
-mod util;
+pub mod util;
pub use crate::generator::posts::parse::{HtmlContent, Post};
use std::path::{Component, Path, PathBuf};
diff --git a/src/generator/posts/parse.rs b/src/generator/posts/parse.rs
index 3270466..c8a359e 100644
--- a/src/generator/posts/parse.rs
+++ b/src/generator/posts/parse.rs
@@ -58,7 +58,7 @@ pub fn prepare_post(post: Post) -> Post {
new_post
}
-#[derive(Debug)]
+#[derive(Debug, PartialEq)]
pub struct Post {
pub path: PathBuf,
pub metadata: PostMetadata,
@@ -122,7 +122,7 @@ impl Post {
}
}
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(deny_unknown_fields)]
pub struct PostMetadata {
pub title: String,
@@ -184,7 +184,7 @@ where
deserializer.deserialize_any(StringOrVec)
}
-#[derive(Debug, Eq)]
+#[derive(Debug, Eq, Clone)]
pub struct Tag {
pub name: String,
pub slug: String,
@@ -213,12 +213,12 @@ impl Hash for Tag {
}
}
-pub trait PostContent: std::fmt::Debug {
+pub trait PostContent: std::fmt::Debug + PartialEq {
fn to_html(self) -> HtmlContent;
fn word_count(&self) -> u32;
}
-#[derive(Debug)]
+#[derive(Debug, PartialEq)]
pub enum AnyContent {
Markdown(MarkdownContent),
Html(HtmlContent),
@@ -240,7 +240,7 @@ impl PostContent for AnyContent {
}
}
-#[derive(Debug)]
+#[derive(Debug, PartialEq)]
pub struct MarkdownContent(String);
impl PostContent for MarkdownContent {
@@ -255,7 +255,7 @@ impl PostContent for MarkdownContent {
}
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
pub struct HtmlContent(String);
impl PostContent for HtmlContent {
diff --git a/src/graph_generator/archive.rs b/src/graph_generator/archive.rs
new file mode 100644
index 0000000..7d526c9
--- /dev/null
+++ b/src/graph_generator/archive.rs
@@ -0,0 +1,85 @@
+use crate::generator::util::output_rendered_template;
+use crate::generator::util::templates::{filters, TemplateCommon};
+use crate::generator::{archive::ArchivePost, HtmlContent, Post};
+use askama::Template;
+use chrono::Datelike;
+use compute_graph::{
+ builder::GraphBuilder,
+ rule::{Input, InputVisitable, Rule},
+ synchronicity::Asynchronous,
+};
+use std::collections::HashMap;
+
+pub fn make_graph(
+ builder: &mut GraphBuilder,
+ posts: &[Input>],
+) -> Input<()> {
+ let posts_by_year = builder.add_rule(PostsByYear(posts.into()));
+
+ let archive = builder.add_rule(GenerateArchive(posts_by_year));
+
+ // TODO: paginated year posts indexes
+
+ archive
+}
+
+struct PostsByYear(Vec>>);
+
+impl InputVisitable for PostsByYear {
+ fn visit_inputs(&self, visitor: &mut impl compute_graph::rule::InputVisitor) {
+ for input in &self.0 {
+ visitor.visit(input);
+ }
+ }
+}
+
+impl Rule for PostsByYear {
+ type Output = HashMap>;
+
+ fn evaluate(&mut self) -> Self::Output {
+ let mut map = HashMap::new();
+ for input in &self.0 {
+ let post = input.value();
+ let year_posts: &mut Vec =
+ map.entry(post.metadata.date.year()).or_default();
+ year_posts.push(ArchivePost {
+ permalink: post.permalink(),
+ title: post.metadata.title.clone(),
+ });
+ }
+ map
+ }
+}
+
+#[derive(InputVisitable)]
+struct GenerateArchive(Input<::Output>);
+
+impl Rule for GenerateArchive {
+ type Output = ();
+
+ fn evaluate(&mut self) -> Self::Output {
+ let posts_by_year = self.input_0();
+ let mut years = posts_by_year
+ .iter()
+ .map(|(y, ps)| (*y, ps))
+ .collect::>();
+ years.sort_by_key(|(year, _)| *year);
+ years.reverse();
+ let template = ArchiveTemplate { years: &years };
+ output_rendered_template(&template, "archive/index.html").expect("generate archive");
+ }
+}
+
+#[derive(Template)]
+#[template(path = "archive.html")]
+struct ArchiveTemplate<'a> {
+ years: &'a [(i32, &'a Vec)],
+}
+
+impl<'a> ArchiveTemplate<'a> {
+ fn permalink(&self) -> &'static str {
+ "/archive/"
+ }
+}
+
+impl<'a> TemplateCommon for ArchiveTemplate<'a> {}
diff --git a/src/graph_generator/mod.rs b/src/graph_generator/mod.rs
new file mode 100644
index 0000000..db719f3
--- /dev/null
+++ b/src/graph_generator/mod.rs
@@ -0,0 +1,54 @@
+mod archive;
+mod posts;
+
+use compute_graph::{
+ builder::GraphBuilder,
+ rule::{Input, InputVisitable, Rule},
+ AsyncGraph,
+};
+
+pub async fn generate() -> anyhow::Result<()> {
+ std::fs::create_dir_all("out").expect("creating output dir");
+
+ // TODO: file watching
+
+ let mut graph = make_graph()?;
+
+ graph.evaluate_async().await;
+
+ println!("{}", graph.as_dot_string());
+
+ Ok(())
+}
+
+fn make_graph() -> anyhow::Result> {
+ let mut builder = GraphBuilder::::new_async();
+
+ let posts = posts::list_post_files()?;
+ let posts = posts
+ .into_iter()
+ .map(|path| builder.add_rule(posts::ParsePost(path)))
+ .collect::>();
+ // let post_metadatas = posts
+ // .iter()
+ // .map(|post| builder.add_rule(posts::ExtractMetadata(post.clone())))
+ // .collect::>();
+
+ let archive = archive::make_graph(&mut builder, &posts);
+
+ builder.set_output(Output { archive });
+ Ok(builder.build()?)
+}
+
+type GraphOutput = ();
+
+#[derive(InputVisitable)]
+struct Output {
+ archive: Input<()>,
+}
+impl Rule for Output {
+ type Output = GraphOutput;
+ fn evaluate(&mut self) -> Self::Output {
+ ()
+ }
+}
diff --git a/src/graph_generator/posts.rs b/src/graph_generator/posts.rs
new file mode 100644
index 0000000..6238f19
--- /dev/null
+++ b/src/graph_generator/posts.rs
@@ -0,0 +1,61 @@
+use crate::generator::posts::parse::{parse_post, prepare_post, PostContent, PostMetadata};
+use crate::generator::{content_path, HtmlContent, Post};
+use compute_graph::rule::{Input, InputVisitable, Rule};
+use std::fs;
+use std::path::PathBuf;
+
+pub fn list_post_files() -> anyhow::Result> {
+ let posts_path = content_path("posts/");
+ let mut paths = vec![];
+ for ent in fs::read_dir(posts_path)? {
+ let Ok(ent) = ent else {
+ continue;
+ };
+ let Ok(ty) = ent.file_type() else {
+ continue;
+ };
+ if ty.is_dir() {
+ paths.push(find_index(ent.path()).expect("folder posts must have index file"));
+ } else {
+ paths.push(ent.path());
+ }
+ }
+ Ok(paths)
+}
+
+fn find_index(path: PathBuf) -> Option {
+ let dir = fs::read_dir(path).ok()?;
+ dir.flatten()
+ .find(|e| e.path().file_stem().unwrap().eq_ignore_ascii_case("index"))
+ .map(|e| e.path())
+}
+
+#[derive(InputVisitable)]
+pub struct ParsePost(pub PathBuf);
+
+impl Rule for ParsePost {
+ type Output = Post;
+ fn evaluate(&mut self) -> Self::Output {
+ match parse_post(self.0.clone()) {
+ Ok(post) => prepare_post(post),
+ Err(e) => {
+ let path = self.0.to_string_lossy();
+ panic!("Failed to parse post {path}: {e}")
+ }
+ }
+ }
+
+ fn node_label(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{}", self.0.file_stem().unwrap().to_string_lossy())
+ }
+}
+
+#[derive(InputVisitable)]
+pub struct ExtractMetadata(pub Input>);
+
+impl Rule for ExtractMetadata {
+ type Output = PostMetadata;
+ fn evaluate(&mut self) -> Self::Output {
+ self.input_0().metadata.clone()
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 051845f..656120f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,6 +2,7 @@
mod activitypub;
mod generator;
+mod graph_generator;
use crate::generator::{HtmlContent, Post};
use axum::{
@@ -35,6 +36,7 @@ async fn main() {
let matches = command!()
.subcommand_required(true)
.arg_required_else_help(true)
+ .subcommand(Command::new("graph-gen"))
.subcommand(Command::new("gen"))
.subcommand(
Command::new("serve")
@@ -49,6 +51,9 @@ async fn main() {
}
match matches.subcommand() {
+ Some(("graph-gen", _)) => {
+ graph_generator::generate().await.unwrap();
+ }
Some(("gen", _)) => {
let _ = generate().await;
}