use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
use codas::types::Text;
mod asset;
pub use asset::{Asset, MediaCategory, MediaType};
pub mod canonicalize;
pub mod favicon;
pub mod image;
pub mod js_bundle;
pub mod markdown;
pub mod minify_html;
pub mod minify_js;
pub mod scss;
pub mod template;
#[derive(Debug)]
pub struct Environment {
pub source_root: PathBuf,
pub kit_imports: BTreeMap<String, PathBuf>,
pub asset_outputs: BTreeMap<String, String>,
}
#[cfg(test)]
impl Environment {
pub fn test() -> Self {
Self {
source_root: std::path::PathBuf::from("."),
kit_imports: Default::default(),
asset_outputs: BTreeMap::new(),
}
}
}
pub trait ProcessesAssets {
fn process(
&self,
env: &Environment,
context: &LayeredContext,
asset: &mut Asset,
) -> Result<bool, ProcessingError>;
}
pub type Context = BTreeMap<Text, ContextValue>;
pub type AssetMetadata = Context;
pub struct LayeredContext {
base: Arc<Context>,
overlays: Vec<Context>,
}
impl LayeredContext {
pub fn new(base: Arc<Context>) -> Self {
Self {
base,
overlays: Vec::new(),
}
}
pub fn from_flat(context: Context) -> Self {
Self::new(Arc::new(context))
}
pub fn get(&self, key: &Text) -> Option<&ContextValue> {
for overlay in self.overlays.iter().rev() {
if let Some(v) = overlay.get(key) {
return Some(v);
}
}
self.base.get(key)
}
pub fn push_layer(&mut self) {
self.overlays.push(Context::new());
}
pub fn pop_layer(&mut self) -> Option<Context> {
self.overlays.pop()
}
pub fn insert(&mut self, key: Text, value: ContextValue) {
self.overlays
.last_mut()
.expect("insert requires at least one overlay")
.insert(key, value);
}
pub fn remove(&mut self, key: &Text) -> Option<ContextValue> {
for overlay in self.overlays.iter_mut().rev() {
if let Some(v) = overlay.remove(key) {
return Some(v);
}
}
None
}
pub fn extend_top(&mut self, entries: Context) {
self.overlays
.last_mut()
.expect("extend_top requires at least one overlay")
.extend(entries);
}
pub fn fill(&mut self, entries: Context) {
for (key, value) in entries {
if self.get(&key).is_none() {
self.insert(key, value);
}
}
}
pub fn insert_layer_below_top(&mut self, layer: Context) {
let top = self.overlays.pop();
self.overlays.push(layer);
if let Some(top) = top {
self.overlays.push(top);
}
}
pub fn child_scope(&self) -> Self {
let mut child = Self {
base: Arc::clone(&self.base),
overlays: self.overlays.clone(),
};
child.push_layer();
child
}
pub fn iter_by_prefix<'a>(
&'a self,
prefix: &'a str,
) -> impl Iterator<Item = (&'a Text, &'a ContextValue)> + 'a {
let start: Text = prefix.into();
self.base
.range(start..)
.take_while(move |(k, _)| k.as_str().starts_with(prefix))
}
pub fn resolve(&self, identifier: &str) -> Option<&ContextValue> {
if !identifier.contains('.') {
let key: Text = identifier.into();
return self.get(&key);
}
let mut segments = identifier.split('.');
let first: Text = segments.next()?.into();
let mut current = self.get(&first)?;
for segment in segments {
match current {
ContextValue::Table(table) => {
let key: Text = segment.into();
current = table.get(&key)?;
}
_ => return None,
}
}
Some(current)
}
}
#[derive(Debug, Clone)]
pub enum ContextValue {
Text(Text),
List(Vec<ContextValue>),
Table(Context),
AssetRef(Text),
}
pub fn context_from_toml(table: toml::Table) -> Result<Context, ProcessingError> {
let mut context = Context::default();
for (key, value) in table {
context.insert(key.into(), ContextValue::from_toml(value)?);
}
Ok(context)
}
pub const FRONTMATTER_DELIMITER: &str = "***";
const FRONTMATTER_DELIMITER_LINE: &str = "\n***\n";
const FRONTMATTER_DELIMITER_START: &str = "***\n";
pub fn extract_frontmatter(text: &Text) -> (&str, Option<Context>) {
let split_pos = if text.starts_with(FRONTMATTER_DELIMITER_START) {
Some(0)
} else {
text.find(FRONTMATTER_DELIMITER_LINE)
};
let Some(pos) = split_pos else {
return (text.as_str(), None);
};
let frontmatter = &text[..pos];
let body_start = if pos == 0 {
FRONTMATTER_DELIMITER_START.len()
} else {
pos + FRONTMATTER_DELIMITER_LINE.len() - 1
};
let body = &text[body_start..];
let table: toml::Table = match toml::from_str(frontmatter) {
Ok(t) => t,
Err(_) => return (text, None),
};
match context_from_toml(table) {
Ok(parsed) => (body, Some(parsed)),
Err(_) => (text, None),
}
}
impl ContextValue {
pub fn from_toml(value: toml::Value) -> Result<Self, ProcessingError> {
match value {
toml::Value::String(s) => Ok(ContextValue::Text(s.into())),
toml::Value::Integer(n) => Ok(ContextValue::Text(n.to_string().into())),
toml::Value::Float(n) => Ok(ContextValue::Text(n.to_string().into())),
toml::Value::Boolean(b) => Ok(ContextValue::Text(b.to_string().into())),
toml::Value::Array(arr) => {
let items: Result<Vec<ContextValue>, _> =
arr.into_iter().map(ContextValue::from_toml).collect();
Ok(ContextValue::List(items?))
}
toml::Value::Table(table) => Ok(ContextValue::Table(context_from_toml(table)?)),
toml::Value::Datetime(dt) => Ok(ContextValue::Text(dt.to_string().into())),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum ProcessingError {
NonTextual,
NonBinary,
Malformed { message: Text },
Compilation { message: Text },
}