Add modding tutorials
This commit is contained in:
parent
49feefaedc
commit
22cbe75dc2
@ -88,7 +88,16 @@ impl<O: 'static, S: Synchronicity> GraphBuilder<O, S> {
|
|||||||
///
|
///
|
||||||
/// Returns an [`Input`] representing the newly-added node, which can be used to construct rules.
|
/// Returns an [`Input`] representing the newly-added node, which can be used to construct rules.
|
||||||
pub fn add_value<V: NodeValue>(&mut self, value: V) -> Input<V> {
|
pub fn add_value<V: NodeValue>(&mut self, value: V) -> Input<V> {
|
||||||
return self.add_node(ConstNode::new(value));
|
return self.add_node(ConstNode::new(value, None));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a constant node with the given value to the graph.
|
||||||
|
///
|
||||||
|
/// The node's label will be the given string.
|
||||||
|
///
|
||||||
|
/// Returns an [`Input`] representing the newly-added node, which can be used to construct rules.
|
||||||
|
pub fn add_named_value<V: NodeValue>(&mut self, value: V, label: String) -> Input<V> {
|
||||||
|
return self.add_node(ConstNode::new(value, Some(label)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds an invalidatable node with the given value to the graph.
|
/// Adds an invalidatable node with the given value to the graph.
|
||||||
|
@ -151,13 +151,15 @@ impl<T: PartialEq + 'static> NodeValue for T {
|
|||||||
|
|
||||||
pub(crate) struct ConstNode<V, S> {
|
pub(crate) struct ConstNode<V, S> {
|
||||||
value: Rc<RefCell<Option<V>>>,
|
value: Rc<RefCell<Option<V>>>,
|
||||||
|
label: Option<String>,
|
||||||
synchronicity: std::marker::PhantomData<S>,
|
synchronicity: std::marker::PhantomData<S>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V, S> ConstNode<V, S> {
|
impl<V, S> ConstNode<V, S> {
|
||||||
pub(crate) fn new(value: V) -> Self {
|
pub(crate) fn new(value: V, label: Option<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
value: Rc::new(RefCell::new(Some(value))),
|
value: Rc::new(RefCell::new(Some(value))),
|
||||||
|
label,
|
||||||
synchronicity: std::marker::PhantomData,
|
synchronicity: std::marker::PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,9 +185,13 @@ impl<V: NodeValue, S: Synchronicity> Node<V, S> for ConstNode<V, S> {
|
|||||||
|
|
||||||
impl<V, S> std::fmt::Debug for ConstNode<V, S> {
|
impl<V, S> std::fmt::Debug for ConstNode<V, S> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(ref label) = self.label {
|
||||||
|
write!(f, "ConstNode<{}>({})", pretty_type_name::<V>(), label)
|
||||||
|
} else {
|
||||||
write!(f, "ConstNode<{}>", pretty_type_name::<V>())
|
write!(f, "ConstNode<{}>", pretty_type_name::<V>())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct InvalidatableConstNode<V, S> {
|
pub(crate) struct InvalidatableConstNode<V, S> {
|
||||||
value: Rc<RefCell<Option<V>>>,
|
value: Rc<RefCell<Option<V>>>,
|
||||||
|
@ -34,7 +34,9 @@
|
|||||||
<a href="/{{ tag.slug }}/">{{ tag.name }}</a>{% if loop.last %}.{% else %},{% endif %}
|
<a href="/{{ tag.slug }}/">{{ tag.name }}</a>{% if loop.last %}.{% else %},{% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<span title="{{ latest_post.word_count }} word{% if latest_post.word_count != 1 %}s{% endif %}">
|
||||||
{{ latest_post.word_count | reading_time }} minute read.
|
{{ latest_post.word_count | reading_time }} minute read.
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="body-content">
|
<div class="body-content">
|
||||||
{% if latest_post.excerpt %}
|
{% if latest_post.excerpt %}
|
||||||
|
@ -25,8 +25,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}{{ post.metadata.title }}{% endblock %}
|
|
||||||
|
|
||||||
{% block content -%}
|
{% block content -%}
|
||||||
|
|
||||||
<article itemprop="blogPost" itemscope itemtype="https://schema.org/BlogPosting">
|
<article itemprop="blogPost" itemscope itemtype="https://schema.org/BlogPosting">
|
||||||
@ -49,7 +47,9 @@
|
|||||||
<a href="/{{ tag.slug }}/">{{ tag.name }}</a>{% if loop.last %}.{% else %},{% endif %}
|
<a href="/{{ tag.slug }}/">{{ tag.name }}</a>{% if loop.last %}.{% else %},{% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<span title="{{ word_count }} word{% if word_count != 1 %}s{% endif %}">
|
||||||
{{ word_count | reading_time }} minute read.
|
{{ word_count | reading_time }} minute read.
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="body-content" itemprop="articleBody">
|
<div class="body-content" itemprop="articleBody">
|
||||||
{{ content }}
|
{{ content }}
|
||||||
|
27
site_test/layout/tutorial_post.html
Normal file
27
site_test/layout/tutorial_post.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{% extends "default" %}
|
||||||
|
|
||||||
|
{% block titlevariable %}
|
||||||
|
{% set title = post.metadata.title ~ " | " ~ post.series_name %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content -%}
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<h1 class="headline">{{ post.metadata.title }}</h1>
|
||||||
|
<p class="article-meta">
|
||||||
|
Published on
|
||||||
|
<time datetime="{{ post.metadata.date | iso_datetime }}">
|
||||||
|
{{ post.metadata.date | pretty_date }},
|
||||||
|
</time>
|
||||||
|
in
|
||||||
|
<a href="/tutorials/{{ post.series_slug }}/">{{ post.series_name }}</a>.
|
||||||
|
<span title="{{ post.word_count }} word{% if post.word_count != 1 %}s{% endif %}">
|
||||||
|
{{ post.word_count | reading_time }} minute read.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div class="body-content">
|
||||||
|
{{ post.content }}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
{%- endblock %}
|
30
site_test/layout/tutorial_series.html
Normal file
30
site_test/layout/tutorial_series.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{% extends "default" %}
|
||||||
|
|
||||||
|
{% block titlevariable %}
|
||||||
|
{% set title = series_name %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content -%}
|
||||||
|
|
||||||
|
<h1 class="headline">{{ series_name }}</h1>
|
||||||
|
|
||||||
|
{% for entry in entries %}
|
||||||
|
|
||||||
|
<h2 class="headline">
|
||||||
|
<a href="/tutorials/{{ series_slug }}/{{ entry.slug }}/">
|
||||||
|
{{ entry.title }}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
<p class="article-meta">
|
||||||
|
Published on
|
||||||
|
<time datetime="{{ entry.date | iso_datetime }}">
|
||||||
|
{{ entry.date | pretty_date }}.
|
||||||
|
</time>
|
||||||
|
<span title="{{ entry.word_count }} word{% if entry.word_count != 1 %}s{% endif %}">
|
||||||
|
{{ entry.word_count | reading_time }} minute read.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{%- endblock %}
|
31
site_test/tutorials.html
Normal file
31
site_test/tutorials.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{% extends "default" %}
|
||||||
|
|
||||||
|
{% block titlevariable %}
|
||||||
|
{% set title = "Modding Tutorials" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content -%}
|
||||||
|
|
||||||
|
<h1 class="headline">Modding Tutorials</h1>
|
||||||
|
|
||||||
|
{% for series in entries %}
|
||||||
|
|
||||||
|
<h2 class="headline">
|
||||||
|
<a href="/tutorials/{{ series.slug }}/">
|
||||||
|
{{ series.name }}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
<p class="article-meta">
|
||||||
|
{{ series.post_count }}
|
||||||
|
post{% if series.post_count != 1 %}s{% endif %}.
|
||||||
|
{% if series.last_updated %}
|
||||||
|
Last updated on
|
||||||
|
<time datetime="{{ series.last_updated | iso_datetime }}">
|
||||||
|
{{ series.last_updated | pretty_date }}.
|
||||||
|
</time>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{%- endblock %}
|
@ -48,7 +48,7 @@ pub fn make_graph(
|
|||||||
name: "archive",
|
name: "archive",
|
||||||
output_path: "/archive/index.html".into(),
|
output_path: "/archive/index.html".into(),
|
||||||
templates: archive_template,
|
templates: archive_template,
|
||||||
context,
|
context: context.into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ pub fn make_graph(
|
|||||||
name: "home",
|
name: "home",
|
||||||
output_path: "index.html".into(),
|
output_path: "index.html".into(),
|
||||||
templates: home_template,
|
templates: home_template,
|
||||||
context,
|
context: context.into(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ mod rss;
|
|||||||
mod static_files;
|
mod static_files;
|
||||||
mod tags;
|
mod tags;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
mod tutorials;
|
||||||
mod tv;
|
mod tv;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
@ -67,6 +68,12 @@ fn make_graph(watcher: Rc<RefCell<FileWatcher>>) -> anyhow::Result<AsyncGraph<()
|
|||||||
|
|
||||||
let tv = tv::make_graph(&mut builder, default_template.clone(), Rc::clone(&watcher));
|
let tv = tv::make_graph(&mut builder, default_template.clone(), Rc::clone(&watcher));
|
||||||
|
|
||||||
|
let tutorials = tutorials::make_graph(
|
||||||
|
&mut builder,
|
||||||
|
default_template.clone(),
|
||||||
|
&mut *watcher.borrow_mut(),
|
||||||
|
);
|
||||||
|
|
||||||
let not_found = not_found::make_graph(
|
let not_found = not_found::make_graph(
|
||||||
&mut builder,
|
&mut builder,
|
||||||
default_template.clone(),
|
default_template.clone(),
|
||||||
@ -82,6 +89,7 @@ fn make_graph(watcher: Rc<RefCell<FileWatcher>>) -> anyhow::Result<AsyncGraph<()
|
|||||||
statics,
|
statics,
|
||||||
rss,
|
rss,
|
||||||
tv,
|
tv,
|
||||||
|
tutorials,
|
||||||
not_found,
|
not_found,
|
||||||
]);
|
]);
|
||||||
builder.set_existing_output(output);
|
builder.set_existing_output(output);
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
use compute_graph::{builder::GraphBuilder, input::Input, synchronicity::Asynchronous};
|
use compute_graph::{builder::GraphBuilder, input::Input, synchronicity::Asynchronous};
|
||||||
|
|
||||||
use crate::generator::{
|
use crate::generator::{
|
||||||
templates::{AddTemplate, BuildTemplateContext, RenderTemplate},
|
templates::{AddTemplate, RenderTemplate},
|
||||||
util::content_path,
|
util::content_path,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{FileWatcher, templates::Templates};
|
use super::{
|
||||||
|
FileWatcher,
|
||||||
|
templates::{Templates, make_template_context},
|
||||||
|
};
|
||||||
|
|
||||||
pub fn make_graph(
|
pub fn make_graph(
|
||||||
builder: &mut GraphBuilder<(), Asynchronous>,
|
builder: &mut GraphBuilder<(), Asynchronous>,
|
||||||
@ -17,18 +20,11 @@ pub fn make_graph(
|
|||||||
builder.add_invalidatable_rule(AddTemplate::new("404", path.clone(), default_template));
|
builder.add_invalidatable_rule(AddTemplate::new("404", path.clone(), default_template));
|
||||||
watcher.watch(path, move || invalidate.invalidate());
|
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 {
|
let render = builder.add_rule(RenderTemplate {
|
||||||
name: "404",
|
name: "404",
|
||||||
output_path: "404.html".into(),
|
output_path: "404.html".into(),
|
||||||
templates,
|
templates,
|
||||||
context,
|
context: make_template_context(&"/404.html".into()).into(),
|
||||||
});
|
});
|
||||||
|
|
||||||
render
|
render
|
||||||
|
@ -320,7 +320,7 @@ impl DynamicRule for MakeWritePosts {
|
|||||||
name: "article",
|
name: "article",
|
||||||
output_path: output_path.into(),
|
output_path: output_path.into(),
|
||||||
templates: self.article_template.clone(),
|
templates: self.article_template.clone(),
|
||||||
context,
|
context: context.into(),
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ pub fn make_graph(
|
|||||||
) -> Input<()> {
|
) -> Input<()> {
|
||||||
let by_tags = builder.add_dynamic_rule(MakePostsByTags::new(posts));
|
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(
|
let (tag_template, invalidate_template) = builder.add_invalidatable_rule(AddTemplate::new(
|
||||||
"tag",
|
"tag",
|
||||||
template_path.clone(),
|
template_path.clone(),
|
||||||
@ -209,7 +209,7 @@ impl DynamicRule for MakeWriteTagPages {
|
|||||||
name: "tag",
|
name: "tag",
|
||||||
output_path: format!("/{slug}/index.html").into(),
|
output_path: format!("/{slug}/index.html").into(),
|
||||||
templates: self.templates.clone(),
|
templates: self.templates.clone(),
|
||||||
context: template_context,
|
context: template_context.into(),
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -119,17 +119,8 @@ impl<T: 'static, F: Fn(&T, &mut Context) -> () + 'static> Rule for BuildTemplate
|
|||||||
type Output = Context;
|
type Output = Context;
|
||||||
|
|
||||||
fn evaluate(&mut self) -> Self::Output {
|
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);
|
(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
|
context
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,6 +133,20 @@ impl<T: 'static, F: Fn(&T, &mut Context) -> () + '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)]
|
#[derive(InputVisitable)]
|
||||||
pub enum TemplatePermalink {
|
pub enum TemplatePermalink {
|
||||||
Constant(#[skip_visit] &'static str),
|
Constant(#[skip_visit] &'static str),
|
||||||
@ -170,22 +175,28 @@ pub struct RenderTemplate {
|
|||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub output_path: TemplateOutputPath,
|
pub output_path: TemplateOutputPath,
|
||||||
pub templates: Input<Templates>,
|
pub templates: Input<Templates>,
|
||||||
pub context: Input<Context>,
|
pub context: RenderTemplateContext,
|
||||||
}
|
}
|
||||||
impl Rule for RenderTemplate {
|
impl Rule for RenderTemplate {
|
||||||
type Output = ();
|
type Output = ();
|
||||||
|
|
||||||
fn evaluate(&mut self) -> Self::Output {
|
fn evaluate(&mut self) -> Self::Output {
|
||||||
let templates = self.templates.value();
|
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 {
|
let path: &Path = match self.output_path {
|
||||||
TemplateOutputPath::Constant(ref p) => p,
|
TemplateOutputPath::Constant(ref p) => p,
|
||||||
TemplateOutputPath::Dynamic(ref input) => &input.value(),
|
TemplateOutputPath::Dynamic(ref input) => &input.value(),
|
||||||
};
|
};
|
||||||
let writer = output_writer(path).expect("output writer");
|
let writer = output_writer(path).expect("output writer");
|
||||||
let result = templates
|
let context = match self.context {
|
||||||
.tera
|
RenderTemplateContext::Constant(ref ctx) => ctx,
|
||||||
.render_to(&self.name, &*self.context.value(), writer);
|
RenderTemplateContext::Dynamic(ref input) => &input.value(),
|
||||||
|
};
|
||||||
|
let result = templates.tera.render_to(&self.name, context, writer);
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
error!("Error rendering template to {path:?}: {e:?}");
|
error!("Error rendering template to {path:?}: {e:?}");
|
||||||
}
|
}
|
||||||
@ -224,6 +235,21 @@ impl From<Input<PathBuf>> for TemplateOutputPath {
|
|||||||
Self::Dynamic(value)
|
Self::Dynamic(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
pub enum RenderTemplateContext {
|
||||||
|
Constant(#[skip_visit] Context),
|
||||||
|
Dynamic(Input<Context>),
|
||||||
|
}
|
||||||
|
impl From<Context> for RenderTemplateContext {
|
||||||
|
fn from(value: Context) -> Self {
|
||||||
|
Self::Constant(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Input<Context>> for RenderTemplateContext {
|
||||||
|
fn from(value: Input<Context>) -> Self {
|
||||||
|
Self::Dynamic(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub mod filters {
|
pub mod filters {
|
||||||
use chrono::{DateTime, Datelike, Local};
|
use chrono::{DateTime, Datelike, Local};
|
||||||
|
232
src/generator/tutorials.rs
Normal file
232
src/generator/tutorials.rs
Normal file
@ -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<Templates>,
|
||||||
|
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::<Vec<_>>();
|
||||||
|
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<TutorialPost>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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::<TutorialPostMetadata>(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<FixedOffset>,
|
||||||
|
slug: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(InputVisitable)]
|
||||||
|
struct MakeRenderTutorials {
|
||||||
|
#[skip_visit]
|
||||||
|
posts: Vec<TutorialPost>,
|
||||||
|
#[skip_visit]
|
||||||
|
templates: Input<Templates>,
|
||||||
|
#[skip_visit]
|
||||||
|
evaluated: bool,
|
||||||
|
render_factory: DynamicNodeFactory<String, ()>,
|
||||||
|
}
|
||||||
|
impl MakeRenderTutorials {
|
||||||
|
fn new(posts: Vec<TutorialPost>, templates: Input<Templates>) -> 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<Input<Self::ChildOutput>> {
|
||||||
|
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<DateTime<FixedOffset>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct TutorialSeriesIndexEntry {
|
||||||
|
title: String,
|
||||||
|
slug: String,
|
||||||
|
date: DateTime<FixedOffset>,
|
||||||
|
word_count: u32,
|
||||||
|
}
|
@ -42,7 +42,7 @@ pub fn make_graph(
|
|||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.watch(tv_path, move || invalidate.invalidate());
|
.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(
|
let (show_template, invalidate) = builder.add_invalidatable_rule(AddTemplate::new(
|
||||||
"show",
|
"show",
|
||||||
show_path.clone(),
|
show_path.clone(),
|
||||||
@ -75,7 +75,7 @@ pub fn make_graph(
|
|||||||
name: "tv",
|
name: "tv",
|
||||||
output_path: "tv/index.html".into(),
|
output_path: "tv/index.html".into(),
|
||||||
templates: index_template,
|
templates: index_template,
|
||||||
context: index_context,
|
context: index_context.into(),
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.add_rule(Combine(void_outputs, render_index))
|
builder.add_rule(Combine(void_outputs, render_index))
|
||||||
@ -309,7 +309,7 @@ impl DynamicRule for MakeRenderShows {
|
|||||||
name: "show",
|
name: "show",
|
||||||
output_path: format!("/tv/{}/index.html", show.slug).into(),
|
output_path: format!("/tv/{}/index.html", show.slug).into(),
|
||||||
templates: self.templates.clone(),
|
templates: self.templates.clone(),
|
||||||
context,
|
context: context.into(),
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user