use std::{
any::{TypeId, type_name},
borrow::Cow,
collections::{HashMap, HashSet},
fs,
sync::{Arc, LazyLock},
};
use camino::{Utf8Path, Utf8PathBuf};
use gray_matter::engine::{JSON, YAML};
use crate::{
FileData, GitRepo, Hash32, Item, LazyAssetError, Loader, LoaderError, loader::Loadable,
};
pub struct Content<T>
where
T: Send + Sync + 'static,
{
pub meta: T,
pub text: String,
}
pub fn glob_content<T>(
path_base: &'static str,
path_glob: &'static str,
preload: fn(&str) -> Result<(T, String), anyhow::Error>,
) -> Loader
where
T: Send + Sync + 'static,
{
Loader::with(move |init| LoaderContent::new(path_base, path_glob, preload, init.repo.clone()))
}
pub struct LoaderContent<T>
where
T: Send + Sync + 'static,
{
path_base: &'static str,
path_glob: &'static str,
preload: fn(&str) -> Result<(T, String), anyhow::Error>,
cached: HashMap<Utf8PathBuf, Item>,
repo: Option<Arc<GitRepo>>,
}
impl<T> LoaderContent<T>
where
T: Send + Sync + 'static,
{
pub(crate) fn new(
path_base: &'static str,
path_glob: &'static str,
preload: fn(&str) -> Result<(T, String), anyhow::Error>,
repo: Option<Arc<GitRepo>>,
) -> Self
where
T: Send + Sync + 'static,
{
Self {
path_base,
path_glob,
preload,
cached: HashMap::new(),
repo,
}
}
fn check_loaded(&self, path: &Utf8Path, hash: Hash32) -> bool {
match self.cached.get(path) {
Some(item) => item.hash == hash,
None => false,
}
}
fn read_file(&self, path: Utf8PathBuf) -> Result<Option<Item>, LoaderError> {
if path.is_dir() {
return Ok(None);
}
let bytes = fs::read(&path)?;
let hash = Hash32::hash(&bytes);
if self.check_loaded(&path, hash) {
return Ok(None);
}
let is_index = matches!(path.file_stem(), Some("index"));
let path_ext = path.extension();
let path_rel = path.strip_prefix(self.path_base).unwrap_or(&path);
let path_rel = if is_index {
path_rel.parent().unwrap_or(path_rel)
} else {
path_rel
};
let path_rel = match path_ext {
Some(ext) => path_rel.with_extension(ext),
None => path_rel.to_path_buf(),
};
let area = match path.file_stem() {
Some("index") => path.parent().unwrap_or(&path),
_ => &path,
};
let area = area.strip_prefix(self.path_base).unwrap_or(area);
let area = area.with_extension("");
Ok(Some(Item {
refl_type: TypeId::of::<Content<T>>(),
refl_name: type_name::<Content<T>>(),
id: path_rel.into_string().into(),
hash,
data: {
let preload = self.preload;
LazyLock::new(Box::new(move || {
let text = String::from_utf8(bytes).map_err(LazyAssetError::new)?;
let (meta, text) = preload(&text).map_err(LazyAssetError::new)?;
Ok(Arc::new(Content { meta, text }))
}))
},
file: Some(Arc::new(FileData {
info: self
.repo
.as_deref()
.and_then(|repo| repo.files.get(path.as_str()).cloned()),
file: path,
area,
})),
}))
}
}
impl<T> Loadable for LoaderContent<T>
where
T: Send + Sync + 'static,
{
fn name(&self) -> Cow<'static, str> {
Utf8Path::new(self.path_base)
.join(self.path_glob)
.to_string()
.into()
}
fn load(&mut self) -> Result<(), LoaderError> {
let pattern = Utf8Path::new(self.path_base).join(self.path_glob);
let mut vec = vec![];
for path in glob::glob(pattern.as_str())? {
let path = Utf8PathBuf::try_from(path?)?;
if let Some(item) = self.read_file(path.clone())? {
vec.push((path, item));
}
}
for (path, item) in vec {
self.cached.insert(path, item);
}
Ok(())
}
fn reload(&mut self, set: &HashSet<Utf8PathBuf>) -> Result<bool, LoaderError> {
let pattern = Utf8Path::new(self.path_base).join(self.path_glob);
let pattern = glob::Pattern::new(pattern.as_str())?;
let mut changed = false;
for path in set {
if !pattern.matches_path(path.as_std_path()) {
continue;
};
if let Some(item) = self.read_file(path.clone())? {
self.cached.insert(path.clone(), item);
changed |= true;
}
}
Ok(changed)
}
fn items(&self) -> Vec<&crate::Item> {
self.cached.values().collect()
}
fn path_base(&self) -> &'static str {
self.path_base
}
fn remove(&mut self, obsolete: &HashSet<Utf8PathBuf>) -> bool {
let before = self.cached.len();
self.cached.retain(|path, _| !obsolete.contains(path));
self.cached.len() < before
}
}
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."
)]
pub 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: LazyLock<Matter<$engine>> = 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!(yaml, YAML);
matter_parser!(json, JSON);