mirror of
https://github.com/zyllian/zyllian.github.io.git
synced 2025-01-17 19:22:30 -08:00
make resources configurable by the user without modifying code
This commit is contained in:
parent
50574bf352
commit
159c8fa5b3
21 changed files with 181 additions and 337 deletions
|
@ -2,7 +2,32 @@ base_url: "https://zyl.gay"
|
|||
title: zyl is gay
|
||||
description: "zyl's website."
|
||||
sass_styles: [index.scss, pet.scss, click.scss]
|
||||
images_per_page: 10
|
||||
blog_posts_per_page: 20
|
||||
cdn_url: "https://i.zyl.gay"
|
||||
s3_prefix: # no longer prefixed
|
||||
|
||||
resources:
|
||||
blog:
|
||||
source_path: blog
|
||||
output_path_short: blog
|
||||
output_path_long: blog
|
||||
resource_template: blog-post
|
||||
resource_list_template: blog-list
|
||||
rss_template: rss/blog-post
|
||||
rss_title: zyl's blog
|
||||
rss_description: feed of recent blog posts on zyl's website.
|
||||
list_title: blog
|
||||
tag_list_title: blog tags
|
||||
resource_name_plural: blog posts
|
||||
resources_per_page: 20
|
||||
images:
|
||||
source_path: images
|
||||
output_path_short: i
|
||||
output_path_long: images
|
||||
resource_template: image
|
||||
resource_list_template: images
|
||||
rss_template: rss/image
|
||||
rss_title: zyl's images
|
||||
rss_description: feed of newly uploaded images from zyl's website.
|
||||
list_title: images
|
||||
tag_list_title: image tags
|
||||
resource_name_plural: images
|
||||
resources_per_page: 10
|
||||
|
|
|
@ -3,7 +3,7 @@ title: being gay is so cool
|
|||
timestamp: 2023-11-14T23:09:00.00Z
|
||||
tags: [gay, lesbiab, trans]
|
||||
desc: holy shit being gay is cool y'all
|
||||
header_image_file: 2023/11/pfp.png
|
||||
cdn_file: 2023/11/pfp.png
|
||||
header_image_alt: A dead Among Us bean layered on top of transgender, lesbian, and bisexual pride flags.
|
||||
---
|
||||
|
|
@ -3,7 +3,7 @@ title: estradiol delivery methods
|
|||
timestamp: 2023-11-12T20:55:00.00Z
|
||||
tags: [trans, hrt, estradiol]
|
||||
desc: a comparison of the four forms of estradiol i've taken since starting hrt
|
||||
header_image_file: 2023/11/syringe.jpeg
|
||||
cdn_file: 2023/11/syringe.jpeg
|
||||
header_image_alt: A photograph of a syringe for instramuscular injection with needle still in its wrapper.
|
||||
---
|
||||
|
|
@ -3,7 +3,7 @@ title: i built a minecraft classic server
|
|||
timestamp: 2024-07-08T10:37:00.00Z
|
||||
tags: [minecraft, minecraft classic, dev]
|
||||
desc: i got bored and built a minecraft classic server
|
||||
header_image_file: 2024/07/classic2.png
|
||||
cdn_file: 2024/07/classic2.png
|
||||
header_image_alt: Screenshot of my Minecraft classic server showing two players standing in front of a stack of blocks.
|
||||
---
|
||||
|
|
@ -3,7 +3,7 @@ title: send trans people money.
|
|||
timestamp: 2024-06-21T14:05:00.00Z
|
||||
tags: [trans, money]
|
||||
desc: you should send trans people money. i'm trans btw
|
||||
header_image_file: 2024/06/gamecube.jpeg
|
||||
cdn_file: 2024/06/gamecube.jpeg
|
||||
header_image_alt: A photo of me holding a Cipon-branded Gamecube controller in my lap.
|
||||
---
|
||||
|
|
@ -3,7 +3,7 @@ title: so now i have a blog
|
|||
timestamp: 2023-06-09T22:20:00.00Z
|
||||
tags: [meta, blog]
|
||||
desc: I added a blog to my site. Here's a picture of some smoke.
|
||||
header_image_file: 2023/06/smoke.jpeg
|
||||
cdn_file: 2023/06/smoke.jpeg
|
||||
header_image_alt: Photo of smoke engulfing an interstate from a highway across a river.
|
||||
---
|
||||
|
|
@ -3,7 +3,7 @@ title: among us 😱
|
|||
timestamp: 2022-12-14T00:00:00.00Z
|
||||
alt: Screenshot of Pokémon White on an evolution screen. Text reads "Congratulations! Your amogus evolved into Amoonguss!"
|
||||
desc: aaahhhh
|
||||
file: amogus.png
|
||||
cdn_file: amogus.png
|
||||
tags: [pokémon, sussy]
|
||||
---
|
||||
|
|
@ -2,6 +2,6 @@
|
|||
title: cat 3
|
||||
timestamp: 2022-12-14T00:00:00.00Z
|
||||
alt: Picture of my cat sleeping in a box barely large enough for them. Their head is resting on one edge of the box.
|
||||
file: boxtop.jpeg
|
||||
cdn_file: boxtop.jpeg
|
||||
tags: [cat]
|
||||
---
|
|
@ -2,6 +2,6 @@
|
|||
title: cat
|
||||
timestamp: 2022-12-14T00:00:00.00Z
|
||||
alt: Picture of my cat sleeping curled up on top of some pillows.
|
||||
file: cat.jpeg
|
||||
cdn_file: cat.jpeg
|
||||
tags: [cat]
|
||||
---
|
|
@ -2,6 +2,6 @@
|
|||
title: cat 2
|
||||
timestamp: 2022-12-14T00:00:00.00Z
|
||||
alt: Close up picture of my cat laying on a shelf while staring not quite at the camera.
|
||||
file: cat2.jpeg
|
||||
cdn_file: cat2.jpeg
|
||||
tags: [cat]
|
||||
---
|
|
@ -2,7 +2,7 @@
|
|||
title: shorts to dresses
|
||||
timestamp: 2022-12-14T00:00:00.00Z
|
||||
alt: Screenshot from Pokémon Black 2 of an NPC saying "This dress is comfy and easy to wear..."
|
||||
file: trans-comfy.png
|
||||
cdn_file: trans-comfy.png
|
||||
tags: [pokémon, trans]
|
||||
---
|
||||
|
|
@ -6,8 +6,8 @@
|
|||
{{/if}}
|
||||
<div class="header-image-wrapper">
|
||||
<p class="short-desc">{{desc}}</p>
|
||||
<img class="header-image" src="{{header_image}}" alt="{{header_image_alt}}"
|
||||
style="object-fit: {{object_fit}}; object-position: {{object_position}};">
|
||||
<img class="header-image" src="{{cdn_file}}" alt="{{header_image_alt}}"
|
||||
style="object-fit: cover; object-position: 50% 50%">
|
||||
</div>
|
||||
<div class="content">
|
||||
{{{content}}}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<div class="image-full">
|
||||
<h1 class="title">{{title}}</h1>
|
||||
<span class="timestamp">published {{timestamp}}</span>
|
||||
<img class="image-actual" src="{{src}}" alt="{{alt}}">
|
||||
<img class="image-actual" src="{{cdn_file}}" alt="{{alt}}">
|
||||
{{{content}}}
|
||||
<p><a href="{{src}}">view full size image</a></p>
|
||||
<p><a href="{{cdn_file}}">view full size image</a></p>
|
||||
<h3 class="tags-title">tags</h3>
|
||||
<div class="image-tags">
|
||||
{{#each tags}}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<div class="images-list">
|
||||
{{#each resources}}
|
||||
<a class="image" href="/i/{{id}}">
|
||||
<img class="image-actual" src="{{src}}" alt="{{alt}}">
|
||||
<img class="image-actual" src="{{cdn_file}}" alt="{{alt}}">
|
||||
<span class="title">{{title}}</span>
|
||||
</a>
|
||||
{{/each}}
|
||||
|
|
98
src/blog.rs
98
src/blog.rs
|
@ -1,98 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
resource::{EmbedMetadata, ResourceBuilderConfig, ResourceMetadata, ResourceMethods},
|
||||
Site, SiteConfig,
|
||||
};
|
||||
|
||||
pub const BLOG_PATH: &str = "blog";
|
||||
|
||||
/// Gets the blog's resource configuration.
|
||||
pub fn get_blog_resource_config(site: &Site) -> ResourceBuilderConfig {
|
||||
ResourceBuilderConfig {
|
||||
source_path: BLOG_PATH.to_string(),
|
||||
output_path_short: BLOG_PATH.to_string(),
|
||||
output_path_long: BLOG_PATH.to_string(),
|
||||
resource_template: "blog-post".to_string(),
|
||||
resource_list_template: "blog-list".to_string(),
|
||||
rss_template: "rss/blog-post".to_string(),
|
||||
rss_title: "zyl's blog".to_string(),
|
||||
rss_description: "Feed of recent blog posts on zyl's website.".to_string(),
|
||||
list_title: "blog".to_string(),
|
||||
tag_list_title: "blog tags".to_string(),
|
||||
resource_name_plural: "blog posts".to_string(),
|
||||
resources_per_page: site.config.blog_posts_per_page,
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata for a blog post.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BlogPostMetadata {
|
||||
/// A short description about the post.
|
||||
pub desc: String,
|
||||
/// Path to the post's header image.
|
||||
pub header_image_file: String,
|
||||
/// Alt text for the post's header image.
|
||||
pub header_image_alt: String,
|
||||
/// Optional custom object fit value.
|
||||
pub image_fit: Option<String>,
|
||||
/// Optional custom object position value.
|
||||
pub image_center: Option<String>,
|
||||
}
|
||||
|
||||
impl BlogPostMetadata {
|
||||
/// Helper to get the CDN URL to the blog post's header image.
|
||||
fn get_header_image(&self, site_config: &SiteConfig) -> eyre::Result<String> {
|
||||
Ok(site_config.cdn_url(&self.header_image_file)?.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Template data for a blog post.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BlogPostTemplateData {
|
||||
/// CDN path to the post's header image.
|
||||
pub header_image: String,
|
||||
/// Custom object fit value.
|
||||
pub object_fit: String,
|
||||
/// Custom object position value.
|
||||
pub object_position: String,
|
||||
}
|
||||
|
||||
impl ResourceMethods<BlogPostTemplateData> for ResourceMetadata<BlogPostMetadata> {
|
||||
fn get_short_desc(&self) -> String {
|
||||
self.inner.desc.clone()
|
||||
}
|
||||
|
||||
fn get_extra_resource_template_data(
|
||||
&self,
|
||||
site_config: &SiteConfig,
|
||||
) -> eyre::Result<BlogPostTemplateData> {
|
||||
// TODO: render markdown
|
||||
Ok(BlogPostTemplateData {
|
||||
header_image: self.inner.get_header_image(site_config)?,
|
||||
object_fit: self
|
||||
.inner
|
||||
.image_fit
|
||||
.clone()
|
||||
.unwrap_or_else(|| "cover".to_string()),
|
||||
object_position: self
|
||||
.inner
|
||||
.image_center
|
||||
.clone()
|
||||
.unwrap_or_else(|| "50% 50%".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_head_data(&self, site_config: &SiteConfig) -> eyre::Result<String> {
|
||||
Ok(EmbedMetadata {
|
||||
title: self.title.clone(),
|
||||
site_name: site_config.title.clone(),
|
||||
description: Some(self.inner.desc.clone()),
|
||||
image: Some(self.inner.get_header_image(site_config)?),
|
||||
url: None,
|
||||
theme_color: EmbedMetadata::default_theme_color(),
|
||||
large_image: true,
|
||||
}
|
||||
.build())
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
//! Module containing the site builder.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use eyre::{eyre, Context, OptionExt};
|
||||
use gray_matter::{engine::YAML, Matter};
|
||||
|
@ -43,12 +43,8 @@ pub struct SiteBuilder<'a> {
|
|||
/// Whether the site is going to be served locally with the dev server.
|
||||
serving: bool,
|
||||
|
||||
/// Resource builder for the site's images section.
|
||||
pub images_builder:
|
||||
ResourceBuilder<crate::images::ImageMetadata, crate::images::ImageTemplateData>,
|
||||
/// Resource builder for the site's blog section.
|
||||
pub blog_builder:
|
||||
ResourceBuilder<crate::blog::BlogPostMetadata, crate::blog::BlogPostTemplateData>,
|
||||
/// The resource builders available to the builder.
|
||||
pub resource_builders: HashMap<String, ResourceBuilder>,
|
||||
}
|
||||
|
||||
impl<'a> SiteBuilder<'a> {
|
||||
|
@ -65,8 +61,7 @@ impl<'a> SiteBuilder<'a> {
|
|||
Self {
|
||||
matter: Matter::new(),
|
||||
reg: Handlebars::new(),
|
||||
images_builder: ResourceBuilder::new(crate::images::get_images_resource_config(&site)),
|
||||
blog_builder: ResourceBuilder::new(crate::blog::get_blog_resource_config(&site)),
|
||||
resource_builders: HashMap::new(),
|
||||
site,
|
||||
build_path,
|
||||
serving,
|
||||
|
@ -112,34 +107,35 @@ impl<'a> SiteBuilder<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
let images_path = self.build_path.join(crate::images::IMAGES_OUT_PATH);
|
||||
if !images_path.exists() {
|
||||
std::fs::create_dir(images_path).wrap_err("Failed to create images path")?;
|
||||
}
|
||||
|
||||
self.reload_images_builder()?;
|
||||
self.reload_blog_builder()?;
|
||||
self.reload()?;
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Reloads the images builder's metadata.
|
||||
pub fn reload_images_builder(&mut self) -> eyre::Result<()> {
|
||||
let mut images_builder = std::mem::take(&mut self.images_builder);
|
||||
images_builder
|
||||
.load_all(self)
|
||||
.wrap_err("Failed to load images metadata")?;
|
||||
self.images_builder = images_builder;
|
||||
/// Performs actions that need to be done when the config changes while serving.
|
||||
pub fn reload(&mut self) -> eyre::Result<()> {
|
||||
self.resource_builders.clear();
|
||||
for (prefix, config) in &self.site.config.resources {
|
||||
self.resource_builders
|
||||
.insert(prefix.to_owned(), ResourceBuilder::new(config.clone()));
|
||||
}
|
||||
|
||||
for prefix in self.resource_builders.keys().cloned().collect::<Vec<_>>() {
|
||||
self.reload_resource_builder(&prefix)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reloads the blog builder's metadata.
|
||||
pub fn reload_blog_builder(&mut self) -> eyre::Result<()> {
|
||||
let mut blog_builder = std::mem::take(&mut self.blog_builder);
|
||||
blog_builder
|
||||
.load_all(self)
|
||||
.wrap_err("Failed to load blog metadata")?;
|
||||
self.blog_builder = blog_builder;
|
||||
/// Reloads a particular resource builder's metadata.
|
||||
pub fn reload_resource_builder(&mut self, builder: &str) -> eyre::Result<()> {
|
||||
let mut resource_builder = self
|
||||
.resource_builders
|
||||
.remove(builder)
|
||||
.ok_or_else(|| eyre!("missing resource builder: {builder}"))?;
|
||||
resource_builder.load_all(self)?;
|
||||
self.resource_builders
|
||||
.insert(builder.to_string(), resource_builder);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -384,13 +380,19 @@ impl<'a> SiteBuilder<'a> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds the site's various image pages.
|
||||
pub fn build_images(&self) -> eyre::Result<()> {
|
||||
self.images_builder.build_all(self)
|
||||
/// Builds all resource types.
|
||||
pub fn build_all_resources(&self) -> eyre::Result<()> {
|
||||
for builder in self.resource_builders.values() {
|
||||
builder.build_all(self)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds the site's blog.
|
||||
pub fn build_blog(&self) -> eyre::Result<()> {
|
||||
self.blog_builder.build_all(self)
|
||||
/// Builds a resource type from the site.
|
||||
pub fn build_resources(&self, resource: &str) -> eyre::Result<()> {
|
||||
self.resource_builders
|
||||
.get(resource)
|
||||
.ok_or_else(|| eyre!("missing resource: {resource}"))?
|
||||
.build_all(self)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use lol_html::{element, RewriteStrSettings};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{blog::BlogPostMetadata, builder::SiteBuilder, resource::ResourceTemplateData};
|
||||
use crate::{builder::SiteBuilder, resource::ResourceTemplateData};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Extra {
|
||||
|
@ -31,6 +31,7 @@ pub fn get_extra(extra: &str) -> Option<Extra> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Extra to append a tempalte to the page.
|
||||
fn append_to(page: &str, content: &str, selector: &str) -> eyre::Result<String> {
|
||||
Ok(lol_html::rewrite_str(
|
||||
page,
|
||||
|
@ -48,22 +49,22 @@ fn append_to(page: &str, content: &str, selector: &str) -> eyre::Result<String>
|
|||
fn index(page: String, builder: &SiteBuilder) -> eyre::Result<String> {
|
||||
#[derive(Debug, Serialize)]
|
||||
struct SidebarTemplateData<'r> {
|
||||
// resources: Vec<&'r ResourceMetadata<BlogPostMetadata>>,
|
||||
resources: Vec<ResourceTemplateData<'r, BlogPostMetadata, ()>>,
|
||||
resources: Vec<ResourceTemplateData<'r>>,
|
||||
}
|
||||
|
||||
let sidebar = builder.reg.render(
|
||||
"extras/index-injection",
|
||||
&SidebarTemplateData {
|
||||
resources: builder
|
||||
.blog_builder
|
||||
.resource_builders
|
||||
.get("blog")
|
||||
.expect("missing blog builder")
|
||||
.loaded_metadata
|
||||
.iter()
|
||||
.take(3)
|
||||
.map(|(id, v)| ResourceTemplateData {
|
||||
resource: v,
|
||||
id: id.clone(),
|
||||
extra: (),
|
||||
timestamp: v.timestamp,
|
||||
})
|
||||
.collect(),
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
resource::{EmbedMetadata, ResourceBuilderConfig, ResourceMetadata, ResourceMethods},
|
||||
Site, SiteConfig,
|
||||
};
|
||||
|
||||
pub(crate) const IMAGES_PATH: &str = "images";
|
||||
pub(crate) const IMAGES_OUT_PATH: &str = "i";
|
||||
|
||||
/// Gets the resource configuration for images.
|
||||
pub fn get_images_resource_config(site: &Site) -> ResourceBuilderConfig {
|
||||
ResourceBuilderConfig {
|
||||
source_path: IMAGES_PATH.to_string(),
|
||||
output_path_short: IMAGES_OUT_PATH.to_string(),
|
||||
output_path_long: "images".to_string(),
|
||||
resource_template: "image".to_string(),
|
||||
resource_list_template: "images".to_string(),
|
||||
rss_template: "rss/image".to_string(),
|
||||
rss_title: "zyl's images".to_string(),
|
||||
rss_description: "feed of newly uploaded images from zyl's website.".to_string(),
|
||||
list_title: "images".to_string(),
|
||||
tag_list_title: "image tags".to_string(),
|
||||
resource_name_plural: "images".to_string(),
|
||||
resources_per_page: site.config.images_per_page,
|
||||
}
|
||||
}
|
||||
|
||||
/// Definition for a remote image.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ImageMetadata {
|
||||
/// The image's alt text.
|
||||
pub alt: String,
|
||||
/// The image's extra description, if any.
|
||||
pub desc: Option<String>,
|
||||
/// The image's file path.
|
||||
pub file: String,
|
||||
}
|
||||
|
||||
impl ImageMetadata {
|
||||
fn get_image_url(&self, site_config: &SiteConfig) -> eyre::Result<String> {
|
||||
Ok(site_config.cdn_url(&self.file)?.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Template data for a specific image.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ImageTemplateData {
|
||||
/// Direct URL to the image's CDN location.
|
||||
/// TODO: link to smaller versions on list pages
|
||||
src: String,
|
||||
}
|
||||
|
||||
impl ResourceMethods<ImageTemplateData> for ResourceMetadata<ImageMetadata> {
|
||||
fn get_short_desc(&self) -> String {
|
||||
self.inner.desc.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_extra_resource_template_data(
|
||||
&self,
|
||||
site_config: &SiteConfig,
|
||||
) -> eyre::Result<ImageTemplateData> {
|
||||
Ok(ImageTemplateData {
|
||||
src: self.inner.get_image_url(site_config)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_head_data(&self, site_config: &SiteConfig) -> eyre::Result<String> {
|
||||
Ok(EmbedMetadata {
|
||||
title: self.title.clone(),
|
||||
site_name: site_config.title.clone(),
|
||||
description: self.inner.desc.clone(),
|
||||
image: Some(self.inner.get_image_url(site_config)?),
|
||||
url: None,
|
||||
theme_color: EmbedMetadata::default_theme_color(),
|
||||
large_image: true,
|
||||
}
|
||||
.build())
|
||||
}
|
||||
}
|
22
src/lib.rs
22
src/lib.rs
|
@ -1,7 +1,5 @@
|
|||
mod blog;
|
||||
mod builder;
|
||||
mod extras;
|
||||
mod images;
|
||||
mod link_list;
|
||||
mod resource;
|
||||
#[cfg(feature = "serve")]
|
||||
|
@ -15,7 +13,7 @@ use std::{
|
|||
|
||||
use eyre::Context;
|
||||
use rayon::prelude::*;
|
||||
use resource::EmbedMetadata;
|
||||
use resource::{EmbedMetadata, ResourceBuilderConfig};
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
use util::get_name;
|
||||
|
@ -41,20 +39,16 @@ pub struct SiteConfig {
|
|||
pub build: Option<String>,
|
||||
/// A list of Sass stylesheets that will be built.
|
||||
pub sass_styles: Vec<PathBuf>,
|
||||
/// The number of images to display on a single page of an image list.
|
||||
pub images_per_page: usize,
|
||||
/// The number of blog posts to display on a single page of a post list.
|
||||
pub blog_posts_per_page: usize,
|
||||
/// URL to the CDN used for the site's images.
|
||||
pub cdn_url: Url,
|
||||
/// Prefix applied to all files uploaded to the site's S3 space.
|
||||
pub s3_prefix: String,
|
||||
/// List of resources the site should build.
|
||||
pub resources: HashMap<String, ResourceBuilderConfig>,
|
||||
}
|
||||
|
||||
impl SiteConfig {
|
||||
/// Gets a CDN url from the given file name.
|
||||
pub fn cdn_url(&self, file: &str) -> eyre::Result<Url> {
|
||||
Ok(self.cdn_url.join(&self.s3_prefix)?.join(file)?)
|
||||
Ok(self.cdn_url.join(file)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,8 +149,12 @@ impl Site {
|
|||
|
||||
builder.site.build_all_pages(&builder)?;
|
||||
builder.build_sass()?;
|
||||
builder.build_images()?;
|
||||
builder.build_blog()?;
|
||||
|
||||
for (_source_path, config) in builder.site.config.resources.iter() {
|
||||
let mut res_builder = resource::ResourceBuilder::new(config.clone());
|
||||
res_builder.load_all(&builder)?;
|
||||
res_builder.build_all(&builder)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
114
src/resource.rs
114
src/resource.rs
|
@ -1,6 +1,5 @@
|
|||
use std::{
|
||||
collections::BTreeMap,
|
||||
marker::PhantomData,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
|
@ -8,14 +7,17 @@ use eyre::Context;
|
|||
use itertools::Itertools;
|
||||
use pulldown_cmark::{Options, Parser};
|
||||
use rss::{validation::Validate, ChannelBuilder, ItemBuilder};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize, Serializer};
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use time::{format_description::well_known::Rfc2822, OffsetDateTime};
|
||||
|
||||
use crate::{builder::SiteBuilder, link_list::Link, PageMetadata, SiteConfig};
|
||||
use crate::{builder::SiteBuilder, link_list::Link, PageMetadata};
|
||||
|
||||
/// Source base path for resources.
|
||||
pub const RESOURCES_PATH: &str = "resources";
|
||||
|
||||
/// Metadata for resources.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ResourceMetadata<T> {
|
||||
pub struct ResourceMetadata {
|
||||
/// The resource's title.
|
||||
pub title: String,
|
||||
/// The resource's timestamp.
|
||||
|
@ -23,9 +25,13 @@ pub struct ResourceMetadata<T> {
|
|||
pub timestamp: OffsetDateTime,
|
||||
/// The resource's tags.
|
||||
pub tags: Vec<String>,
|
||||
/// Special field that gets transformed to the full CDN URL for the given path.
|
||||
pub cdn_file: Option<String>,
|
||||
/// The resource's description, if any.
|
||||
pub desc: Option<String>,
|
||||
/// Extra resource data not included.
|
||||
#[serde(flatten)]
|
||||
pub inner: T,
|
||||
pub inner: serde_yml::Value,
|
||||
/// Whether the resource is a draft. Drafts can be committed without being published to the live site.
|
||||
#[serde(default)]
|
||||
pub draft: bool,
|
||||
|
@ -35,21 +41,18 @@ pub struct ResourceMetadata<T> {
|
|||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ResourceTemplateData<'r, M, E> {
|
||||
pub struct ResourceTemplateData<'r> {
|
||||
/// The resource's metadata.
|
||||
#[serde(flatten)]
|
||||
pub resource: &'r ResourceMetadata<M>,
|
||||
pub resource: &'r ResourceMetadata,
|
||||
/// The resource's ID.
|
||||
pub id: String,
|
||||
/// Extra data to be passed to the template.
|
||||
#[serde(flatten)]
|
||||
pub extra: E,
|
||||
/// The resource's timestamp. Duplicated to change serialization method.
|
||||
#[serde(serialize_with = "ResourceTemplateData::<M, E>::timestamp_formatter")]
|
||||
#[serde(serialize_with = "ResourceTemplateData::timestamp_formatter")]
|
||||
pub timestamp: OffsetDateTime,
|
||||
}
|
||||
|
||||
impl<'r, M, E> ResourceTemplateData<'r, M, E> {
|
||||
impl<'r> ResourceTemplateData<'r> {
|
||||
fn timestamp_formatter<S>(timestamp: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
|
@ -64,20 +67,6 @@ impl<'r, M, E> ResourceTemplateData<'r, M, E> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Trait for getting extra template data from resource metadata.
|
||||
pub trait ResourceMethods<E>
|
||||
where
|
||||
E: Serialize,
|
||||
{
|
||||
fn get_short_desc(&self) -> String;
|
||||
|
||||
fn get_extra_resource_template_data(&self, site_config: &SiteConfig) -> eyre::Result<E>;
|
||||
|
||||
fn get_head_data(&self, _site_config: &SiteConfig) -> eyre::Result<String> {
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// struct for adding custom meta content embeds
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct EmbedMetadata {
|
||||
|
@ -130,8 +119,8 @@ impl EmbedMetadata {
|
|||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ResourceListTemplateData<'r, M, E> {
|
||||
resources: Vec<&'r ResourceTemplateData<'r, M, E>>,
|
||||
struct ResourceListTemplateData<'r> {
|
||||
resources: Vec<&'r ResourceTemplateData<'r>>,
|
||||
tag: Option<&'r str>,
|
||||
page: usize,
|
||||
page_max: usize,
|
||||
|
@ -145,7 +134,7 @@ struct ExtraResourceRenderData {
|
|||
}
|
||||
|
||||
/// Config for the resource builder.
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
pub struct ResourceBuilderConfig {
|
||||
/// Path to where the resources should be loaded from.
|
||||
pub source_path: String,
|
||||
|
@ -174,27 +163,20 @@ pub struct ResourceBuilderConfig {
|
|||
}
|
||||
|
||||
/// Helper to genericize resource building.
|
||||
#[derive(Debug)]
|
||||
pub struct ResourceBuilder<M, E> {
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ResourceBuilder {
|
||||
/// The builder's config.
|
||||
pub config: ResourceBuilderConfig,
|
||||
/// The currently loaded resource metadata.
|
||||
pub loaded_metadata: Vec<(String, ResourceMetadata<M>)>,
|
||||
_extra: PhantomData<E>,
|
||||
pub loaded_metadata: Vec<(String, ResourceMetadata)>,
|
||||
}
|
||||
|
||||
impl<M, E> ResourceBuilder<M, E>
|
||||
where
|
||||
M: Serialize + DeserializeOwned,
|
||||
E: Serialize,
|
||||
ResourceMetadata<M>: ResourceMethods<E>,
|
||||
{
|
||||
impl ResourceBuilder {
|
||||
/// Creates a new resource builder.
|
||||
pub fn new(config: ResourceBuilderConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
loaded_metadata: Default::default(),
|
||||
_extra: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,13 +190,13 @@ where
|
|||
}
|
||||
|
||||
/// Loads resource metadata from the given path.
|
||||
fn load(builder: &SiteBuilder, path: &Path) -> eyre::Result<(String, ResourceMetadata<M>)> {
|
||||
fn load(builder: &SiteBuilder, path: &Path) -> eyre::Result<(String, ResourceMetadata)> {
|
||||
let id = Self::get_id(path);
|
||||
|
||||
let input = std::fs::read_to_string(path)?;
|
||||
let mut page = builder
|
||||
.matter
|
||||
.parse_with_struct::<ResourceMetadata<M>>(&input)
|
||||
.parse_with_struct::<ResourceMetadata>(&input)
|
||||
.ok_or_else(|| eyre::anyhow!("Failed to parse resource front matter"))?;
|
||||
|
||||
let parser = Parser::new_ext(&page.content, Options::all());
|
||||
|
@ -222,6 +204,9 @@ where
|
|||
pulldown_cmark::html::push_html(&mut html, parser);
|
||||
|
||||
page.data.content = html;
|
||||
if let Some(cdn_file) = page.data.cdn_file {
|
||||
page.data.cdn_file = Some(builder.site.config.cdn_url(&cdn_file)?.to_string());
|
||||
}
|
||||
|
||||
Ok((id, page.data))
|
||||
}
|
||||
|
@ -233,6 +218,7 @@ where
|
|||
for e in builder
|
||||
.site
|
||||
.site_path
|
||||
.join(RESOURCES_PATH)
|
||||
.join(&self.config.source_path)
|
||||
.read_dir()?
|
||||
{
|
||||
|
@ -262,13 +248,12 @@ where
|
|||
&self,
|
||||
builder: &SiteBuilder,
|
||||
id: String,
|
||||
resource: &ResourceMetadata<M>,
|
||||
resource: &ResourceMetadata,
|
||||
) -> eyre::Result<()> {
|
||||
let out_path = self.build_path(&builder.build_path, &id);
|
||||
let out = {
|
||||
let data = ResourceTemplateData {
|
||||
resource,
|
||||
extra: resource.get_extra_resource_template_data(&builder.site.config)?,
|
||||
id,
|
||||
timestamp: resource.timestamp,
|
||||
};
|
||||
|
@ -282,7 +267,20 @@ where
|
|||
},
|
||||
&out,
|
||||
ExtraResourceRenderData {
|
||||
head: resource.get_head_data(&builder.site.config)?,
|
||||
head: EmbedMetadata {
|
||||
title: resource.title.clone(),
|
||||
site_name: builder.site.config.title.clone(),
|
||||
description: resource.desc.clone(),
|
||||
image: if let Some(cdn_file) = &resource.cdn_file {
|
||||
Some(builder.site.config.cdn_url(cdn_file)?.to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
url: None,
|
||||
theme_color: EmbedMetadata::default_theme_color(),
|
||||
large_image: true,
|
||||
}
|
||||
.build(),
|
||||
},
|
||||
)?;
|
||||
std::fs::write(out_path, out)?;
|
||||
|
@ -309,28 +307,22 @@ where
|
|||
|
||||
let mut data = Vec::with_capacity(lmd.len());
|
||||
for (id, resource) in lmd.iter() {
|
||||
let extra = resource.get_extra_resource_template_data(&builder.site.config)?;
|
||||
data.push(ResourceTemplateData {
|
||||
resource,
|
||||
extra,
|
||||
id: id.clone(),
|
||||
timestamp: resource.timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
fn build_list<M, E>(
|
||||
fn build_list(
|
||||
builder: &SiteBuilder,
|
||||
config: &ResourceBuilderConfig,
|
||||
list: Vec<&ResourceTemplateData<M, E>>,
|
||||
list: Vec<&ResourceTemplateData>,
|
||||
title: &str,
|
||||
tag: Option<&str>,
|
||||
out_path: &Path,
|
||||
items_per_page: usize,
|
||||
) -> eyre::Result<()>
|
||||
where
|
||||
M: Serialize,
|
||||
E: Serialize,
|
||||
{
|
||||
) -> eyre::Result<()> {
|
||||
if !out_path.exists() {
|
||||
std::fs::create_dir_all(out_path)?;
|
||||
}
|
||||
|
@ -381,7 +373,7 @@ where
|
|||
)?;
|
||||
|
||||
// Build resource lists by tag
|
||||
let mut tags: BTreeMap<String, Vec<&ResourceTemplateData<M, E>>> = BTreeMap::new();
|
||||
let mut tags: BTreeMap<String, Vec<&ResourceTemplateData>> = BTreeMap::new();
|
||||
for resource in &data {
|
||||
for tag in resource.resource.tags.iter().cloned() {
|
||||
tags.entry(tag).or_default().push(resource);
|
||||
|
@ -442,7 +434,7 @@ where
|
|||
))?
|
||||
.to_string(),
|
||||
))
|
||||
.description(Some(resource.resource.get_short_desc()))
|
||||
.description(resource.resource.desc.clone())
|
||||
.pub_date(Some(resource.timestamp.format(&Rfc2822)?))
|
||||
.content(Some(
|
||||
builder.reg.render(&self.config.rss_template, &resource)?,
|
||||
|
@ -472,13 +464,3 @@ where
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<M, E> Default for ResourceBuilder<M, E> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: Default::default(),
|
||||
loaded_metadata: Default::default(),
|
||||
_extra: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,10 @@ use warp::{
|
|||
Filter,
|
||||
};
|
||||
|
||||
use crate::{util::get_name, Site, SiteBuilder, PAGES_PATH, ROOT_PATH, SASS_PATH, TEMPLATES_PATH};
|
||||
use crate::{
|
||||
resource::RESOURCES_PATH, util::get_name, Site, SiteBuilder, PAGES_PATH, ROOT_PATH, SASS_PATH,
|
||||
TEMPLATES_PATH,
|
||||
};
|
||||
|
||||
fn with_build_path(
|
||||
build_path: PathBuf,
|
||||
|
@ -31,6 +34,30 @@ fn rel(path: &Path, prefix: &Path) -> Result<PathBuf, std::path::StripPrefixErro
|
|||
Ok(path.strip_prefix(prefix)?.to_owned())
|
||||
}
|
||||
|
||||
/// Helper to build resources in the case of creation or removal.
|
||||
fn build_resources(builder: &mut SiteBuilder, path: &Path) -> eyre::Result<()> {
|
||||
let paths: Vec<_> = builder
|
||||
.resource_builders
|
||||
.values()
|
||||
.map(|b| {
|
||||
(
|
||||
b.config.source_path.clone(),
|
||||
path.strip_prefix(&b.config.source_path),
|
||||
)
|
||||
})
|
||||
.filter_map(|(p, v)| v.ok().map(|v| (p, v)))
|
||||
.collect();
|
||||
if paths.len() > 1 {
|
||||
todo!("handle more than one possible match");
|
||||
}
|
||||
if let Some((prefix, _path)) = paths.first() {
|
||||
// HACK: this could get very inefficient with a larger number of resources. should definitely optimize
|
||||
builder.reload_resource_builder(prefix)?;
|
||||
builder.build_resources(prefix)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates or updates a resource.
|
||||
fn create(
|
||||
builder: &mut SiteBuilder,
|
||||
|
@ -57,12 +84,12 @@ fn create(
|
|||
builder.refresh_template(&template_name_str, path)?;
|
||||
if build {
|
||||
builder.site.build_all_pages(builder)?;
|
||||
builder.build_images()?;
|
||||
builder.build_blog()?;
|
||||
builder.build_all_resources()?;
|
||||
}
|
||||
} else if relative_path.display().to_string() == "config.yaml" {
|
||||
let new_config = serde_yml::from_str(&std::fs::read_to_string(path)?)?;
|
||||
builder.site.config = new_config;
|
||||
builder.reload()?;
|
||||
builder.site.build_all_pages(builder)?;
|
||||
} else if let Ok(_sass_path) = relative_path.strip_prefix(SASS_PATH) {
|
||||
if build {
|
||||
|
@ -70,14 +97,8 @@ fn create(
|
|||
}
|
||||
} else if let Ok(root_path) = relative_path.strip_prefix(ROOT_PATH) {
|
||||
std::fs::copy(path, builder.build_path.join(root_path))?;
|
||||
} else if let Ok(_image_path) = relative_path.strip_prefix(crate::images::IMAGES_PATH) {
|
||||
// HACK: this could get very inefficient with a larger number of images. should definitely optimize
|
||||
builder.reload_images_builder()?;
|
||||
builder.build_images()?;
|
||||
} else if let Ok(_blog_path) = relative_path.strip_prefix(crate::blog::BLOG_PATH) {
|
||||
// HACK: same as above
|
||||
builder.reload_blog_builder()?;
|
||||
builder.build_blog()?;
|
||||
} else if let Ok(resources_path) = relative_path.strip_prefix(RESOURCES_PATH) {
|
||||
build_resources(builder, resources_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -106,14 +127,8 @@ fn remove(builder: &mut SiteBuilder, path: &Path, relative_path: &Path) -> eyre:
|
|||
builder.build_sass().wrap_err("Failed to rebuild Sass")?;
|
||||
} else if let Ok(root_path) = relative_path.strip_prefix(ROOT_PATH) {
|
||||
std::fs::remove_file(builder.build_path.join(root_path))?;
|
||||
} else if let Ok(_image_path) = relative_path.strip_prefix(crate::images::IMAGES_PATH) {
|
||||
// HACK: same as in `create`
|
||||
builder.reload_images_builder()?;
|
||||
builder.build_images()?;
|
||||
} else if let Ok(_blog_path) = relative_path.strip_prefix(crate::blog::BLOG_PATH) {
|
||||
// HACK: same as above
|
||||
builder.reload_blog_builder()?;
|
||||
builder.build_blog()?;
|
||||
} else if let Ok(resources_path) = relative_path.strip_prefix(RESOURCES_PATH) {
|
||||
build_resources(builder, resources_path)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -141,9 +156,8 @@ impl Site {
|
|||
}
|
||||
builder.build_sass().wrap_err("Failed to build Sass")?;
|
||||
builder
|
||||
.build_images()
|
||||
.wrap_err("Failed to build image pages")?;
|
||||
builder.build_blog().wrap_err("Failed to build blog")?;
|
||||
.build_all_resources()
|
||||
.wrap_err("Failed to build resources")?;
|
||||
|
||||
// Map of websocket connections
|
||||
let peers: Arc<Mutex<HashMap<SocketAddr, WebSocket>>> =
|
||||
|
|
Loading…
Add table
Reference in a new issue