mirror of
https://github.com/zyllian/zyllian.github.io.git
synced 2025-05-09 18:16:43 -07:00
Make the method to define image pages more generic
This commit is contained in:
parent
e755173d39
commit
b5d44862a2
6 changed files with 427 additions and 292 deletions
|
@ -14,7 +14,7 @@
|
||||||
<a href="./{{next}}">Next page</a>
|
<a href="./{{next}}">Next page</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<div class="images-list">
|
<div class="images-list">
|
||||||
{{#each images}}
|
{{#each resources}}
|
||||||
<a class="image" href="/i/{{id}}">
|
<a class="image" href="/i/{{id}}">
|
||||||
<img class="image-actual" src="{{src}}" alt="{{alt}}">
|
<img class="image-actual" src="{{src}}" alt="{{alt}}">
|
||||||
<span class="title">{{title}}</span>
|
<span class="title">{{title}}</span>
|
||||||
|
|
|
@ -10,7 +10,7 @@ use pulldown_cmark::{Options, Parser};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{images::ImageMetadata, util, PageMetadata, Site, ROOT_PATH, SASS_PATH};
|
use crate::{util, PageMetadata, Site, ROOT_PATH, SASS_PATH};
|
||||||
|
|
||||||
/// URLs which need to have a "me" rel attribute.
|
/// URLs which need to have a "me" rel attribute.
|
||||||
const ME_URLS: &[&str] = &["https://mas.to/@zyl"];
|
const ME_URLS: &[&str] = &["https://mas.to/@zyl"];
|
||||||
|
@ -240,11 +240,6 @@ impl<'a> SiteBuilder<'a> {
|
||||||
|
|
||||||
/// Builds the site's various image pages.
|
/// Builds the site's various image pages.
|
||||||
pub fn build_images(&self) -> anyhow::Result<()> {
|
pub fn build_images(&self) -> anyhow::Result<()> {
|
||||||
let images = ImageMetadata::load_all(&self.site.site_path)?;
|
crate::images::build_images(self)
|
||||||
ImageMetadata::build_lists(self, &images)?;
|
|
||||||
for (id, image) in images {
|
|
||||||
image.build(self, id)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
319
src/images.rs
319
src/images.rs
|
@ -1,312 +1,75 @@
|
||||||
use std::{
|
use serde::{Deserialize, Serialize};
|
||||||
collections::BTreeMap,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
use itertools::Itertools;
|
|
||||||
use rss::{validation::Validate, ChannelBuilder, ItemBuilder};
|
|
||||||
use serde::{Deserialize, Serialize, Serializer};
|
|
||||||
use time::{format_description::well_known::Rfc2822, OffsetDateTime};
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{builder::SiteBuilder, link_list::Link, PageMetadata, SiteConfig};
|
use crate::{
|
||||||
|
builder::SiteBuilder,
|
||||||
|
resource::{ResourceBuilder, ResourceBuilderConfig, ResourceMetadata, ResourceMethods},
|
||||||
|
SiteConfig,
|
||||||
|
};
|
||||||
|
|
||||||
pub(crate) const IMAGES_PATH: &str = "images";
|
pub(crate) const IMAGES_PATH: &str = "images";
|
||||||
pub(crate) const IMAGES_OUT_PATH: &str = "i";
|
pub(crate) const IMAGES_OUT_PATH: &str = "i";
|
||||||
|
|
||||||
|
pub fn build_images(site_builder: &SiteBuilder) -> anyhow::Result<()> {
|
||||||
|
let config = 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: "Zyllian's images".to_string(),
|
||||||
|
rss_description: "Feed of newly uploaded images from Zyllian'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_builder.site.config.images_per_page,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut builder = ResourceBuilder::<ImageMetadata, ImageTemplateData>::new(config);
|
||||||
|
builder.load_all(&site_builder.site.site_path)?;
|
||||||
|
builder.build_all(site_builder)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Definition for a remote image.
|
/// Definition for a remote image.
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct ImageMetadata {
|
pub struct ImageMetadata {
|
||||||
/// The image's title.
|
|
||||||
pub title: String,
|
|
||||||
/// The image's timestamp.
|
|
||||||
#[serde(with = "time::serde::rfc3339")]
|
|
||||||
pub timestamp: OffsetDateTime,
|
|
||||||
/// The image's alt text.
|
/// The image's alt text.
|
||||||
pub alt: String,
|
pub alt: String,
|
||||||
/// The image's extra description, if any.
|
/// The image's extra description, if any.
|
||||||
pub desc: Option<String>,
|
pub desc: Option<String>,
|
||||||
/// The image's file path.
|
/// The image's file path.
|
||||||
pub file: String,
|
pub file: String,
|
||||||
/// The image's tags.
|
|
||||||
pub tags: Vec<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageMetadata {
|
impl ImageMetadata {
|
||||||
/// Gets an image's ID from its path.
|
|
||||||
pub fn get_id(path: &Path) -> String {
|
|
||||||
path.with_extension("")
|
|
||||||
.file_name()
|
|
||||||
.expect("Should never fail")
|
|
||||||
.to_string_lossy()
|
|
||||||
.into_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads an image's ID and metadata.
|
|
||||||
pub fn load(path: &Path) -> anyhow::Result<(String, Self)> {
|
|
||||||
let id = Self::get_id(path);
|
|
||||||
let metadata: ImageMetadata = serde_yaml::from_str(&std::fs::read_to_string(path)?)?;
|
|
||||||
Ok((id, metadata))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads all available images.
|
|
||||||
pub fn load_all(site_path: &Path) -> anyhow::Result<Vec<(String, Self)>> {
|
|
||||||
let images_path = site_path.join(IMAGES_PATH);
|
|
||||||
let mut images = Vec::new();
|
|
||||||
for e in images_path.read_dir()? {
|
|
||||||
let p = e?.path();
|
|
||||||
if let Some(ext) = p.extension() {
|
|
||||||
if ext == "yml" {
|
|
||||||
let (id, metadata) = Self::load(&p)?;
|
|
||||||
images.push((id, metadata));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
images.sort_by(|a, b| a.1.timestamp.cmp(&b.1.timestamp).reverse());
|
|
||||||
Ok(images)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds an image's page.
|
|
||||||
pub fn build(self, builder: &SiteBuilder, id: String) -> anyhow::Result<()> {
|
|
||||||
let out = {
|
|
||||||
let data = ImageTemplateData {
|
|
||||||
image: &self,
|
|
||||||
src: self.cdn_url(&builder.site.config)?.to_string(),
|
|
||||||
id: &id,
|
|
||||||
timestamp: self.timestamp,
|
|
||||||
};
|
|
||||||
builder.reg.render("image", &data)?
|
|
||||||
};
|
|
||||||
let out = builder.build_page_raw(
|
|
||||||
PageMetadata {
|
|
||||||
title: Some(self.title),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
&out,
|
|
||||||
)?;
|
|
||||||
let out_path = Self::build_path(&builder.build_path, &id);
|
|
||||||
std::fs::write(out_path, out)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets an image's CDN url.
|
/// Gets an image's CDN url.
|
||||||
pub fn cdn_url(&self, config: &SiteConfig) -> anyhow::Result<Url> {
|
pub fn cdn_url(&self, config: &SiteConfig) -> anyhow::Result<Url> {
|
||||||
Ok(config.cdn_url.join(&config.s3_prefix)?.join(&self.file)?)
|
Ok(config.cdn_url.join(&config.s3_prefix)?.join(&self.file)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets an image's build path.
|
|
||||||
pub fn build_path(build_path: &Path, id: &str) -> PathBuf {
|
|
||||||
build_path
|
|
||||||
.join(IMAGES_OUT_PATH)
|
|
||||||
.join(id)
|
|
||||||
.with_extension("html")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds the various list pages for images.
|
|
||||||
pub fn build_lists(builder: &SiteBuilder, metadata: &[(String, Self)]) -> anyhow::Result<()> {
|
|
||||||
let mut data = Vec::with_capacity(metadata.len());
|
|
||||||
for (id, metadata) in metadata {
|
|
||||||
data.push(ImageTemplateData {
|
|
||||||
image: metadata,
|
|
||||||
src: metadata.cdn_url(&builder.site.config)?.to_string(),
|
|
||||||
id,
|
|
||||||
timestamp: metadata.timestamp,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_list(
|
|
||||||
builder: &SiteBuilder,
|
|
||||||
list: Vec<&ImageTemplateData>,
|
|
||||||
title: &str,
|
|
||||||
tag: Option<&str>,
|
|
||||||
out_path: &Path,
|
|
||||||
items_per_page: usize,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
if !out_path.exists() {
|
|
||||||
std::fs::create_dir_all(out_path)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let page_max = list.len() / items_per_page
|
|
||||||
+ if list.len() % items_per_page == 0 {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
};
|
|
||||||
let mut previous = None;
|
|
||||||
let mut next;
|
|
||||||
for (page, iter) in list.iter().chunks(items_per_page).into_iter().enumerate() {
|
|
||||||
next = (page + 1 != page_max).then_some(page + 2);
|
|
||||||
let data = ImageListTemplateData {
|
|
||||||
images: iter.copied().collect(),
|
|
||||||
tag,
|
|
||||||
page: page + 1,
|
|
||||||
page_max,
|
|
||||||
previous,
|
|
||||||
next,
|
|
||||||
};
|
|
||||||
let out = builder.reg.render("images", &data)?;
|
|
||||||
let out = builder.build_page_raw(
|
|
||||||
PageMetadata {
|
|
||||||
title: Some(title.to_owned()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
&out,
|
|
||||||
)?;
|
|
||||||
if page == 0 {
|
|
||||||
std::fs::write(out_path.join("index.html"), &out)?;
|
|
||||||
}
|
|
||||||
std::fs::write(
|
|
||||||
out_path.join(out_path.join((page + 1).to_string()).with_extension("html")),
|
|
||||||
out,
|
|
||||||
)?;
|
|
||||||
previous = Some(page + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
build_list(
|
|
||||||
builder,
|
|
||||||
data.iter().collect(),
|
|
||||||
"Images",
|
|
||||||
None,
|
|
||||||
&builder.build_path.join("images"),
|
|
||||||
builder.site.config.images_per_page,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut tags: BTreeMap<String, Vec<&ImageTemplateData>> = BTreeMap::new();
|
|
||||||
for image in &data {
|
|
||||||
for tag in image.image.tags.iter().cloned() {
|
|
||||||
tags.entry(tag).or_default().push(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let links = tags
|
|
||||||
.iter()
|
|
||||||
.map(|(tag, data)| {
|
|
||||||
let count = data.len();
|
|
||||||
(
|
|
||||||
Link::new(
|
|
||||||
format!("/{IMAGES_OUT_PATH}/tag/{tag}/"),
|
|
||||||
format!("{tag} ({count})"),
|
|
||||||
),
|
|
||||||
count,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.sorted_by(|(_, a), (_, b)| a.cmp(b).reverse())
|
|
||||||
.map(|(l, _)| l)
|
|
||||||
.collect();
|
|
||||||
let out = crate::link_list::render_basic_link_list(builder, links, "Image Tags")?;
|
|
||||||
std::fs::write(
|
|
||||||
builder.build_path.join(IMAGES_OUT_PATH).join("tags.html"),
|
|
||||||
out,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (tag, data) in tags {
|
|
||||||
build_list(
|
|
||||||
builder,
|
|
||||||
data,
|
|
||||||
&format!("Images tagged {tag}"),
|
|
||||||
Some(tag.as_str()),
|
|
||||||
&builder
|
|
||||||
.build_path
|
|
||||||
.join(IMAGES_OUT_PATH)
|
|
||||||
.join("tag")
|
|
||||||
.join(&tag),
|
|
||||||
builder.site.config.images_per_page,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut items = Vec::with_capacity(data.len());
|
|
||||||
for image in data {
|
|
||||||
items.push(
|
|
||||||
ItemBuilder::default()
|
|
||||||
.title(Some(image.image.title.to_owned()))
|
|
||||||
.link(Some(
|
|
||||||
builder
|
|
||||||
.site
|
|
||||||
.config
|
|
||||||
.base_url
|
|
||||||
.join(&format!("{IMAGES_OUT_PATH}/{}", image.id))?
|
|
||||||
.to_string(),
|
|
||||||
))
|
|
||||||
.description(image.image.desc.to_owned())
|
|
||||||
.pub_date(Some(image.image.timestamp.format(&Rfc2822)?))
|
|
||||||
.content(Some(builder.reg.render("rss/image", &image)?))
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build RSS feed
|
|
||||||
let channel = ChannelBuilder::default()
|
|
||||||
.title("Zyllian's images".to_string())
|
|
||||||
.link(
|
|
||||||
builder
|
|
||||||
.site
|
|
||||||
.config
|
|
||||||
.base_url
|
|
||||||
.join("images/")
|
|
||||||
.expect("Should never fail"),
|
|
||||||
)
|
|
||||||
.description("Feed of newly uploaded images from Zyllian's website.".to_string())
|
|
||||||
.last_build_date(Some(OffsetDateTime::now_utc().format(&Rfc2822)?))
|
|
||||||
.items(items)
|
|
||||||
.build();
|
|
||||||
channel.validate().context("Failed to validate RSS feed")?;
|
|
||||||
let out = channel.to_string();
|
|
||||||
std::fs::write(builder.build_path.join("images").join("rss.xml"), out)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Template data for a specific image.
|
/// Template data for a specific image.
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct ImageTemplateData<'i> {
|
struct ImageTemplateData {
|
||||||
/// The image's regular metadata.
|
|
||||||
#[serde(flatten)]
|
|
||||||
image: &'i ImageMetadata,
|
|
||||||
/// Direct URL to the image's CDN location.
|
/// Direct URL to the image's CDN location.
|
||||||
/// TODO: link to smaller versions on list pages
|
/// TODO: link to smaller versions on list pages
|
||||||
src: String,
|
src: String,
|
||||||
/// The image's ID.
|
|
||||||
id: &'i str,
|
|
||||||
/// The image's timestamp. (Duplicated to change the serialization method.)
|
|
||||||
#[serde(serialize_with = "ImageTemplateData::timestamp_formatter")]
|
|
||||||
timestamp: OffsetDateTime,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'i> ImageTemplateData<'i> {
|
impl ResourceMethods<ImageTemplateData> for ResourceMetadata<ImageMetadata> {
|
||||||
fn timestamp_formatter<S>(timestamp: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
|
fn get_short_desc(&self) -> String {
|
||||||
where
|
self.inner.desc.clone().unwrap_or_default()
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
let out = timestamp
|
|
||||||
.format(
|
|
||||||
&time::format_description::parse("[weekday], [month repr:long] [day], [year]")
|
|
||||||
.expect("Should never fail"),
|
|
||||||
)
|
|
||||||
.expect("Should never fail");
|
|
||||||
serializer.serialize_str(&out)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Template data for image lists.
|
fn get_extra_resource_template_data(
|
||||||
#[derive(Debug, Serialize)]
|
&self,
|
||||||
struct ImageListTemplateData<'i> {
|
site_config: &SiteConfig,
|
||||||
/// The list of images to display.
|
) -> anyhow::Result<ImageTemplateData> {
|
||||||
images: Vec<&'i ImageTemplateData<'i>>,
|
Ok(ImageTemplateData {
|
||||||
/// The current tag, if any.
|
src: self.inner.cdn_url(site_config)?.to_string(),
|
||||||
tag: Option<&'i str>,
|
})
|
||||||
/// The current page.
|
}
|
||||||
page: usize,
|
|
||||||
/// The total number of pages.
|
|
||||||
page_max: usize,
|
|
||||||
/// The previous page, if any.
|
|
||||||
previous: Option<usize>,
|
|
||||||
/// The next page, if any.
|
|
||||||
next: Option<usize>,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
mod builder;
|
mod builder;
|
||||||
mod images;
|
mod images;
|
||||||
mod link_list;
|
mod link_list;
|
||||||
|
mod resource;
|
||||||
#[cfg(feature = "serve")]
|
#[cfg(feature = "serve")]
|
||||||
pub mod serving;
|
pub mod serving;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
380
src/resource.rs
Normal file
380
src/resource.rs
Normal file
|
@ -0,0 +1,380 @@
|
||||||
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
marker::PhantomData,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use rss::{validation::Validate, ChannelBuilder, ItemBuilder};
|
||||||
|
use serde::{de::DeserializeOwned, Deserialize, Serialize, Serializer};
|
||||||
|
use time::{format_description::well_known::Rfc2822, OffsetDateTime};
|
||||||
|
|
||||||
|
use crate::{builder::SiteBuilder, link_list::Link, PageMetadata, SiteConfig};
|
||||||
|
|
||||||
|
/// Metadata for resources.
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct ResourceMetadata<T> {
|
||||||
|
/// The resource's title.
|
||||||
|
pub title: String,
|
||||||
|
/// The resource's timestamp.
|
||||||
|
#[serde(with = "time::serde::rfc3339")]
|
||||||
|
pub timestamp: OffsetDateTime,
|
||||||
|
/// The resource's tags.
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
/// Extra resource data not included.
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub inner: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct ResourceTemplateData<'r, M, E> {
|
||||||
|
/// The resource's metadata.
|
||||||
|
#[serde(flatten)]
|
||||||
|
resource: &'r ResourceMetadata<M>,
|
||||||
|
/// The resource's ID.
|
||||||
|
id: String,
|
||||||
|
/// Extra data to be passed to the template.
|
||||||
|
#[serde(flatten)]
|
||||||
|
extra: E,
|
||||||
|
/// The resource's timestamp. Duplicated to change serialization method.
|
||||||
|
#[serde(serialize_with = "ResourceTemplateData::<M, E>::timestamp_formatter")]
|
||||||
|
timestamp: OffsetDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r, M, E> ResourceTemplateData<'r, M, E> {
|
||||||
|
fn timestamp_formatter<S>(timestamp: &OffsetDateTime, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let out = timestamp
|
||||||
|
.format(
|
||||||
|
&time::format_description::parse("[weekday], [month repr:long] [day], [year]")
|
||||||
|
.expect("Should never fail"),
|
||||||
|
)
|
||||||
|
.expect("Should never fail");
|
||||||
|
serializer.serialize_str(&out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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) -> anyhow::Result<E>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ResourceListTemplateData<'r, M, E> {
|
||||||
|
resources: Vec<&'r ResourceTemplateData<'r, M, E>>,
|
||||||
|
tag: Option<&'r str>,
|
||||||
|
page: usize,
|
||||||
|
page_max: usize,
|
||||||
|
previous: Option<usize>,
|
||||||
|
next: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Config for the resource builder.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ResourceBuilderConfig {
|
||||||
|
/// Path to where the resources should be loaded from.
|
||||||
|
pub source_path: String,
|
||||||
|
/// Path to where the resource pages should be written to.
|
||||||
|
pub output_path_short: String,
|
||||||
|
/// Path to where the main list should be written to.
|
||||||
|
pub output_path_long: String,
|
||||||
|
/// The template used to render a single resource.
|
||||||
|
pub resource_template: String,
|
||||||
|
/// The template used to render a list of resources.
|
||||||
|
pub resource_list_template: String,
|
||||||
|
/// Template used when rendering the RSS feed.
|
||||||
|
pub rss_template: String,
|
||||||
|
/// The RSS feed's title.
|
||||||
|
pub rss_title: String,
|
||||||
|
/// The description for the RSS feed.
|
||||||
|
pub rss_description: String,
|
||||||
|
/// Title for the main list of resources.
|
||||||
|
pub list_title: String,
|
||||||
|
/// Title for the page containing a list of tags.
|
||||||
|
pub tag_list_title: String,
|
||||||
|
/// Name for the resource type in plural.
|
||||||
|
pub resource_name_plural: String,
|
||||||
|
/// The number of resources to display on a single page.
|
||||||
|
pub resources_per_page: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper to genericize resource building.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ResourceBuilder<M, E> {
|
||||||
|
/// The builder's config.
|
||||||
|
config: ResourceBuilderConfig,
|
||||||
|
/// The currently loaded resource metadata.
|
||||||
|
loaded_metadata: Vec<(String, ResourceMetadata<M>)>,
|
||||||
|
_extra: PhantomData<E>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<M, E> ResourceBuilder<M, E>
|
||||||
|
where
|
||||||
|
M: Serialize + DeserializeOwned,
|
||||||
|
E: Serialize,
|
||||||
|
ResourceMetadata<M>: ResourceMethods<E>,
|
||||||
|
{
|
||||||
|
/// Creates a new resource builder.
|
||||||
|
pub fn new(config: ResourceBuilderConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
config,
|
||||||
|
loaded_metadata: Default::default(),
|
||||||
|
_extra: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a resource's ID from its path.
|
||||||
|
fn get_id(path: &Path) -> String {
|
||||||
|
path.with_extension("")
|
||||||
|
.file_name()
|
||||||
|
.expect("Should never fail")
|
||||||
|
.to_string_lossy()
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads resource metadata from the given path.
|
||||||
|
fn load(path: &Path) -> anyhow::Result<(String, ResourceMetadata<M>)> {
|
||||||
|
let id = Self::get_id(path);
|
||||||
|
let metadata = serde_yaml::from_str(&std::fs::read_to_string(path)?)?;
|
||||||
|
Ok((id, metadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads all resource metadata from the given config.
|
||||||
|
pub fn load_all(&mut self, site_path: &Path) -> anyhow::Result<()> {
|
||||||
|
self.loaded_metadata.clear();
|
||||||
|
for e in site_path.join(&self.config.source_path).read_dir()? {
|
||||||
|
let p = e?.path();
|
||||||
|
if let Some("yml") = p.extension().and_then(|e| e.to_str()) {
|
||||||
|
let (id, metadata) = Self::load(&p)?;
|
||||||
|
self.loaded_metadata.push((id, metadata));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.loaded_metadata
|
||||||
|
.sort_by(|a, b| b.1.timestamp.cmp(&a.1.timestamp));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a resource's build path.
|
||||||
|
fn build_path(&self, base_path: &Path, id: &str) -> PathBuf {
|
||||||
|
base_path
|
||||||
|
.join(&self.config.output_path_short)
|
||||||
|
.join(id)
|
||||||
|
.with_extension("html")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a single resource page.
|
||||||
|
fn build(
|
||||||
|
&self,
|
||||||
|
builder: &SiteBuilder,
|
||||||
|
id: String,
|
||||||
|
resource: &ResourceMetadata<M>,
|
||||||
|
) -> anyhow::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,
|
||||||
|
};
|
||||||
|
builder.reg.render(&self.config.resource_template, &data)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let out = builder.build_page_raw(
|
||||||
|
PageMetadata {
|
||||||
|
title: Some(resource.title.clone()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&out,
|
||||||
|
)?;
|
||||||
|
std::fs::write(out_path, out)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_all(&self, builder: &SiteBuilder) -> anyhow::Result<()> {
|
||||||
|
for (id, resource) in &self.loaded_metadata {
|
||||||
|
self.build(builder, id.clone(), resource)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data = Vec::with_capacity(self.loaded_metadata.len());
|
||||||
|
for (id, resource) in &self.loaded_metadata {
|
||||||
|
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>(
|
||||||
|
builder: &SiteBuilder,
|
||||||
|
config: &ResourceBuilderConfig,
|
||||||
|
list: Vec<&ResourceTemplateData<M, E>>,
|
||||||
|
title: &str,
|
||||||
|
tag: Option<&str>,
|
||||||
|
out_path: &Path,
|
||||||
|
items_per_page: usize,
|
||||||
|
) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
M: Serialize,
|
||||||
|
E: Serialize,
|
||||||
|
{
|
||||||
|
if !out_path.exists() {
|
||||||
|
std::fs::create_dir_all(out_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let page_max = list.len() / items_per_page + (list.len() % items_per_page).min(1);
|
||||||
|
let mut previous = None;
|
||||||
|
let mut next;
|
||||||
|
for (page, iter) in list.iter().chunks(items_per_page).into_iter().enumerate() {
|
||||||
|
next = (page + 1 != page_max).then_some(page + 2);
|
||||||
|
let data = ResourceListTemplateData {
|
||||||
|
resources: iter.copied().collect(),
|
||||||
|
tag,
|
||||||
|
page: page + 1,
|
||||||
|
page_max,
|
||||||
|
previous,
|
||||||
|
next,
|
||||||
|
};
|
||||||
|
let out = builder.reg.render(&config.resource_list_template, &data)?;
|
||||||
|
let out = builder.build_page_raw(
|
||||||
|
PageMetadata {
|
||||||
|
title: Some(title.to_owned()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&out,
|
||||||
|
)?;
|
||||||
|
if page == 0 {
|
||||||
|
std::fs::write(out_path.join("index.html"), &out)?;
|
||||||
|
}
|
||||||
|
std::fs::write(
|
||||||
|
out_path.join((page + 1).to_string()).with_extension("html"),
|
||||||
|
out,
|
||||||
|
)?;
|
||||||
|
previous = Some(page + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
let out_path = builder.build_path.join(&self.config.output_path_short);
|
||||||
|
|
||||||
|
// Build main list of resources
|
||||||
|
build_list(
|
||||||
|
builder,
|
||||||
|
&self.config,
|
||||||
|
data.iter().collect(),
|
||||||
|
&self.config.list_title,
|
||||||
|
None,
|
||||||
|
&builder.build_path.join(&self.config.output_path_long),
|
||||||
|
self.config.resources_per_page,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Build resource lists by tag
|
||||||
|
let mut tags: BTreeMap<String, Vec<&ResourceTemplateData<M, E>>> = BTreeMap::new();
|
||||||
|
for resource in &data {
|
||||||
|
for tag in resource.resource.tags.iter().cloned() {
|
||||||
|
tags.entry(tag).or_default().push(resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build list of tags
|
||||||
|
{
|
||||||
|
let links = tags
|
||||||
|
.iter()
|
||||||
|
.map(|(tag, data)| {
|
||||||
|
let count = data.len();
|
||||||
|
(
|
||||||
|
Link::new(
|
||||||
|
format!("/{}/tag/{tag}/", self.config.output_path_short),
|
||||||
|
format!("{tag} ({count})"),
|
||||||
|
),
|
||||||
|
count,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.sorted_by(|(_, a), (_, b)| b.cmp(a))
|
||||||
|
.map(|(l, _)| l)
|
||||||
|
.collect();
|
||||||
|
let out = crate::link_list::render_basic_link_list(
|
||||||
|
builder,
|
||||||
|
links,
|
||||||
|
&self.config.tag_list_title,
|
||||||
|
)?;
|
||||||
|
std::fs::write(out_path.join("tags.html"), out)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (tag, data) in tags {
|
||||||
|
build_list(
|
||||||
|
builder,
|
||||||
|
&self.config,
|
||||||
|
data,
|
||||||
|
&format!("{} tagged {tag}", self.config.resource_name_plural),
|
||||||
|
Some(tag.as_str()),
|
||||||
|
&out_path.join("tag").join(&tag),
|
||||||
|
self.config.resources_per_page,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build RSS feed
|
||||||
|
let mut items = Vec::with_capacity(data.len());
|
||||||
|
for resource in data {
|
||||||
|
items.push(
|
||||||
|
ItemBuilder::default()
|
||||||
|
.title(Some(resource.resource.title.to_owned()))
|
||||||
|
.link(Some(
|
||||||
|
builder
|
||||||
|
.site
|
||||||
|
.config
|
||||||
|
.base_url
|
||||||
|
.join(&format!(
|
||||||
|
"{}/{}",
|
||||||
|
self.config.output_path_short, resource.id
|
||||||
|
))?
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
.description(Some(resource.resource.get_short_desc()))
|
||||||
|
.pub_date(Some(resource.timestamp.format(&Rfc2822)?))
|
||||||
|
.content(Some(
|
||||||
|
builder.reg.render(&self.config.rss_template, &resource)?,
|
||||||
|
))
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel = ChannelBuilder::default()
|
||||||
|
.title(self.config.rss_title.clone())
|
||||||
|
.link(
|
||||||
|
builder
|
||||||
|
.site
|
||||||
|
.config
|
||||||
|
.base_url
|
||||||
|
.join(&format!("{}/", self.config.output_path_long))
|
||||||
|
.expect("Should never fail"),
|
||||||
|
)
|
||||||
|
.description(self.config.rss_description.clone())
|
||||||
|
.last_build_date(Some(OffsetDateTime::now_utc().format(&Rfc2822)?))
|
||||||
|
.items(items)
|
||||||
|
.build();
|
||||||
|
channel.validate().context("Failed to validate RSS feed")?;
|
||||||
|
let out = channel.to_string();
|
||||||
|
std::fs::write(
|
||||||
|
builder
|
||||||
|
.build_path
|
||||||
|
.join(&self.config.output_path_long)
|
||||||
|
.join("rss.xml"),
|
||||||
|
out,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,9 +18,7 @@ use warp::{
|
||||||
Filter,
|
Filter,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{Site, SiteBuilder, PAGES_PATH, ROOT_PATH, SASS_PATH, TEMPLATES_PATH};
|
||||||
images::ImageMetadata, Site, SiteBuilder, PAGES_PATH, ROOT_PATH, SASS_PATH, TEMPLATES_PATH,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn with_build_path(
|
fn with_build_path(
|
||||||
build_path: PathBuf,
|
build_path: PathBuf,
|
||||||
|
@ -109,8 +107,6 @@ fn remove(builder: &mut SiteBuilder, path: &Path, relative_path: &Path) -> anyho
|
||||||
} else if let Ok(root_path) = relative_path.strip_prefix(ROOT_PATH) {
|
} else if let Ok(root_path) = relative_path.strip_prefix(ROOT_PATH) {
|
||||||
std::fs::remove_file(builder.build_path.join(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) {
|
} else if let Ok(_image_path) = relative_path.strip_prefix(crate::images::IMAGES_PATH) {
|
||||||
let p = ImageMetadata::build_path(&builder.build_path, &ImageMetadata::get_id(path));
|
|
||||||
std::fs::remove_file(p)?;
|
|
||||||
// HACK: same as in `create`
|
// HACK: same as in `create`
|
||||||
builder.build_images()?;
|
builder.build_images()?;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue