//! template processing use std::str::FromStr; use bempline::{Document, Options, variables}; use paige::Frontmatter; use timeparse::OffsetDateTime; mod features; use crate::{ config::Settings, fs::{RequestPath, SafePath}, state::SplormState, templated::features::Feature, }; /// variables extracted from the [`Frontmatter`] /// /// Contains all known-variables taken from the [`Frontmatter`] as well as /// methods to apply those variables to the template. /// /// Process a file with [`Self::build()`] and then use the `apply_` methods /// to fill in the template /// /// See the `TEMPLATES.md` file in the repository root to see what these /// variables mean pub struct TemplateVariables<'fm> { template: Option<&'fm str>, markup: Option, features: Vec, style: Vec<&'fm str>, title: &'fm str, description: Option<&'fm str>, published: Option, skip_path: Option, } impl<'fm> TemplateVariables<'fm> { /// Grabs variables from the [`Frontmatter`], filling in defaults /// if a variable is required and missing, or if it fails to parse. pub fn build(safe: SafePath<'fm>, fm: &'fm Frontmatter) -> Self { let filename = safe.file_name().unwrap_or("unknown"); Self { template: fm.get("template"), markup: fm .get("markup") .map(|s| MarkupStyle::from_str(s).ok()) .flatten(), features: fm .get_many("features") .into_iter() .filter_map(|s| Feature::from_str(s).ok()) .collect(), title: fm.get("title").unwrap_or_else(|| filename), style: fm.get_many("style"), description: fm.get("description"), published: fm .get("published") .map(|raw| timeparse::parse(raw).ok()) .flatten(), skip_path: fm.get("skip-path").map(|s| s.parse().ok()).flatten(), } } pub fn template(&self) -> Option<&str> { self.template } pub fn features(&self) -> &[Feature] { &self.features } /// Apply `style` varaibles pub fn apply_styles(&self, doc: &mut Document) { if let Some(styles) = doc.get_pattern("styles") { self.style.iter().for_each(|style| { let mut cloned = styles.clone(); cloned.set("style", style); doc.set_pattern(cloned); }); } } /// Apply the page's `title` pub fn apply_title(&self, doc: &mut Document) { doc.set("title", self.title); } /// Use the `markup` variable to determine how to process the content of /// the template pub fn apply_markup(&self, doc: &mut Document, content: &str) -> String { match self.markup { None | Some(MarkupStyle::Legacy) => { let markedup = markup::legacy::process(&content); markedup.fill_document(doc); markedup.into_content() } Some(MarkupStyle::Breakdown) => unimplemented!(), } } /// Apply the `published` variable pub fn apply_published(&self, doc: &mut Document) { if let Some(mut published_pattern) = doc.get_pattern("published") { if let Some(published) = self.published.as_ref() { let published_human = timeparse::format::human_long(published); let published_machine = timeparse::format::iso8601(published); variables!(published_pattern, published_human, published_machine); doc.set_pattern(published_pattern); } } } /// Apply path-based navigation pub fn apply_pathnav(&self, doc: &mut Document, path: &RequestPath) { let webpath = path.web_path(); let mut components: Vec<&str> = webpath.split('/').filter(|s| !s.is_empty()).collect(); // we don't want the name itself components.pop(); if let Some(path_pattern) = doc.get_pattern("pathnav") { let mut home = path_pattern.clone(); home.set("pathnav_link", "/"); home.set("pathnav_name", "home"); doc.set_pattern(home); let count = components.len().saturating_sub(self.skip_path.unwrap_or(0)); let mut link = String::from("/"); for part in components.iter().take(count) { link.push_str(&format!("{part}/")); let mut pat = path_pattern.clone(); pat.set("pathnav_link", &link); pat.set("pathnav_name", part); doc.set_pattern(pat); } } } /// Apply opengraph tag related varaibles pub fn apply_opengraph(&self, doc: &mut Document, settings: &Settings, path: &RequestPath) { if let Some(desc) = self.description { let og_title = self.title; let og_site_name = settings.hostname(); let og_url = format!("https://{}{}", settings.hostname(), path.web_path()); let og_description = desc; variables!(doc, og_title, og_url, og_site_name, og_description); } } /// Parse the provided content as a template and apply selected features. pub async fn apply_features(&self, content: &str, state: &SplormState) -> String { let mut doc = Document::from_str(&content, Options::default()).unwrap(); for feat in &self.features { match feat { Feature::SystemStats => features::apply_system_stats(&mut doc, state).await, } } doc.compile() } } /// markup style a template is using pub enum MarkupStyle { Legacy, Breakdown, } impl FromStr for MarkupStyle { type Err = &'static str; fn from_str(s: &str) -> Result { match s { "legacy" => Ok(MarkupStyle::Legacy), "breakdown" => Ok(MarkupStyle::Breakdown), _ => Err("invalid markup style"), } } }