import { type DocumentChild, getTextOfRange, parseDocument, } from "@lib/documents.ts"; import { parse, stringify } from "yaml"; import { createCrud } from "@lib/crud.ts"; import { extractHashTags } from "@lib/string.ts"; import { Ingredient, IngredientGroup } from "@lib/recipeSchema.ts"; import { fixRenderedMarkdown } from "@lib/helpers.ts"; import { parseIngredients } from "@lib/parseIngredient.ts"; export type Recipe = { type: "recipe"; id: string; name: string; description?: string; markdown?: string; ingredients: (Ingredient | IngredientGroup)[]; instructions?: string[]; notes?: string[]; tags: string[]; meta?: { time?: string; link?: string; image?: string; rating?: number; portion?: number; author?: string; average?: string; thumbnail?: string; }; }; function extractSteps( content: string, seperator: RegExp = /\n(?=\d+\.)/g, ): string[] { const steps = content.split(seperator).map((step) => { const match = step.match(/^(\d+)\.\s*(.*)/); if (match) return match[2]; return step; }).filter((step) => !!step); return steps as string[]; } export function parseRecipe(original: string, id: string): Recipe { const doc = parseDocument(original); let name = ""; let meta: Recipe["meta"] = {}; const groups: DocumentChild[][] = []; let group: DocumentChild[] = []; for (const child of doc.children) { if (child.type === "yaml") { try { meta = parse(child.value) as Recipe["meta"]; } catch (err) { console.log("Error parsing YAML", err); } continue; } if ( child.type === "heading" && child.depth === 1 && !name && child.children.length === 1 && child.children[0].type === "text" ) { name = child.children[0].value; continue; } if (child.type === "thematicBreak") { groups.push(group); group = []; continue; } group.push(child); } if (group.length) { groups.push(group); } let description = getTextOfRange(groups[0], original); let ingredientsText = getTextOfRange(groups[1], original); if (ingredientsText) { ingredientsText = ingredientsText.replace(/#+\s?Ingredients?/, ""); } else { ingredientsText = ""; } const ingredients = parseIngredients(ingredientsText); const instructionText = getTextOfRange(groups[2], original); let instructions = extractSteps(instructionText || ""); if (instructions.length <= 1) { const d = extractSteps(instructionText || "", /\n/g); if (d.length > instructions.length) { instructions = d; } } const tags = extractHashTags(description || ""); if (description) { for (const tag of tags) { description = description.replace("#" + tag, ""); } } return { type: "recipe", id, meta, name, tags, markdown: original, notes: getTextOfRange(groups[3], original)?.split("\n"), description, ingredients, instructions, }; } function filterUndefinedFromObject( obj: T, ) { return Object.fromEntries( Object.entries(obj).filter(([_, v]) => v !== undefined), ); } export function renderRecipe(recipe: Recipe) { const meta = filterUndefinedFromObject(recipe.meta || {}); // Clean up meta properties delete meta.thumbnail; delete meta.average; const recipeImage = meta.image ? `![](${meta.image})` : ""; // Format ingredient groups and standalone ingredients const ingredients = recipe.ingredients .map((item) => { if ("items" in item) { return `\n*${item.name}*\n${ item.items .map((ing) => { if (ing.quantity && ing.unit) { return `- **${ing.quantity.trim() || ""}${ ing.unit.trim() || "" }** ${ing.name}`; } return `- ${ing.name}`; }) .join("\n") }`; } if (item.quantity && item.unit) { return `- **${item.quantity?.trim() || ""}${ item.unit?.trim() || "" }** ${item.name}`; } if (item.quantity) { return `- **${item.quantity}** ${item.name}`; } return `- ${item.name}`; }) .join("\n"); // Format instructions as a numbered list const instructions = recipe.instructions ? recipe.instructions.map((step, i) => `${i + 1}. ${step}`).join("\n") : ""; // Render the final markdown return fixRenderedMarkdown(`${ Object.keys(meta).length ? `--- ${stringify(meta)} ---` : `--- ---` } # ${recipe.name} ${recipe.meta?.image ? recipeImage : ""} ${recipe.tags.map((t) => `#${t.replaceAll(" ", "-")}`).join(" ")} ${recipe.description || ""} --- ${ingredients ? `## Ingredients\n\n${ingredients}\n\n---\n` : ""} ${instructions ? `${instructions}\n\n---` : ""} ${recipe.notes?.length ? `\n${recipe.notes.join("\n")}` : ""} `); } const crud = createCrud({ prefix: `Recipes/`, parse: parseRecipe, render: renderRecipe, hasThumbnails: true, }); export const getAllRecipes = crud.readAll; export const getRecipe = crud.read; export const updateRecipe = crud.update; export const createRecipe = crud.create;