Start implementing graph-backed generator
This commit is contained in:
parent
8f0fe08ecc
commit
9cb6a8c6ce
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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 %}
|
||||
|
@ -19,8 +19,27 @@ pub fn generate(posts: &[Post<HtmlContent>]) {
|
||||
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::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()),
|
||||
},
|
||||
"archive/index.html",
|
||||
)
|
||||
.expect("generating archive");
|
||||
|
||||
for (year, posts) in years {
|
||||
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)]
|
||||
#[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> {}
|
||||
|
@ -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};
|
||||
|
@ -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 {
|
||||
|
85
src/graph_generator/archive.rs
Normal file
85
src/graph_generator/archive.rs
Normal 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> {}
|
54
src/graph_generator/mod.rs
Normal file
54
src/graph_generator/mod.rs
Normal 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 {
|
||||
()
|
||||
}
|
||||
}
|
61
src/graph_generator/posts.rs
Normal file
61
src/graph_generator/posts.rs
Normal 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()
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user