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]]
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",

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]
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"

View File

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

View File

@ -19,7 +19,26 @@ pub fn generate(posts: &[Post<HtmlContent>]) {
years.sort_by_key(|(year, _)| *year);
years.reverse();
output_rendered_template(&ArchiveTemplate { years: &years }, "archive/index.html")
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::<Vec<_>>(),
)
})
.collect::<Vec<_>>()),
},
"archive/index.html",
)
.expect("generating archive");
for (year, posts) in years {
@ -32,10 +51,16 @@ pub fn generate(posts: &[Post<HtmlContent>]) {
}
}
#[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<HtmlContent>>)>,
years: &'a [(i32, Vec<ArchivePost>)],
}
impl<'a> TemplateCommon for ArchiveTemplate<'a> {}

View File

@ -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};

View File

@ -58,7 +58,7 @@ pub fn prepare_post(post: Post<AnyContent>) -> Post<HtmlContent> {
new_post
}
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub struct Post<Content: PostContent> {
pub path: PathBuf,
pub metadata: PostMetadata,
@ -122,7 +122,7 @@ impl<C: PostContent> Post<C> {
}
}
#[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 {

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 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;
}