use {
crate::{util::with_added_extension_but_stable, ErrorKind, PlannedTransformation},
::core::cell::RefCell,
::hashbrown::{hash_map::EntryRef, HashMap},
::liquid::{object, Object, Parser, Template},
::serde::Deserialize,
::std::{
fs::{self, OpenOptions},
path::{Path, PathBuf},
rc::Rc,
},
::toml::from_str,
::tracing::{instrument, trace_span, Level},
};
pub extern crate liquid;
pub struct Liquid {
pub parser: Parser,
cache: HashMap<PathBuf, Rc<Template>>,
}
impl Liquid {
#[must_use]
pub fn new(parser: Parser) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Self {
parser,
cache: HashMap::new(),
}))
}
pub fn parse(&mut self, path: &Path) -> Result<Rc<Template>, ErrorKind> {
Ok(match self.cache.entry_ref(path) {
EntryRef::Occupied(entry) => entry.into_mut(),
EntryRef::Vacant(entry) => {
entry.insert(Rc::new(self.parser.parse_file(path).map_err(|err| {
let source_code = match fs::read_to_string(path) {
Ok(src) => src,
Err(err) => return ErrorKind::Io(err),
};
ErrorKind::LiquidIntegration(LiquidErrorKind::LiquidParsing(
err,
path.to_path_buf(),
source_code,
))
})?))
}
}
.clone())
}
pub fn clear_cache(&mut self) {
self.cache.clear();
}
}
#[derive(Deserialize, Debug)]
struct Frontmatter {
pub template: Option<FrontmatterTemplate>,
pub props: Option<liquid::Object>,
}
#[derive(Deserialize, Debug)]
struct FrontmatterTemplate {
pub path: Option<PathBuf>,
#[serde(default)]
pub local: bool,
}
#[must_use]
pub fn default_globals(_: PathBuf, props: Option<Object>, body: String) -> Object {
object!({
"body": body,
"props": props.unwrap_or_default(),
})
}
#[::tyfling::debug(.globals)]
pub struct LiquidPlan {
pub template: Rc<Template>,
pub globals: Object,
}
impl PlannedTransformation for LiquidPlan {
#[instrument(, name = "render liquid template", level = Level::DEBUG)]
fn execute(self: Box<Self>, dst: PathBuf) -> Result<(), ErrorKind> {
self.template
.render_to(
&mut OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.append(false)
.read(false)
.open(&dst)?,
&self.globals,
)
.map_err(|err| ErrorKind::LiquidIntegration(LiquidErrorKind::LiquidRendering(err, dst)))
}
}
pub fn create_templated(
default_template: PathBuf,
liquid: Rc<RefCell<Liquid>>,
mut globals: impl for<'a> FnMut(PathBuf, Option<Object>, String) -> Object,
mut lang: impl for<'a> FnMut(&'a str, &'a Path) -> Result<(String, String), ErrorKind>,
) -> impl FnMut(PathBuf, Vec<String>) -> Result<Box<dyn PlannedTransformation>, ErrorKind> {
move |src: PathBuf, _| {
let _span = trace_span!("templated liquid", ?default_template).entered();
let content = fs::read_to_string(&src)?;
let (frontmatter, body) = lang(&content, &src)?;
let frontmatter =
from_str::<Frontmatter>(&frontmatter).map_err(LiquidErrorKind::FrontmatterParsing)?;
let template = if let Some(template) = frontmatter.template {
with_added_extension_but_stable(
&if template.local {
if let Some(path) = template.path {
if path.is_absolute() {
return Err(LiquidErrorKind::FrontmatterAbsoluteLocalPath(path).into());
}
src.parent().unwrap().join(path)
} else {
src.with_extension("")
}
} else if let Some(path) = template.path {
path
} else {
default_template.clone()
},
"liquid",
)
} else {
default_template.clone()
};
Ok(Box::new(LiquidPlan {
template: liquid.borrow_mut().parse(&template)?,
globals: globals(src, frontmatter.props, body),
}))
}
}
pub fn create_standalone(
liquid: Rc<RefCell<Liquid>>,
mut globals: impl for<'a> FnMut(PathBuf) -> Object,
) -> impl FnMut(PathBuf, Vec<String>) -> Result<Box<dyn PlannedTransformation>, ErrorKind> {
move |src: PathBuf, _| {
let _span = trace_span!("standalone liquid").entered();
Ok(Box::new(LiquidPlan {
template: liquid.borrow_mut().parse(&src)?,
globals: globals(src),
}))
}
}
#[derive(::thiserror::Error, ::miette::Diagnostic, Debug)]
pub enum LiquidErrorKind {
#[error("template parsing failed for {}", .1.to_str().unwrap())]
#[diagnostic(code(dollgen::liquid::template_parse_failed))]
LiquidParsing(#[source] ::liquid::Error, PathBuf, #[source_code] String),
#[error("template rendering failed for {}", .1.to_str().unwrap())]
#[diagnostic(code(dollgen::liquid::template_parse_failed))]
LiquidRendering(#[source] ::liquid::Error, PathBuf),
#[error("frontmatter parsing failed")]
#[diagnostic(code(dollgen::liquid::frontmatter_parse_failed))]
FrontmatterParsing(#[source] ::toml::de::Error),
#[error("frontmatter requests a local template, but provides an absolute path")]
#[diagnostic(
code(dollgen::liquid::frontmatter_absolute_local_path),
help("either change to a relative path or remove the local attribute")
)]
FrontmatterAbsoluteLocalPath(PathBuf),
}