use crate::error::{
ConfigError, ConfigErrorKind, KrikError, KrikResult, ThemeError, ThemeErrorKind,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tera::{Context, Tera};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThemeConfig {
pub name: String,
pub version: String,
pub author: Option<String>,
pub description: Option<String>,
pub templates: HashMap<String, String>,
}
#[derive(Debug)]
pub struct Theme {
pub config: ThemeConfig,
pub templates: Tera,
pub theme_path: PathBuf,
}
impl Theme {
pub fn builder() -> ThemeBuilder {
ThemeBuilder::new()
}
pub fn load_from_path<P: AsRef<Path>>(theme_path: P) -> KrikResult<Self> {
Theme::builder()
.theme_path(theme_path)
.autoescape_html(false)
.enable_reload(false)
.build()
}
fn default_config() -> String {
r#"
name = "default"
version = "1.0.0"
description = "Default Krik theme"
[templates]
page = "page"
post = "post"
index = "index"
"#
.to_string()
}
pub fn default_templates() -> Tera {
let themes_path = PathBuf::from("themes/default/templates");
if themes_path.exists() {
match Tera::new(&format!("{}/**/*.html", themes_path.display())) {
Ok(mut tera) => {
tera.autoescape_on(vec![]);
return tera;
}
Err(_) => {
}
}
}
let mut tera = Tera::default();
tera.autoescape_on(vec![]);
tera
}
pub fn render_page(&self, template_name: &str, context: &Context) -> KrikResult<String> {
let template_with_ext = format!("{template_name}.html");
if let Ok(rendered) = self.templates.render(&template_with_ext, context) {
return Ok(rendered);
}
self.templates.render(template_name, context).map_err(|e| {
KrikError::Template(Box::new(crate::error::TemplateError {
kind: crate::error::TemplateErrorKind::RenderError(e),
template: template_name.to_string(),
context: "Rendering template via Theme::render_page".to_string(),
}))
})
}
pub fn try_reload_templates(&mut self) {
let templates_path = self.theme_path.join("templates");
if templates_path.exists() {
if let Ok(new_tera) = Tera::new(&format!("{}/**/*.html", templates_path.display())) {
let mut tera = new_tera;
tera.autoescape_on(vec![]);
self.templates = tera;
}
}
}
}
#[derive(Debug, Default)]
pub struct ThemeBuilder {
theme_path: Option<PathBuf>,
autoescape_html: bool,
enable_reload: bool,
}
impl ThemeBuilder {
fn new() -> Self {
Self {
theme_path: None,
autoescape_html: false,
enable_reload: false,
}
}
pub fn theme_path<P: AsRef<Path>>(mut self, path: P) -> Self {
self.theme_path = Some(path.as_ref().to_path_buf());
self
}
pub fn autoescape_html(mut self, enabled: bool) -> Self {
self.autoescape_html = enabled;
self
}
pub fn enable_reload(mut self, enabled: bool) -> Self {
self.enable_reload = enabled;
self
}
pub fn build(self) -> KrikResult<Theme> {
let theme_path = match self.theme_path {
Some(p) => p,
None => PathBuf::from("themes/default"),
};
let config_path = theme_path.join("theme.toml");
let config_content = match std::fs::read_to_string(&config_path) {
Ok(s) => s,
Err(_) => Theme::default_config(), };
let config: ThemeConfig = match toml::from_str(&config_content) {
Ok(cfg) => cfg,
Err(e) => {
return Err(KrikError::Theme(Box::new(ThemeError {
kind: ThemeErrorKind::InvalidConfig(ConfigError {
kind: ConfigErrorKind::InvalidToml(e),
path: Some(config_path.clone()),
context: "Parsing theme configuration".to_string(),
}),
theme_path: theme_path.clone(),
context: format!("Failed to parse {}", config_path.display()),
})));
}
};
let templates_path = theme_path.join("templates");
let mut templates = if templates_path.exists() {
match Tera::new(&format!("{}/**/*.html", templates_path.display())) {
Ok(t) => t,
Err(e) => {
return Err(KrikError::Theme(Box::new(ThemeError {
kind: ThemeErrorKind::AssetError(format!(
"Template compilation failed: {}",
e
)),
theme_path: theme_path.clone(),
context: "Compiling theme templates".to_string(),
})));
}
}
} else {
Theme::default_templates()
};
if self.autoescape_html {
} else {
templates.autoescape_on(vec![]);
}
let mut theme = Theme {
config,
templates,
theme_path,
};
if self.enable_reload {
theme.try_reload_templates();
}
Ok(theme)
}
}