use camino::{Utf8Path, Utf8PathBuf};
use cooklang::MetadataResult;
use cooklang_fs::RecipeEntry;
use minijinja::{context, Value};
use crate::{config::UiConfig, util::meta_name};
use super::AppState;
pub mod about;
pub mod convert_popover;
pub mod index;
pub mod open_editor;
pub mod recipe;
pub mod search;
pub mod sse_updates;
pub mod static_file;
pub use about::about;
pub use convert_popover::convert_popover;
pub use index::index;
pub use open_editor::open_editor;
pub use recipe::recipe;
pub use search::search;
pub use sse_updates::sse_updates;
pub use static_file::static_file;
macro_rules! ok_status {
($res:expr) => {
ok_status!($res, INTERNAL_SERVER_ERROR)
};
($res:expr, $status:ident) => {
match $res {
Ok(val) => val,
Err(err) => {
tracing::error!("Error in handler: {err}");
return axum::http::StatusCode::$status.into_response();
}
}
};
}
pub(crate) use ok_status;
macro_rules! mj_ok {
($res:expr) => {{
#[cfg(debug_assertions)]
match $res {
Ok(val) => val,
Err(err) => {
let mut s = format!("{err:#}");
let mut err = &err as &dyn std::error::Error;
while let Some(next_err) = err.source() {
s.push_str(&format!("\ncaused by: {next_err:#}"));
err = next_err;
}
let html = minijinja::render!("<h1>Error rendering template</h1><pre>{{ err|escape }}</pre>", err => s);
return Html(html).into_response();
}
}
#[cfg(not(debug_assertions))]
crate::serve::handlers::ok_status!($res, INTERNAL_SERVER_ERROR)
}};
}
pub(crate) use mj_ok;
pub fn check_path(p: &str) -> Result<(), axum::http::StatusCode> {
let path = camino::Utf8Path::new(p);
if !path.components().all(|c| match c {
camino::Utf8Component::Normal(comp) => {
let valid = Utf8Path::new(comp)
.components()
.all(|c| matches!(c, camino::Utf8Component::Normal(_)));
valid
}
_ => false,
}) {
return Err(axum::http::StatusCode::BAD_REQUEST);
}
Ok(())
}
fn clean_path(p: &Utf8Path, base_path: &Utf8Path) -> Utf8PathBuf {
let p = p
.strip_prefix(base_path)
.expect("dir entry path not relative to base path");
#[cfg(windows)]
let p = Utf8PathBuf::from(p.to_string().replace('\\', "/"));
#[cfg(not(windows))]
let p = p.to_path_buf();
p
}
fn recipe_entry_context(
r: RecipeEntry,
state: &AppState,
meta: Option<&MetadataResult>,
) -> Option<Value> {
let mut metadata = Value::UNDEFINED;
let mut error = false;
let mut image = None;
if let Some(m) = meta.and_then(|res| res.valid_output()) {
let tags = Value::from_iter(
m.tags()
.unwrap_or(&[])
.iter()
.map(|t| tag_context(t.as_str(), &state.config.ui)),
);
if let Some(external_image) = m.map.get("image") {
image = Some(external_image.clone());
}
let name = meta_name(m).unwrap_or(r.name()).to_string();
metadata = context! {
tags,
emoji => m.emoji(),
desc => m.description(),
name,
}
} else {
error = true;
}
if image.is_none() {
image = r
.images()
.iter()
.find(|i| i.indexes.is_none())
.map(|i| image_url(&i.path, &state.base_path));
}
let path = clean_path(r.path(), &state.base_path).with_extension("");
Some(context! {
fallback_name => r.name(),
href => format!("/r/{path}"),
error,
image,
..metadata,
})
}
fn image_url(path: &Utf8Path, base_path: &Utf8Path) -> String {
format!("/src/{}", clean_path(path, base_path))
}
fn tag_context(name: &str, ui_config: &UiConfig) -> Value {
let emoji = ui_config
.tags
.get(name)
.and_then(|c| c.emoji.as_deref())
.and_then(|s| {
if s.starts_with(':') && s.ends_with(':') {
emojis::get_by_shortcode(&s[1..s.len() - 1])
} else {
emojis::get(s)
}
})
.map(|e| e.as_str());
context! { emoji, name }
}
#[derive(Debug)]
struct Searcher {
tags: Vec<String>,
name_parts: Vec<String>,
}
impl Searcher {
fn matches_recipe(&self, name: &str, tags: &[String]) -> bool {
let name = name.to_lowercase();
for part in &self.name_parts {
if !name.contains(part) {
return false;
}
}
for tag in &self.tags {
if !tags.contains(tag) {
return false;
}
}
true
}
}