pub mod generic;
pub use generic::Document;
#[cfg(feature = "image")]
pub mod image;
#[cfg(feature = "image")]
pub use image::Image;
#[cfg(feature = "grass")]
pub mod css;
#[cfg(feature = "grass")]
pub use css::Stylesheet;
pub mod js;
pub use js::Script;
pub mod svelte;
pub use svelte::Svelte;
#[cfg(feature = "tokio")]
pub mod tokio;
use std::{collections::HashMap, fs};
use camino::{Utf8Path, Utf8PathBuf};
use glob::{Pattern, glob};
use gray_matter::engine::YAML;
use petgraph::graph::NodeIndex;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use crate::{
Hash32, TaskContext,
error::{BuildError, HauchiwaError},
graph::{Dynamic, TypedTask},
importmap::ImportMap,
};
#[derive(Debug)]
pub struct Assets<T> {
map: HashMap<camino::Utf8PathBuf, T>,
}
impl<T: Clone> Assets<T> {
pub fn get(&self, path: impl AsRef<Utf8Path>) -> Result<&T, HauchiwaError> {
self.map
.get(path.as_ref())
.ok_or(HauchiwaError::AssetNotFound(
path.as_ref().to_string().into(),
))
}
pub fn values(&self) -> std::collections::hash_map::Values<'_, Utf8PathBuf, T> {
self.map.values()
}
pub fn glob(&self, pattern: &str) -> Result<Vec<(&Utf8PathBuf, &T)>, HauchiwaError> {
let matcher = Pattern::new(pattern)?;
let matches: Vec<_> = self
.map
.iter()
.filter(|(path, _)| matcher.matches(path.as_str()))
.collect();
Ok(matches)
}
}
pub struct Input {
pub path: Utf8PathBuf,
pub(crate) hash: Hash32,
}
impl Input {
pub fn read(&self) -> std::io::Result<Box<[u8]>> {
fs::read(&self.path).map(Into::into)
}
}
#[derive(Clone)]
pub struct Store {
pub(crate) imports: ImportMap,
}
impl Store {
pub fn new() -> Self {
Self {
imports: ImportMap::new(),
}
}
pub fn save(&self, data: &[u8], ext: &str) -> Result<Utf8PathBuf, BuildError> {
let hash = Hash32::hash(data);
let hash = hash.to_hex();
let path_temp = Utf8Path::new(".cache/hash").join(&hash);
let path_dist = Utf8Path::new("dist/hash").join(&hash).with_extension(ext);
let path_root = Utf8Path::new("/hash/").join(&hash).with_extension(ext);
if !path_temp.exists() {
fs::create_dir_all(".cache/hash")?;
fs::write(&path_temp, data)?;
}
let dir = path_dist.parent().unwrap_or(&path_dist);
fs::create_dir_all(dir)?;
fs::copy(&path_temp, &path_dist)?;
Ok(path_root)
}
pub fn register(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.imports.register(key, value);
}
}
impl Default for Store {
fn default() -> Self {
Self::new()
}
}
type GlobCallback<G, R> = Box<
dyn Fn(&TaskContext<G>, &mut Store, Input) -> anyhow::Result<(Utf8PathBuf, R)> + Send + Sync,
>;
pub struct GlobAssetsTask<G, R>
where
G: Send + Sync + 'static,
R: Send + Sync + 'static,
{
glob_entry: Vec<&'static str>,
glob_watch: Vec<Pattern>,
callback: GlobCallback<G, R>,
}
impl<G, R> GlobAssetsTask<G, R>
where
G: Send + Sync + 'static,
R: Send + Sync + 'static,
{
pub fn new<F>(
glob_entry: Vec<&'static str>,
glob_watch: Vec<&'static str>,
callback: F,
) -> Result<Self, HauchiwaError>
where
F: Fn(&TaskContext<G>, &mut Store, Input) -> anyhow::Result<(Utf8PathBuf, R)>
+ Send
+ Sync
+ 'static,
{
Ok(Self {
glob_entry: glob_entry.to_vec(),
glob_watch: glob_watch
.into_iter()
.map(Pattern::new)
.collect::<Result<_, _>>()?,
callback: Box::new(callback),
})
}
}
impl<G, R> TypedTask<G> for GlobAssetsTask<G, R>
where
G: Send + Sync + 'static,
R: Send + Sync + 'static,
{
type Output = Assets<R>;
fn get_name(&self) -> String {
self.glob_entry.join(", ")
}
fn dependencies(&self) -> Vec<NodeIndex> {
vec![]
}
fn execute(
&self,
context: &TaskContext<G>,
runtime: &mut Store,
_: &[Dynamic],
) -> anyhow::Result<Self::Output> {
let mut paths = Vec::new();
for glob_entry in &self.glob_entry {
for path in glob(glob_entry)? {
paths.push(Utf8PathBuf::try_from(path?)?);
}
}
let results: anyhow::Result<Vec<_>> = paths
.into_par_iter()
.map(|path| {
let hash = Hash32::hash_file(&path)?;
let file = Input { path, hash };
let mut rt = Store::new();
let (out_path, res) = (self.callback)(context, &mut rt, file)?;
Ok((out_path, res, rt.imports))
})
.collect();
let mut registry = HashMap::new();
for (path, res, imports) in results? {
registry.insert(path, res);
runtime.imports.merge(imports);
}
let registry = Assets { map: registry };
Ok(registry)
}
fn is_dirty(&self, path: &Utf8Path) -> bool {
self.glob_watch.iter().any(|p| p.matches(path.as_str()))
}
}
macro_rules! matter_parser {
($name:ident, $engine:path) => {
#[doc = concat!(
"This function can be used to extract metadata from a document with `D` as the frontmatter shape.\n",
"Configured to use [`", stringify!($engine), "`] as the engine of the parser."
)]
fn $name<D>(content: &str) -> Result<(D, String), anyhow::Error>
where
D: for<'de> serde::Deserialize<'de> + Send + Sync + 'static,
{
use gray_matter::{Matter, Pod};
static PARSER: std::sync::LazyLock<Matter<$engine>> = std::sync::LazyLock::new(Matter::<$engine>::new);
let entity = PARSER.parse(content)?;
let object = entity
.data
.unwrap_or_else(Pod::new_hash)
.deserialize::<D>()
.map_err(|e| anyhow::anyhow!("Malformed frontmatter:\n{e}"))?;
Ok((
object,
entity.content,
))
}
};
}
matter_parser!(parse_yaml, YAML);