use crate::web::templates::TodaysMenu;
use regex::Regex;
use std::sync::LazyLock;
static TIME_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\((\d{2}:\d{2})\)").unwrap());
static MEAL_HEADER_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\s*\(\d{2}:\d{2}\)\s*").unwrap());
pub fn find_todays_menu(
base_path: &camino::Utf8Path,
exclude: Option<&camino::Utf8Path>,
) -> Option<TodaysMenu> {
let now = chrono::Local::now();
let today = now.format("%Y-%m-%d").to_string();
let today_display = now.format("%A, %B %-d").to_string();
let menus = cooklang_find::list_menus_for_date(&[base_path], &today).unwrap_or_default();
let entry = menus.iter().find(|menu| match (exclude, menu.path()) {
(Some(dir), Some(path)) => !path.starts_with(dir),
_ => true,
})?;
let full_path = entry.path()?;
let relative = full_path
.strip_prefix(base_path)
.unwrap_or(full_path.as_ref());
let menu_name = entry.name().clone().unwrap_or_else(|| relative.to_string());
let menu_path = relative
.as_str()
.trim_end_matches(".cook")
.trim_end_matches(".menu")
.to_string();
Some(TodaysMenu {
menu_name,
menu_path,
date_display: today_display,
})
}
pub fn extract_time(header: &str) -> Option<String> {
TIME_RE.captures(header).map(|caps| caps[1].to_string())
}
pub fn extract_meal_type(header: &str) -> String {
let stripped = header.trim().trim_end_matches(':').trim();
MEAL_HEADER_RE.replace_all(stripped, "").trim().to_string()
}
pub fn is_meal_header(text: &str) -> bool {
let trimmed = text.trim();
if !trimmed.ends_with(':') {
return false;
}
let before_colon = trimmed.trim_end_matches(':').trim();
!before_colon.is_empty()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn find_todays_menu_matches_section_with_today() {
let temp = TempDir::new().unwrap();
let dir = camino::Utf8Path::from_path(temp.path()).unwrap();
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
let content = format!("= Day 1 ({today})\n\nBreakfast:\n- @eggs{{}}\n");
fs::write(dir.join("week.menu"), content).unwrap();
let result = find_todays_menu(dir, None);
assert!(result.is_some());
assert_eq!(result.unwrap().menu_path, "week");
}
#[test]
fn find_todays_menu_matches_bare_date_header() {
let temp = TempDir::new().unwrap();
let dir = camino::Utf8Path::from_path(temp.path()).unwrap();
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
let content = format!("= {today} Dinner\n\nBreakfast:\n- @eggs{{}}\n");
fs::write(dir.join("week.menu"), content).unwrap();
let result = find_todays_menu(dir, None);
assert!(result.is_some());
assert_eq!(result.unwrap().menu_path, "week");
}
#[test]
fn find_todays_menu_returns_none_when_no_section_matches_today() {
let temp = TempDir::new().unwrap();
let dir = camino::Utf8Path::from_path(temp.path()).unwrap();
let content = "= Day 1 (1999-01-01)\n\nBreakfast:\n- @eggs{}\n";
fs::write(dir.join("week.menu"), content).unwrap();
let result = find_todays_menu(dir, None);
assert!(result.is_none());
}
#[test]
fn find_todays_menu_skips_excluded_dir() {
let temp = TempDir::new().unwrap();
let dir = camino::Utf8Path::from_path(temp.path()).unwrap();
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
let out = dir.join("_site/menu");
fs::create_dir_all(&out).unwrap();
fs::write(out.join("week.menu"), format!("= Day 1 ({today})\n")).unwrap();
assert!(find_todays_menu(dir, None).is_some());
assert!(find_todays_menu(dir, Some(&dir.join("_site"))).is_none());
}
}