Start implementing graph-backed generator

This commit is contained in:
Shadowfacts 2024-11-05 11:19:48 -05:00
parent 8f0fe08ecc
commit 9cb6a8c6ce
10 changed files with 256 additions and 18 deletions

7
Cargo.lock generated
View File

@ -54,9 +54,9 @@ checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.7.6" version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"once_cell", "once_cell",
@ -362,6 +362,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"compute_graph_macros", "compute_graph_macros",
"petgraph", "petgraph",
"quote",
"syn 2.0.85",
"tokio", "tokio",
] ]
@ -2687,6 +2689,7 @@ dependencies = [
"base64", "base64",
"chrono", "chrono",
"clap", "clap",
"compute_graph",
"env_logger", "env_logger",
"futures", "futures",
"html5ever", "html5ever",

View File

@ -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] [package]
name = "v6" name = "v6"
@ -21,6 +25,7 @@ axum = "0.5.6"
base64 = "0.13" base64 = "0.13"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
clap = { version = "3.1", features = ["cargo"] } clap = { version = "3.1", features = ["cargo"] }
compute_graph = { path = "crates/compute_graph" }
env_logger = "0.9" env_logger = "0.9"
futures = "0.3" futures = "0.3"
html5ever = "0.26" html5ever = "0.26"

View File

@ -9,8 +9,8 @@
<ul> <ul>
{% for post in posts %} {% for post in posts %}
<li> <li>
<a href="{{ post.permalink() }}"> <a href="{{ post.permalink }}">
{{ post.metadata.title }} {{ post.title }}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}

View File

@ -19,8 +19,27 @@ pub fn generate(posts: &[Post<HtmlContent>]) {
years.sort_by_key(|(year, _)| *year); years.sort_by_key(|(year, _)| *year);
years.reverse(); years.reverse();
output_rendered_template(&ArchiveTemplate { years: &years }, "archive/index.html") output_rendered_template(
.expect("generating archive"); &ArchiveTemplate {
years: &(years
.iter()
.map(|(year, posts)| {
(
*year,
posts
.iter()
.map(|post| ArchivePost {
permalink: post.permalink(),
title: post.metadata.title.clone(),
})
.collect::<Vec<_>>(),
)
})
.collect::<Vec<_>>()),
},
"archive/index.html",
)
.expect("generating archive");
for (year, posts) in years { for (year, posts) in years {
let template = YearTemplate { let template = YearTemplate {
@ -32,10 +51,16 @@ pub fn generate(posts: &[Post<HtmlContent>]) {
} }
} }
#[derive(PartialEq)]
pub struct ArchivePost {
pub permalink: String,
pub title: String,
}
#[derive(Template)] #[derive(Template)]
#[template(path = "archive.html")] #[template(path = "archive.html")]
struct ArchiveTemplate<'a> { struct ArchiveTemplate<'a> {
years: &'a Vec<(i32, Vec<&'a Post<HtmlContent>>)>, years: &'a [(i32, Vec<ArchivePost>)],
} }
impl<'a> TemplateCommon for ArchiveTemplate<'a> {} impl<'a> TemplateCommon for ArchiveTemplate<'a> {}

View File

@ -1,16 +1,16 @@
mod archive; pub mod archive;
mod copy; mod copy;
mod css; mod css;
mod highlight; mod highlight;
mod home; mod home;
mod markdown; mod markdown;
mod pagination; mod pagination;
mod posts; pub mod posts;
mod rss; mod rss;
mod tags; mod tags;
mod tutorials; mod tutorials;
mod tv; mod tv;
mod util; pub mod util;
pub use crate::generator::posts::parse::{HtmlContent, Post}; pub use crate::generator::posts::parse::{HtmlContent, Post};
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};

View File

