@@ -49,7 +47,9 @@ {{ tag.name }}{% if loop.last %}.{% else %},{% endif %} {% endfor %} - {{ word_count | reading_time }} minute read. + + {{ word_count | reading_time }} minute read. + {{ content }} diff --git a/site_test/show.html b/site_test/layout/show.html similarity index 100% rename from site_test/show.html rename to site_test/layout/show.html diff --git a/site_test/tag.html b/site_test/layout/tag.html similarity index 100% rename from site_test/tag.html rename to site_test/layout/tag.html diff --git a/site_test/layout/tutorial_post.html b/site_test/layout/tutorial_post.html new file mode 100644 index 0000000..cd80cd3 --- /dev/null +++ b/site_test/layout/tutorial_post.html @@ -0,0 +1,27 @@ +{% extends "default" %} + +{% block titlevariable %} +{% set title = post.metadata.title ~ " | " ~ post.series_name %} +{% endblock %} + +{% block content -%} + + + {{ post.metadata.title }} + + Published on + + {{ post.metadata.date | pretty_date }}, + + in + {{ post.series_name }}. + + {{ post.word_count | reading_time }} minute read. + + + + {{ post.content }} + + + +{%- endblock %} diff --git a/site_test/layout/tutorial_series.html b/site_test/layout/tutorial_series.html new file mode 100644 index 0000000..368b80f --- /dev/null +++ b/site_test/layout/tutorial_series.html @@ -0,0 +1,30 @@ +{% extends "default" %} + +{% block titlevariable %} +{% set title = series_name %} +{% endblock %} + +{% block content -%} + +{{ series_name }} + +{% for entry in entries %} + + + + {{ entry.title }} + + + + Published on + + {{ entry.date | pretty_date }}. + + + {{ entry.word_count | reading_time }} minute read. + + + +{% endfor %} + +{%- endblock %} diff --git a/site_test/tutorials.html b/site_test/tutorials.html new file mode 100644 index 0000000..f61bcbc --- /dev/null +++ b/site_test/tutorials.html @@ -0,0 +1,31 @@ +{% extends "default" %} + +{% block titlevariable %} +{% set title = "Modding Tutorials" %} +{% endblock %} + +{% block content -%} + +Modding Tutorials + +{% for series in entries %} + + + + {{ series.name }} + + + + {{ series.post_count }} + post{% if series.post_count != 1 %}s{% endif %}. + {% if series.last_updated %} + Last updated on + + {{ series.last_updated | pretty_date }}. + + {% endif %} + + +{% endfor %} + +{%- endblock %} diff --git a/src/generator/archive.rs b/src/generator/archive.rs index e48d021..88da2ce 100644 --- a/src/generator/archive.rs +++ b/src/generator/archive.rs @@ -48,7 +48,7 @@ pub fn make_graph( name: "archive", output_path: "/archive/index.html".into(), templates: archive_template, - context, + context: context.into(), }) } diff --git a/src/generator/home.rs b/src/generator/home.rs index 75c029f..0f2f897 100644 --- a/src/generator/home.rs +++ b/src/generator/home.rs @@ -46,7 +46,7 @@ pub fn make_graph( name: "home", output_path: "index.html".into(), templates: home_template, - context, + context: context.into(), }) } diff --git a/src/generator/mod.rs b/src/generator/mod.rs index bf94fc8..bb0fa45 100644 --- a/src/generator/mod.rs +++ b/src/generator/mod.rs @@ -8,6 +8,7 @@ mod rss; mod static_files; mod tags; mod templates; +mod tutorials; mod tv; mod util; @@ -67,6 +68,12 @@ fn make_graph(watcher: Rc>) -> anyhow::Result>) -> anyhow::Result, @@ -17,18 +20,11 @@ pub fn make_graph( builder.add_invalidatable_rule(AddTemplate::new("404", path.clone(), default_template)); watcher.watch(path, move || invalidate.invalidate()); - let void = builder.add_value(()); - let context = builder.add_rule(BuildTemplateContext::new( - "/404.html".into(), - void, - |_, _| {}, - )); - let render = builder.add_rule(RenderTemplate { name: "404", output_path: "404.html".into(), templates, - context, + context: make_template_context(&"/404.html".into()).into(), }); render diff --git a/src/generator/posts.rs b/src/generator/posts.rs index 46fde7e..07342ae 100644 --- a/src/generator/posts.rs +++ b/src/generator/posts.rs @@ -320,7 +320,7 @@ impl DynamicRule for MakeWritePosts { name: "article", output_path: output_path.into(), templates: self.article_template.clone(), - context, + context: context.into(), }) }); } diff --git a/src/generator/tags.rs b/src/generator/tags.rs index dad32aa..f31a360 100644 --- a/src/generator/tags.rs +++ b/src/generator/tags.rs @@ -27,7 +27,7 @@ pub fn make_graph( ) -> Input<()> { let by_tags = builder.add_dynamic_rule(MakePostsByTags::new(posts)); - let template_path = content_path("tag.html"); + let template_path = content_path("layout/tag.html"); let (tag_template, invalidate_template) = builder.add_invalidatable_rule(AddTemplate::new( "tag", template_path.clone(), @@ -209,7 +209,7 @@ impl DynamicRule for MakeWriteTagPages { name: "tag", output_path: format!("/{slug}/index.html").into(), templates: self.templates.clone(), - context: template_context, + context: template_context.into(), }) }); } diff --git a/src/generator/templates.rs b/src/generator/templates.rs index 1c095dc..1870f0f 100644 --- a/src/generator/templates.rs +++ b/src/generator/templates.rs @@ -119,17 +119,8 @@ impl () + 'static> Rule for BuildTemplate type Output = Context; fn evaluate(&mut self) -> Self::Output { - let mut context = Context::new(); + let mut context = make_template_context(&self.permalink); (self.func)(&*self.input.value(), &mut context); - context.insert("_domain", &*DOMAIN); - match &self.permalink { - TemplatePermalink::Constant(s) => context.insert("_permalink", s), - TemplatePermalink::ConstantOwned(s) => context.insert("_permalink", s), - TemplatePermalink::Dynamic(input) => context.insert("_permalink", &*input.value()), - } - context.insert("_stylesheet_cache_buster", &*CB); - context.insert("_development", &cfg!(debug_assertions)); - context.insert("_generated_at", &Local::now()); context } @@ -142,6 +133,20 @@ impl () + 'static> Rule for BuildTemplate } } +pub fn make_template_context(permalink: &TemplatePermalink) -> Context { + let mut context = Context::new(); + context.insert("_domain", &*DOMAIN); + match permalink { + TemplatePermalink::Constant(s) => context.insert("_permalink", s), + TemplatePermalink::ConstantOwned(s) => context.insert("_permalink", s), + TemplatePermalink::Dynamic(input) => context.insert("_permalink", &*input.value()), + } + context.insert("_stylesheet_cache_buster", &*CB); + context.insert("_development", &cfg!(debug_assertions)); + context.insert("_generated_at", &Local::now()); + context +} + #[derive(InputVisitable)] pub enum TemplatePermalink { Constant(#[skip_visit] &'static str), @@ -170,22 +175,28 @@ pub struct RenderTemplate { pub name: &'static str, pub output_path: TemplateOutputPath, pub templates: Input, - pub context: Input, + pub context: RenderTemplateContext, } impl Rule for RenderTemplate { type Output = (); fn evaluate(&mut self) -> Self::Output { let templates = self.templates.value(); - assert!(templates.tera.get_template_names().any(|n| n == self.name)); + let has_template = templates.tera.get_template_names().any(|n| n == self.name); + if !has_template { + error!("Missing template {:?}", self.name); + return; + } let path: &Path = match self.output_path { TemplateOutputPath::Constant(ref p) => p, TemplateOutputPath::Dynamic(ref input) => &input.value(), }; let writer = output_writer(path).expect("output writer"); - let result = templates - .tera - .render_to(&self.name, &*self.context.value(), writer); + let context = match self.context { + RenderTemplateContext::Constant(ref ctx) => ctx, + RenderTemplateContext::Dynamic(ref input) => &input.value(), + }; + let result = templates.tera.render_to(&self.name, context, writer); if let Err(e) = result { error!("Error rendering template to {path:?}: {e:?}"); } @@ -224,6 +235,21 @@ impl From> for TemplateOutputPath { Self::Dynamic(value) } } +#[derive(InputVisitable)] +pub enum RenderTemplateContext { + Constant(#[skip_visit] Context), + Dynamic(Input), +} +impl From for RenderTemplateContext { + fn from(value: Context) -> Self { + Self::Constant(value) + } +} +impl From> for RenderTemplateContext { + fn from(value: Input) -> Self { + Self::Dynamic(value) + } +} pub mod filters { use chrono::{DateTime, Datelike, Local}; diff --git a/src/generator/tutorials.rs b/src/generator/tutorials.rs new file mode 100644 index 0000000..10de2e2 --- /dev/null +++ b/src/generator/tutorials.rs @@ -0,0 +1,232 @@ +use chrono::{DateTime, FixedOffset}; +use compute_graph::input::InputVisitable; +use compute_graph::rule::{DynamicNodeFactory, DynamicRule, DynamicRuleContext}; +use compute_graph::{builder::GraphBuilder, input::Input, synchronicity::Asynchronous}; +use serde::{Deserialize, Serialize}; + +use crate::generator::templates::{AddTemplate, RenderTemplate, make_template_context}; +use crate::generator::util::{Combine, MapDynamicToVoid, word_count}; + +use super::markdown; +use super::util::slugify::slugify; +use super::util::{content_path, from_frontmatter}; +use super::{FileWatcher, templates::Templates}; + +pub fn make_graph( + builder: &mut GraphBuilder<(), Asynchronous>, + default_template: Input, + watcher: &mut FileWatcher, +) -> Input<()> { + let post_path = content_path("layout/tutorial_post.html"); + let (post_template, invalidate) = builder.add_invalidatable_rule(AddTemplate::new( + "tutorial_post", + post_path.clone(), + default_template.clone(), + )); + watcher.watch(post_path, move || invalidate.invalidate()); + + let series_path = content_path("layout/tutorial_series.html"); + let (series_template, invalidate) = builder.add_invalidatable_rule(AddTemplate::new( + "tutorial_series", + series_path.clone(), + default_template.clone(), + )); + watcher.watch(series_path, move || invalidate.invalidate()); + + let index_path = content_path("tutorials.html"); + let (index_template, invalidate) = builder.add_invalidatable_rule(AddTemplate::new( + "tutorials", + index_path.clone(), + default_template, + )); + watcher.watch(index_path, move || invalidate.invalidate()); + + let serieses = vec![ + read_series("forge-modding-1102", "Forge Mods for 1.10.2"), + read_series("forge-modding-1112", "Forge Mods for 1.11.2"), + read_series("forge-modding-112", "Forge Mods for 1.12"), + ]; + + let mut index_entries = vec![]; + let mut render_inputs = vec![]; + + for series in serieses { + index_entries.push(TutorialIndexEntry { + name: series.name, + slug: series.slug, + post_count: series.posts.len(), + last_updated: series.posts.iter().map(|p| p.metadata.date).max(), + }); + + let mut series_context = + make_template_context(&format!("/tutorials/{}/", series.slug).into()); + let entries = series + .posts + .iter() + .map(|post| TutorialSeriesIndexEntry { + title: post.metadata.title.clone(), + slug: post.slug.clone(), + date: post.metadata.date, + word_count: post.word_count, + }) + .collect::>(); + series_context.insert("entries", &entries); + series_context.insert("series_name", series.name); + series_context.insert("series_slug", series.slug); + render_inputs.push(builder.add_rule(RenderTemplate { + name: "tutorial_series", + output_path: format!("tutorials/{}/index.html", series.slug).into(), + templates: series_template.clone(), + context: series_context.into(), + })); + + let render_dynamic = builder.add_dynamic_rule(MakeRenderTutorials::new( + series.posts, + post_template.clone(), + )); + render_inputs.push(builder.add_rule(MapDynamicToVoid(render_dynamic))) + } + + let mut index_context = make_template_context(&"/tutorials/".into()); + index_context.insert("entries", &index_entries); + render_inputs.push(builder.add_rule(RenderTemplate { + name: "tutorials", + output_path: "tutorials/index.html".into(), + templates: index_template, + context: index_context.into(), + })); + + Combine::make(builder, &render_inputs) +} + +fn read_series(slug: &'static str, name: &'static str) -> TutorialSeries { + let mut path = content_path("tutorials"); + path.push(slug); + let entries = std::fs::read_dir(path).expect("reading tutorial dir"); + let posts = entries + .into_iter() + .map(move |ent| { + let ent = ent.expect("tutorial dir ent"); + assert!( + ent.file_type() + .expect("getting tutorial post file type") + .is_file() + ); + let path = ent.path(); + assert!(path.extension().unwrap().eq_ignore_ascii_case("md")); + let str = std::fs::read_to_string(path).expect("reading tutorial post"); + TutorialPost::new(&str, slug, name) + }) + .collect(); + + TutorialSeries { name, slug, posts } +} + +struct TutorialSeries { + name: &'static str, + slug: &'static str, + posts: Vec, +} + +#[derive(PartialEq, Clone, Serialize)] +struct TutorialPost { + series_slug: &'static str, + series_name: &'static str, + slug: String, + content: String, + word_count: u32, + metadata: TutorialPostMetadata, +} + +impl TutorialPost { + fn new(contents: &str, series_slug: &'static str, series_name: &'static str) -> Self { + let (metadata, rest) = + from_frontmatter::(contents).expect("parsing tutorial metadata"); + let slug = metadata + .slug + .clone() + .unwrap_or_else(|| slugify(&metadata.title)); + + let mut buf = vec![]; + markdown::render(rest, &mut buf); + let html = String::from_utf8(buf).unwrap(); + + let word_count = word_count::markdown(contents); + + TutorialPost { + series_slug, + series_name, + slug, + content: html, + word_count, + metadata, + } + } +} + +#[derive(PartialEq, Clone, Deserialize, Serialize)] +struct TutorialPostMetadata { + title: String, + date: DateTime, + slug: Option, +} + +#[derive(InputVisitable)] +struct MakeRenderTutorials { + #[skip_visit] + posts: Vec, + #[skip_visit] + templates: Input, + #[skip_visit] + evaluated: bool, + render_factory: DynamicNodeFactory, +} +impl MakeRenderTutorials { + fn new(posts: Vec, templates: Input) -> Self { + Self { + posts, + templates, + evaluated: false, + render_factory: DynamicNodeFactory::new(), + } + } +} +impl DynamicRule for MakeRenderTutorials { + type ChildOutput = (); + fn evaluate(&mut self, ctx: &mut impl DynamicRuleContext) -> Vec> { + assert!(!self.evaluated); + self.evaluated = true; + for post in self.posts.drain(..) { + let mut context = make_template_context( + &format!("/tutorials/{}/{}/", post.series_slug, post.slug).into(), + ); + context.insert("post", &post); + self.render_factory.add_node(ctx, post.slug.clone(), |ctx| { + ctx.add_rule(RenderTemplate { + name: "tutorial_post", + output_path: format!("tutorials/{}/{}/index.html", post.series_slug, post.slug) + .into(), + templates: self.templates.clone(), + context: context.into(), + }) + }); + } + self.render_factory.all_nodes(ctx) + } +} + +#[derive(Serialize)] +struct TutorialIndexEntry { + name: &'static str, + slug: &'static str, + post_count: usize, + last_updated: Option>, +} + +#[derive(Serialize)] +struct TutorialSeriesIndexEntry { + title: String, + slug: String, + date: DateTime, + word_count: u32, +} diff --git a/src/generator/tv.rs b/src/generator/tv.rs index 50ef343..6be5814 100644 --- a/src/generator/tv.rs +++ b/src/generator/tv.rs @@ -42,7 +42,7 @@ pub fn make_graph( .borrow_mut() .watch(tv_path, move || invalidate.invalidate()); - let show_path = content_path("show.html"); + let show_path = content_path("layout/show.html"); let (show_template, invalidate) = builder.add_invalidatable_rule(AddTemplate::new( "show", show_path.clone(), @@ -75,7 +75,7 @@ pub fn make_graph( name: "tv", output_path: "tv/index.html".into(), templates: index_template, - context: index_context, + context: index_context.into(), }); builder.add_rule(Combine(void_outputs, render_index)) @@ -309,7 +309,7 @@ impl DynamicRule for MakeRenderShows { name: "show", output_path: format!("/tv/{}/index.html", show.slug).into(), templates: self.templates.clone(), - context, + context: context.into(), }) }); }
+ {{ post.metadata.title }} + + Published on + + {{ post.metadata.date | pretty_date }}, + + in + {{ post.series_name }}. + + {{ post.word_count | reading_time }} minute read. + + + + {{ post.content }} + +