cookcli 0.28.0

A command-line interface for managing and working with Cooklang recipes
Documentation
use askama::Template;
use fluent_templates::Loader;
use serde::{Deserialize, Serialize};
use unic_langid::LanguageIdentifier;

/// Helper struct for translations in templates
#[derive(Clone, Debug, Serialize)]
pub struct Tr {
    #[serde(skip)]
    lang: LanguageIdentifier,
}

impl Tr {
    pub fn new(lang: LanguageIdentifier) -> Self {
        Self { lang }
    }

    pub fn t(&self, key: &str) -> String {
        crate::server::i18n::LOCALES.lookup(&self.lang, key)
    }

    pub fn lang_string(&self) -> String {
        self.lang.to_string()
    }
}

mod filters {
    use askama::Result;
    use url::Url;

    pub fn hostname(url: &str) -> Result<String> {
        Ok(Url::parse(url)
            .ok()
            .and_then(|u| u.host_str().map(String::from))
            .unwrap_or_else(|| url.to_string()))
    }
}

#[derive(Template)]
#[template(path = "error.html")]
pub struct ErrorTemplate {
    pub active: String,
    pub error_message: String,
    pub tr: Tr,
    pub prefix: String,
}

pub struct TodaysMenu {
    pub menu_name: String,
    pub menu_path: String,
    pub date_display: String,
}

#[derive(Template)]
#[template(path = "recipes.html")]
pub struct RecipesTemplate {
    pub active: String,
    pub current_name: String,
    pub breadcrumbs: Vec<Breadcrumb>,
    pub items: Vec<RecipeItem>,
    pub todays_menu: Option<TodaysMenu>,
    pub tr: Tr,
    pub prefix: String,
}

#[derive(Template)]
#[template(path = "recipe.html")]
pub struct RecipeTemplate {
    pub active: String,
    pub recipe: RecipeData,
    pub recipe_path: String,
    pub breadcrumbs: Vec<String>,
    pub scale: f64,
    pub tags: Vec<String>,
    pub ingredients: Vec<IngredientData>,
    pub cookware: Vec<CookwareData>,
    pub sections: Vec<RecipeSection>,
    pub image_path: Option<String>,
    pub tr: Tr,
    pub prefix: String,
}

impl RecipeTemplate {
    /// Build JSON data for the cooking mode feature.
    /// This is called from the template to embed structured recipe data.
    pub fn cooking_mode_json(&self) -> String {
        let sections: Vec<serde_json::Value> = self
            .sections
            .iter()
            .map(|section| {
                let ingredients: Vec<serde_json::Value> = section
                    .cooking_mode_ingredients
                    .iter()
                    .map(|ing| {
                        serde_json::json!({
                            "name": ing.name,
                            "quantity": ing.quantity,
                            "unit": ing.unit,
                            "note": ing.note,
                        })
                    })
                    .collect();

                let steps: Vec<serde_json::Value> = section
                    .items
                    .iter()
                    .filter_map(|item| match item {
                        RecipeSectionItem::Step(step) => {
                            let step_ingredients: Vec<serde_json::Value> = step
                                .ingredients
                                .iter()
                                .map(|ing| {
                                    serde_json::json!({
                                        "name": ing.name,
                                        "quantity": ing.quantity,
                                        "unit": ing.unit,
                                        "note": ing.note,
                                    })
                                })
                                .collect();

                            Some(serde_json::json!({
                                "type": "step",
                                "number": step.number,
                                "globalNumber": section.step_offset + step.number,
                                "image": step.image_path,
                                "ingredients": step_ingredients,
                            }))
                        }
                        RecipeSectionItem::Note(_) => None,
                    })
                    .collect();

                serde_json::json!({
                    "name": section.name,
                    "stepOffset": section.step_offset,
                    "ingredients": ingredients,
                    "steps": steps,
                })
            })
            .collect();

        let data = serde_json::json!({
            "name": self.recipe.name,
            "scale": self.scale,
            "image": self.image_path,
            "sections": sections,
        });

        // Escape </script> sequences to prevent premature script tag closing
        serde_json::to_string(&data)
            .map(|s| s.replace("</", "<\\/"))
            .unwrap_or_else(|_| "{}".to_string())
    }
}

#[derive(Template)]
#[template(path = "menu.html")]
pub struct MenuTemplate {
    pub active: String,
    pub name: String,
    pub recipe_path: String,
    pub breadcrumbs: Vec<String>,
    pub scale: f64,
    pub metadata: Option<RecipeMetadata>,
    pub sections: Vec<MenuSection>,
    pub image_path: Option<String>,
    pub tr: Tr,
    pub prefix: String,
}