@ -58,7 +58,7 @@ pub fn prepare_post(post: Post<AnyContent>) -> Post<HtmlContent> {
new_post new_post
} }
#[derive(Debug)] #[derive(Debug, PartialEq)]
pub struct Post<Content: PostContent> { pub struct Post<Content: PostContent> {
pub path: PathBuf, pub path: PathBuf,
pub metadata: PostMetadata, pub metadata: PostMetadata,
@ -122,7 +122,7 @@ impl<C: PostContent> Post<C> {
} }
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, PartialEq, Clone)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct PostMetadata { pub struct PostMetadata {
pub title: String, pub title: String,
@ -184,7 +184,7 @@ where
deserializer.deserialize_any(StringOrVec) deserializer.deserialize_any(StringOrVec)
} }
#[derive(Debug, Eq)] #[derive(Debug, Eq, Clone)]
pub struct Tag { pub struct Tag {
pub name: String, pub name: String,
pub slug: 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 to_html(self) -> HtmlContent;
fn word_count(&self) -> u32; fn word_count(&self) -> u32;
} }
#[derive(Debug)] #[derive(Debug, PartialEq)]
pub enum AnyContent { pub enum AnyContent {
Markdown(MarkdownContent), Markdown(MarkdownContent),
Html(HtmlContent), Html(HtmlContent),
@ -240,7 +240,7 @@ impl PostContent for AnyContent {
} }
} }
#[derive(Debug)] #[derive(Debug, PartialEq)]
pub struct MarkdownContent(String); pub struct MarkdownContent(String);
impl PostContent for MarkdownContent { impl PostContent for MarkdownContent {
@ -255,7 +255,7 @@ impl PostContent for MarkdownContent {
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct HtmlContent(String); pub struct HtmlContent(String);
impl PostContent for HtmlContent { impl PostContent for HtmlContent {

View File

@ -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<super::GraphOutput, Asynchronous>,
posts: &[Input<Post<HtmlContent>>],
) -> 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<Input<Post<HtmlContent>>>);
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<i32, Vec<ArchivePost>>;
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<ArchivePost> =
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<<PostsByYear as Rule>::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::<Vec<_>>();
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<ArchivePost>)],
}
impl<'a> ArchiveTemplate<'a> {
fn permalink(&self) -> &'static str {
"/archive/"
}
}
impl<'a> TemplateCommon for ArchiveTemplate<'a> {}

View File

@ -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<AsyncGraph<GraphOutput>> {
let mut builder = GraphBuilder::<GraphOutput, _>::new_async();
let posts = posts::list_post_files()?;
let posts = posts
.into_iter()
.map(|path| builder.add_rule(posts::ParsePost(path)))
.collect::<Vec<_>>();
// let post_metadatas = posts
// .iter()
// .map(|post| builder.add_rule(posts::ExtractMetadata(post.clone())))
// .collect::<Vec<_>>();
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 {
()
}
}

View File

@ -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<Vec<PathBuf>> {
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<PathBuf> {
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<HtmlContent>;
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<C: PostContent>(pub Input<Post<C>>);
impl<C: PostContent + 'static> Rule for ExtractMetadata<C> {
type Output = PostMetadata;
fn evaluate(&mut self) -> Self::Output {
self.input_0().metadata.clone()
}
}

View File

@ -2,6 +2,7 @@
mod activitypub; mod activitypub;
mod generator; mod generator;
mod graph_generator;
use crate::generator::{HtmlContent, Post}; use crate::generator::{HtmlContent, Post};
use axum::{ use axum::{
@ -35,6 +36,7 @@ async fn main() {
let matches = command!() let matches = command!()
.subcommand_required(true) .subcommand_required(true)
.arg_required_else_help(true) .arg_required_else_help(true)
.subcommand(Command::new("graph-gen"))
.subcommand(Command::new("gen")) .subcommand(Command::new("gen"))
.subcommand( .subcommand(
Command::new("serve") Command::new("serve")
@ -49,6 +51,9 @@ async fn main() {
} }
match matches.subcommand() { match matches.subcommand() {
Some(("graph-gen", _)) => {
graph_generator::generate().await.unwrap();
}
Some(("gen", _)) => { Some(("gen", _)) => {
let _ = generate().await; let _ = generate().await;
} }