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]]
|
[[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",
|
||||||
|
@ -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"
|
||||||
|
@ -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 %}
|
||||||
|
@ -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> {}
|
||||||
|
@ -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};
|
||||||
|
@ -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 {
|
||||||
|
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 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;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user