#![doc = include_str!("../README.doll")]
#![warn(
clippy::pedantic,
clippy::allow_attributes_without_reason,
missing_docs
)]
#![allow(clippy::missing_errors_doc, reason = "a lot of ")]
pub use ::capturing_glob::{Entry, Pattern};
use {
::capturing_glob::{glob_with, MatchOptions},
::miette::{Diagnostic, NamedSource, SourceSpan},
::std::{
collections::HashSet,
fs,
path::{Path, PathBuf},
},
::strfmt::{strfmt_map, DisplayStr, FmtError, Formatter},
::tracing::{debug_span, error, info_span, instrument, Level},
};
#[cfg(feature = "liquid")]
pub mod liquid;
#[cfg(feature = "minijinja")]
pub mod minijinja;
#[cfg(feature = "scss")]
pub mod scss;
#[cfg(feature = "wasm")]
pub mod wasm;
pub mod lang;
mod util;
#[::tyfling::debug(
"+ {:?}\n- {:?}\n> \"{dst}\"",
include.iter().map(ToString::to_string).collect::<Vec<_>>(),
exclude.iter().map(ToString::to_string).collect::<Vec<_>>()
)]
pub struct Rule<'a> {
pub include: &'a [Pattern],
pub exclude: &'a [Pattern],
pub dst: &'static str,
pub plan: &'a mut dyn FnMut(
PathBuf,
Vec<String>,
) -> Result<Box<dyn PlannedTransformation>, ErrorKind>,
}
pub trait PlannedTransformation: ::core::any::Any + ::core::fmt::Debug {
fn execute(self: Box<Self>, dst: PathBuf) -> Result<(), ErrorKind>;
}
impl PlannedTransformation for () {
fn execute(self: Box<Self>, _: PathBuf) -> Result<(), ErrorKind> {
Ok(())
}
}
impl PlannedTransformation for Vec<u8> {
#[instrument(skip(self), name = "write binary blob", level = Level::DEBUG)]
fn execute(self: Box<Self>, dst: PathBuf) -> Result<(), ErrorKind> {
fs::write(dst, *self).map_err(ErrorKind::Io)
}
}
impl PlannedTransformation for String {
#[instrument(skip(self), name = "write string data", level = Level::DEBUG)]
fn execute(self: Box<Self>, dst: PathBuf) -> Result<(), ErrorKind> {
fs::write(dst, self.as_bytes()).map_err(ErrorKind::Io)
}
}
impl PlannedTransformation for PathBuf {
#[instrument(name = "copy", level = Level::DEBUG)]
fn execute(self: Box<Self>, dst: PathBuf) -> Result<(), ErrorKind> {
fs::copy(*self, dst).map_err(ErrorKind::Io)?;
Ok(())
}
}
#[derive(Debug)]
pub struct Plan {
pub dst: PathBuf,
pub data: Box<dyn PlannedTransformation>,
}
pub fn run(rules: &mut [Rule<'_>]) -> Result<(), ErrorKind> {
execute(plan(rules)?)
}
#[instrument(skip(rules))]
pub fn plan(rules: &mut [Rule<'_>]) -> Result<Vec<Plan>, ErrorKind> {
let mut plans = Vec::new();
let mut visited = HashSet::new();
for (rule_index, rule) in rules.iter_mut().enumerate() {
let _span = debug_span!("rule", rule_index, ?rule).entered();
for (include_index, include) in rule.include.iter().enumerate() {
let _span =
debug_span!("include", include_index, include = include.to_string()).entered();
for entry in glob_with(
include.as_str(),
&MatchOptions {
case_sensitive: true,
require_literal_leading_dot: false,
require_literal_separator: true,
},
)
.map_err(|err| ErrorKind::Pattern {
label: [::miette::LabeledSpan::new_primary_with_span(
Some(err.msg.to_string()),
SourceSpan::new(err.pos.into(), 1),
)],
src: NamedSource::new(
format!("rules[{rule_index}].include[{include_index}]"),
include.to_string(),
),
})? {
let entry = entry?;
let src_file = entry.path();
let captures = {
let mut captures = Vec::new();
let mut i = 1; while let Some(capture) = entry.group(i) {
i += 1;
captures.push(
capture
.to_str()
.ok_or(ErrorKind::NonUTF8PathCharacters)?
.to_string(),
);
}
captures
};
let dst_file = format(rule.dst, &captures)?;
let dst_file = Path::new(&*dst_file);
let _span = info_span!(
"plan file",
src = src_file.to_str().unwrap(),
dst = dst_file.to_str().unwrap()
)
.entered();
if !src_file.is_file() {
error!("skipped (not a file)");
continue;
}
if visited.contains(src_file) {
error!("skipped (already visited)");
continue;
}
if rule.exclude.iter().any(|ignore| {
if ignore.matches_path(src_file) {
error!("skipped (matched ignore)");
true
} else {
false
}
}) {
continue;
}
plans.push(Plan {
dst: dst_file.to_path_buf(),
data: (rule.plan)(src_file.to_path_buf(), captures)?,
});
visited.insert(src_file.to_path_buf());
}
}
}
Ok(plans)
}
#[instrument(skip(plans))]
pub fn execute(plans: Vec<Plan>) -> Result<(), ErrorKind> {
for plan in plans {
fs::create_dir_all(plan.dst.parent().unwrap())?;
plan.data.execute(plan.dst)?;
}
Ok(())
}
pub fn format<T: AsRef<str>>(fmt: &str, captures: &[T]) -> Result<String, ErrorKind> {
Ok(strfmt_map(fmt, |mut fmt: Formatter| {
captures
.get(
fmt.key
.parse::<usize>()
.map_err(|_| FmtError::KeyError(format!("non-numeric key: \"{}\"", fmt.key)))?,
)
.ok_or_else(|| FmtError::KeyError(format!("key {} out of range", fmt.key)))?
.as_ref()
.display_str(&mut fmt)
})?)
}
#[instrument(level = Level::DEBUG)]
pub fn noop(_: PathBuf, _: Vec<String>) -> Result<Box<dyn PlannedTransformation>, ErrorKind> {
Ok(Box::new(()))
}
#[instrument(level = Level::DEBUG)]
pub fn copy(src: PathBuf, _: Vec<String>) -> Result<Box<dyn PlannedTransformation>, ErrorKind> {
Ok(Box::new(src))
}
#[derive(::thiserror::Error, ::miette::Diagnostic, Debug)]
pub enum ErrorKind {
#[error("pattern failure to compile")]
#[diagnostic(code(dollgen::glob::bad_pattern))]
Pattern {
#[label(collection)]
label: [::miette::LabeledSpan; 1],
#[source_code]
src: ::miette::NamedSource<String>,
},
#[error("glob failure")]
#[diagnostic(code(dollgen::glob::failure))]
Glob(
#[source]
#[from]
::capturing_glob::GlobError,
),
#[error("failure to parse format string")]
#[diagnostic(code(dollgen::format_str))]
Format(
#[source]
#[from]
::strfmt::FmtError,
),
#[cfg(feature = "liquid")]
#[error("liquid integration failure")]
#[diagnostic(code(dollgen::liquid))]
LiquidIntegration(
#[source]
#[from]
liquid::LiquidErrorKind,
),
#[cfg(feature = "minijinja")]
#[error("minijinja integration failure")]
#[diagnostic(code(dollgen::minijinja))]
MinijinjaIntegration(
#[source]
#[from]
minijinja::MinijinjaErrorKind,
),
#[cfg(feature = "scss")]
#[error("scss integration failure")]
#[diagnostic(code(dollgen::scss))]
SCSSIntegration {
#[label(collection)]
span: [::miette::LabeledSpan; 1],
#[source_code]
src: ::miette::NamedSource<String>,
},
#[cfg(feature = "wasm")]
#[error("wasm integration failure")]
#[diagnostic(code(dollgen::wasm))]
WASMIntegration(
#[source]
#[from]
wasm::WASMErrorKind,
),
#[error("template source lang failure")]
#[diagnostic(code(dollgen::lang))]
Lang(
#[source]
#[from]
lang::LangErrorKind,
),
#[error("fs error")]
#[diagnostic(code(dollgen::io))]
Io(
#[source]
#[from]
::std::io::Error,
),
#[error("non-utf8 path characters")]
#[diagnostic(code(dollgen::io::non_utf8_path))]
NonUTF8PathCharacters,
#[error("non-utf8 content")]
#[diagnostic(code(dollgen::io::non_utf8_content))]
NonUTF8Characters,
#[error("other")]
#[diagnostic(transparent)]
Other(#[source] Box<dyn Diagnostic + Send + Sync>),
}