implement custom timestamp formats for resources, resolves #15

This commit is contained in:
zyl 2024-11-13 14:52:36 -08:00
parent 0233bf0dad
commit 5b09813f24
Signed by: zyl
SSH key fingerprint: SHA256:uxxbSXbdroP/OnKBGnEDk5q7EKB2razvstC/KmzdXXs
6 changed files with 62 additions and 38 deletions

View file

@ -109,6 +109,10 @@ the name of this resource type if it is plural.
how many resources of this type to display per page when content is paginated. how many resources of this type to display per page when content is paginated.
### `timestamp_format`
format to display timestamps as, as defined by version 2 of [the rust time crate's format description](https://time-rs.github.io/book/api/format-description.html).
## defining a resource ## defining a resource
resources are made up of markdown files with yaml front matter. for instance: resources are made up of markdown files with yaml front matter. for instance:
@ -154,3 +158,11 @@ whether the resource is a draft and should be excluded from normal builds. defau
### other properties ### other properties
resources may add extra properties which will get passed to the various resource templates later. simply add the property like it was any other property. resources may add extra properties which will get passed to the various resource templates later. simply add the property like it was any other property.
## extra properties
in addition to the resource properties, resources may receive additional properties from webdog as follows:
### `readable_timestamp`
the resource's timestamp in the timestamp format provided in the resource type's config.

View file

@ -1,19 +1,19 @@
{% extends "base.tera" %} {% extends "base.tera" %}
{% block content %} {% block content %}
<div> <div>
<h1>{{ title }}</h1> <h1>{{ data.title }}</h1>
<span>published {{ timestamp }}</span> <span>published {{ data.readable_timestamp }}</span>
{% if draft %} {% if data.draft %}
<h2>DRAFT</h2> <h2>DRAFT</h2>
{% endif %} {% endif %}
<p>{{ desc }}</p> <p>{{ data.desc }}</p>
<div> <div>
{{ content | safe }} {{ data.content | safe }}
</div> </div>
<hr /> <hr />
<h3>tags</h3> <h3>tags</h3>
<div> <div>
{% for tag in tags %} {% for tag in data.tags %}
<a href="/!!RESOURCE_TYPE!!/tag/{{tag}}">{{ tag }}</a>{% if not loop.last %},{% endif %} <a href="/!!RESOURCE_TYPE!!/tag/{{tag}}">{{ tag }}</a>{% if not loop.last %},{% endif %}
{% endfor %} {% endfor %}
</div> </div>

View file

@ -1,3 +1,4 @@
use itertools::Itertools;
use lol_html::{element, RewriteStrSettings}; use lol_html::{element, RewriteStrSettings};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -91,22 +92,31 @@ fn resource_list_outside(
let data: ResourceListData = serde_yml::from_value(data.inner.clone())?; let data: ResourceListData = serde_yml::from_value(data.inner.clone())?;
let res_builder = builder
.resource_builders
.get(&data.resource)
.ok_or_else(|| eyre::eyre!("missing resource builder: {}", data.resource))?;
let resource_list = builder.tera.render( let resource_list = builder.tera.render(
&data.template, &data.template,
&tera::Context::from_serialize(ResourceListTemplateData { &tera::Context::from_serialize(ResourceListTemplateData {
resources: builder resources: res_builder
.resource_builders
.get(&data.resource)
.ok_or_else(|| eyre::eyre!("missing resource builder: {}", data.resource))?
.loaded_metadata .loaded_metadata
.iter() .iter()
.take(data.count) .take(data.count)
.map(|(id, v)| ResourceTemplateData { .map(|(id, v)| {
crate::util::format_timestamp(
v.data().timestamp,
&res_builder.config.timestamp_format,
)
.map(|ts| (id, v, ts))
})
.map_ok(|(id, v, ts)| ResourceTemplateData {
resource: v, resource: v,
id: id.clone(), id: id.clone(),
timestamp: v.data().timestamp, readable_timestamp: ts,
}) })
.collect(), .collect::<eyre::Result<Vec<_>>>()?,
})?, })?,
)?; )?;

View file

@ -222,6 +222,7 @@ fn main() -> eyre::Result<()> {
tag_list_title: format!("{name} tags"), tag_list_title: format!("{name} tags"),
resource_name_plural: plural, resource_name_plural: plural,
resources_per_page: 3, resources_per_page: 3,
timestamp_format: "[weekday], [month repr:long] [day], [year]".to_string(),
}; };
config.resources.insert(id.clone(), resource_config); config.resources.insert(id.clone(), resource_config);

View file

@ -7,11 +7,12 @@ use eyre::Context;
use itertools::Itertools; use itertools::Itertools;
use pulldown_cmark::{Options, Parser}; use pulldown_cmark::{Options, Parser};
use rss::{validation::Validate, ChannelBuilder, ItemBuilder}; use rss::{validation::Validate, ChannelBuilder, ItemBuilder};
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize};
use time::{format_description::well_known::Rfc2822, OffsetDateTime}; use time::{format_description::well_known::Rfc2822, OffsetDateTime};
use crate::{ use crate::{
builder::SiteBuilder, frontmatter::FrontMatterRequired, link_list::Link, PageMetadata, builder::SiteBuilder, frontmatter::FrontMatterRequired, link_list::Link,
util::format_timestamp, PageMetadata,
}; };
/// Metadata for resources. /// Metadata for resources.
@ -43,24 +44,8 @@ pub struct ResourceTemplateData<'r> {
pub resource: &'r FrontMatterRequired<ResourceMetadata>, pub resource: &'r FrontMatterRequired<ResourceMetadata>,
/// The resource's ID. /// The resource's ID.
pub id: String, pub id: String,
/// The resource's timestamp. Duplicated to change serialization method. /// The resource's timestamp in a readable format.
#[serde(serialize_with = "ResourceTemplateData::timestamp_formatter")] pub readable_timestamp: String,
pub timestamp: OffsetDateTime,
}
impl<'r> ResourceTemplateData<'r> {
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)
}
} }
/// struct for adding custom meta content embeds /// struct for adding custom meta content embeds
@ -121,7 +106,7 @@ struct ResourceListTemplateData<'r> {
} }
/// Config for the resource builder. /// Config for the resource builder.
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceBuilderConfig { pub struct ResourceBuilderConfig {
/// Path to where the resources should be loaded from. /// Path to where the resources should be loaded from.
pub source_path: String, pub source_path: String,
@ -145,6 +130,8 @@ pub struct ResourceBuilderConfig {
pub resource_name_plural: String, pub resource_name_plural: String,
/// The number of resources to display on a single page. /// The number of resources to display on a single page.
pub resources_per_page: usize, pub resources_per_page: usize,
/// The format to use for the readable timestamp.
pub timestamp_format: String,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
@ -158,7 +145,7 @@ pub struct ResourceRSSBuilderConfig {
} }
/// Helper to genericize resource building. /// Helper to genericize resource building.
#[derive(Debug, Default)] #[derive(Debug)]
pub struct ResourceBuilder { pub struct ResourceBuilder {
/// The builder's config. /// The builder's config.
pub config: ResourceBuilderConfig, pub config: ResourceBuilderConfig,
@ -277,7 +264,10 @@ impl ResourceBuilder {
ResourceTemplateData { ResourceTemplateData {
resource, resource,
id, id,
timestamp: resource.data.as_ref().expect("should never fail").timestamp, readable_timestamp: format_timestamp(
resource.data().timestamp,
&self.config.timestamp_format,
)?,
}, },
)?; )?;
std::fs::write(out_path, out)?; std::fs::write(out_path, out)?;
@ -307,7 +297,10 @@ impl ResourceBuilder {
data.push(ResourceTemplateData { data.push(ResourceTemplateData {
resource, resource,
id: id.clone(), id: id.clone(),
timestamp: resource.data().timestamp, readable_timestamp: format_timestamp(
resource.data().timestamp,
&self.config.timestamp_format,
)?,
}); });
} }
@ -435,7 +428,7 @@ impl ResourceBuilder {
.to_string(), .to_string(),
)) ))
.description(resource.resource.data().desc.clone()) .description(resource.resource.data().desc.clone())
.pub_date(Some(resource.timestamp.format(&Rfc2822)?)) .pub_date(Some(resource.resource.data().timestamp.format(&Rfc2822)?))
.content(Some(builder.tera.render( .content(Some(builder.tera.render(
&rss.template, &rss.template,
&tera::Context::from_serialize(resource)?, &tera::Context::from_serialize(resource)?,

View file

@ -2,6 +2,8 @@
use std::path::Path; use std::path::Path;
use time::OffsetDateTime;
/// Simple helper to remove the contents of a directory without removing the directory itself. /// Simple helper to remove the contents of a directory without removing the directory itself.
pub fn remove_dir_contents(path: &Path) -> eyre::Result<()> { pub fn remove_dir_contents(path: &Path) -> eyre::Result<()> {
for entry in path.read_dir()? { for entry in path.read_dir()? {
@ -16,3 +18,9 @@ pub fn remove_dir_contents(path: &Path) -> eyre::Result<()> {
Ok(()) Ok(())
} }
/// Helper to format a timestamp according to the given format.
pub fn format_timestamp(ts: OffsetDateTime, format: &str) -> eyre::Result<String> {
let fmt = time::format_description::parse_borrowed::<2>(format)?;
Ok(ts.format(&fmt)?)
}