use std::{collections::HashMap, io, path::Path};
use minijinja::Value;
use walkdir::WalkDir;
use crate::{Blog, Metadata, Mime, html::Parsed};
use super::{CssRule, Dir, HasCssRules, HasNav, Mount, NavItem, Serve, Url};
#[derive(Debug, Default, Hash, serde::Serialize, serde::Deserialize)]
pub struct Page {
pub title: Option<String>,
pub subtitle: Option<String>,
}
impl Page {
fn template_data(&self) -> HashMap<&'static str, Value> {
let mut res = HashMap::new();
if let Some(title) = &self.title {
res.insert("title", title.into());
}
if let Some(subtitle) = &self.subtitle {
res.insert("subtitle", subtitle.into());
}
res
}
}
#[derive(Debug)]
pub struct Pages {
mount: Mount,
nav: Vec<NavItem>,
rules: Vec<CssRule>,
locale: Option<String>,
}
impl Pages {
const PREFIX: &str = r#"{% extends "page" %}{% block body %}"#;
const SUFFIX: &str = r#"{% endblock %}"#;
pub fn new(url: impl Into<Url>, dir: impl Into<Dir>) -> Self {
Self {
mount: Mount::new(url, dir),
nav: vec![],
rules: vec![],
locale: None,
}
}
pub fn locale(mut self, new: impl Into<String>) -> Self {
self.locale = Some(new.into());
self
}
}
impl HasNav for Pages {
fn get_nav(&mut self) -> &mut Vec<NavItem> {
&mut self.nav
}
}
impl HasCssRules for Pages {
fn get_rules(&mut self) -> &mut Vec<CssRule> {
&mut self.rules
}
}
impl Serve for Pages {
fn base(&self) -> &Url {
&self.mount.url
}
fn scan(&self, dir: &Path) -> io::Result<Metadata> {
let base = dir.join(&self.mount.dir.0);
let mut metadata = Metadata::default();
for item in WalkDir::new(&base).follow_root_links(true).follow_links(true) {
let item = item?;
if !item.file_type().is_file() {
continue;
}
let Some(name) = item.path().to_str() else {
continue;
};
if ![".html", ".jinja"].iter().any(|ext| name.ends_with(ext)) {
continue;
}
let dir = Dir(item.path().strip_prefix(dir).unwrap().to_path_buf());
let url = self.mount.url(&dir).unwrap();
let suffixless = if url.ends_with("index.html") {
url.parent().unwrap().to_owned()
} else {
url.with_extension("")
};
metadata.files.insert(Url(suffixless), Mime::HTML);
}
Ok(metadata)
}
fn render(&self, blog: &Blog, url: &Url, into: &mut dyn io::Write) -> io::Result<()> {
let in_dir = self
.mount
.dir(url)
.ok_or(io::Error::new(io::ErrorKind::NotFound, format!("not served: {url:?}")))?;
let in_base = blog.dir.join(&in_dir.0);
let file: Parsed<Page> = [in_base.with_extension("html"), in_base.join("index.html")]
.into_iter()
.filter_map(|p| Parsed::load(&p).ok())
.next()
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
format!("no file matching {in_base:?}"),
)
})?;
let page = file.extra();
let mut nb =
String::with_capacity(Self::PREFIX.len() + file.body().len() + Self::SUFFIX.len());
nb.push_str(Self::PREFIX);
nb.push_str(file.body());
nb.push_str(Self::SUFFIX);
let template = blog
.env
.template_from_named_str(url.str(), &nb)
.map_err(|e| io::Error::other(format!("invalid template: {e}")))?;
let mut value = blog.template_data();
if let Some(locale) = &self.locale {
value.insert("locale", Value::from(locale));
}
value.extend(page.template_data());
value.extend(file.template_data());
let stylesheets = blog.stylesheets(&self.rules);
value.insert("stylesheets", stylesheets.iter().flat_map(|u| u.to_str()).collect());
value.insert("nav", self.nav.iter().map(Value::from_serialize).collect());
let context = value.into_iter().collect::<Value>();
template
.render_captured_to(context, into)
.map_err(|e| io::Error::other(format!("couldn't render: {e}")))?;
Ok(())
}
}