#[derive(Template)]
#[template(path = "shopping_list.html")]
pub struct ShoppingListTemplate {
    pub active: String,
    pub tr: Tr,
    pub prefix: String,
}

#[derive(Template)]
#[template(path = "preferences.html")]
pub struct PreferencesTemplate {
    pub active: String,
    pub aisle_path: String,
    pub pantry_path: String,
    pub base_path: String,
    pub version: String,
    pub tr: Tr,
    pub sync_enabled: bool,
    pub sync_logged_in: bool,
    pub sync_email: Option<String>,
    pub sync_syncing: bool,
    pub prefix: String,
}

#[derive(Template)]
#[template(path = "pantry.html")]
pub struct PantryTemplate {
    pub active: String,
    pub configured: bool,
    pub sections: Vec<PantrySection>,
    pub tr: Tr,
    pub prefix: String,
}

#[derive(Template)]
#[template(path = "edit.html")]
pub struct EditTemplate {
    pub active: String,
    pub recipe_name: String,
    pub recipe_path: String,
    pub content: String,
    pub base_path: String,
    pub tr: Tr,
    pub prefix: String,
}

#[derive(Template)]
#[template(path = "new.html")]
pub struct NewTemplate {
    pub active: String,
    pub tr: Tr,
    pub error: Option<String>,
    pub filename: Option<String>,
    pub prefix: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct PantrySection {
    pub name: String,
    pub items: Vec<PantryItem>,
}

#[derive(Debug, Clone, Serialize)]
pub struct PantryItem {
    pub name: String,
    pub quantity: Option<String>,
    pub bought: Option<String>,
    pub expire: Option<String>,
    pub low: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Breadcrumb {
    pub name: String,
    pub path: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecipeItem {
    pub name: String,
    pub path: String,
    pub is_directory: bool,
    pub count: Option<usize>,
    pub description: Option<String>,
    pub tags: Vec<String>,
    pub image_path: Option<String>,
    pub is_menu: bool,
}

#[derive(Debug, Clone, Serialize)]
pub struct RecipeData {
    pub name: String,
    pub metadata: Option<RecipeMetadata>,
}

#[derive(Debug, Clone, Serialize)]
pub struct RecipeMetadata {
    pub servings: Option<String>,
    pub time: Option<String>,
    pub difficulty: Option<String>,
    pub course: Option<String>,
    pub prep_time: Option<String>,
    pub cook_time: Option<String>,
    pub cuisine: Option<String>,
    pub diet: Option<String>,
    pub author: Option<String>,
    pub description: Option<String>,
    pub source: Option<String>,
    pub source_url: Option<String>,
    pub custom: Vec<(String, String)>,
}

#[derive(Debug, Clone, Serialize)]
pub struct IngredientData {
    pub name: String,
    pub quantity: Option<String>,
    pub unit: Option<String>,
    /// Preparation note from Cooklang shorthand notation (e.g., "@tomatoes{2}(diced)" -> "diced")
    pub note: Option<String>,
    pub reference_path: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct CookwareData {
    pub name: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct RecipeSection {
    pub name: Option<String>,
    pub items: Vec<RecipeSectionItem>,
    pub step_offset: usize,
    pub ingredients: Vec<IngredientData>,
    /// Uncombined ingredients for cooking mode mise en place, sorted by aisle order.
    /// Each ingredient occurrence appears separately (not merged by name).
    pub cooking_mode_ingredients: Vec<IngredientData>,
}

#[derive(Debug, Clone, Serialize)]
pub enum RecipeSectionItem {
    Step(StepData),
    Note(String),
}

#[derive(Debug, Clone, Serialize)]
pub struct StepData {
    pub number: usize,
    pub items: Vec<StepItem>,
    pub ingredients: Vec<StepIngredient>,
    pub image_path: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct StepIngredient {
    pub name: String,
    pub quantity: Option<String>,
    pub unit: Option<String>,
    /// Preparation note from Cooklang shorthand notation (e.g., "@tomatoes{2}(diced)" -> "diced")
    pub note: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub enum StepItem {
    Text(String),
    Ingredient {
        name: String,
        reference_path: Option<String>,
    },
    Cookware(String),
    Timer(String),
    Quantity(String),
    LineBreak,
}

#[derive(Debug, Clone, Serialize)]
pub struct MenuSection {
    pub name: Option<String>,
    pub lines: Vec<Vec<MenuSectionItem>>,
}

#[derive(Debug, Clone, Serialize)]
pub enum MenuSectionItem {
    Text(String),
    RecipeReference {
        name: String,
        scale: Option<f64>,
    },
    Ingredient {
        name: String,
        quantity: Option<String>,
        unit: Option<String>,
    },
}