import { type DocumentChild, getTextOfChild, getTextOfRange, parseDocument, } from "@lib/documents.ts"; import { parse } from "yaml"; import { parseIngredient } from "https://esm.sh/parse-ingredient@1.0.1"; import { createCrud } from "@lib/crud.ts"; import { extractHashTags } from "@lib/string.ts"; export type IngredientGroup = { name: string; ingredients: Ingredient[]; }; export type Ingredient = { type: string; unit?: string; amount?: string; }; export type Ingredients = (Ingredient | IngredientGroup)[]; export type Recipe = { type: "recipe"; id: string; name: string; description?: string; ingredients: Ingredients; preparation?: string; tags: string[]; meta?: { time?: string; link?: string; image?: string; rating?: number; portion?: number; author?: string; average?: string; thumbnail?: string; }; }; function parseIngredientItem(listItem: DocumentChild): Ingredient | undefined { if (listItem.type === "listItem") { const children: DocumentChild[] = listItem.children[0]?.children || listItem.children; const text = children.map((c) => getTextOfChild(c)).join(" ").trim(); const ing = parseIngredient(text, { additionalUOMs: { tableSpoon: { short: "EL", plural: "Table Spoons", alternates: ["el", "EL", "Tbsp", "tbsp"], }, teaSpoon: { short: "TL", plural: "Tea Spoon", alternates: ["tl", "TL", "Tsp", "tsp", "teaspoon"], }, litre: { short: "L", plural: "liters", alternates: ["L", "l"], }, paket: { short: "Paket", plural: "Pakets", alternates: ["Paket", "paket"], }, }, }); return { type: ing[0].description, unit: ing[0].unitOfMeasure, amount: ing[0].quantity, }; } return; } const isIngredient = (item: Ingredient | undefined): item is Ingredient => { return !!item; }; function parseIngredientsList(list: DocumentChild): Ingredient[] { if (list.type === "list" && "children" in list) { return list.children.map((listItem) => { return parseIngredientItem(listItem); }).filter(isIngredient); } return []; } function parseIngredients(children: DocumentChild[]): Recipe["ingredients"] { const ingredients: (Ingredient | IngredientGroup)[] = []; if (!children) return []; let skip = false; for (let i = 0; i < children.length; i++) { if (skip) { skip = false; continue; } const child = children[i]; if (child.type === "paragraph") { const nextChild = children[i + 1]; if (nextChild.type !== "list") continue; ingredients.push({ name: getTextOfChild(child) || "", ingredients: parseIngredientsList(nextChild), }); skip = true; continue; } if (child.type === "list") { ingredients.push(...parseIngredientsList(child)); } } return ingredients; } 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") { meta = parse(child.value) as Recipe["meta"]; 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); const ingredients = parseIngredients(groups[1]); const preparation = getTextOfRange(groups[2], original); const tags = extractHashTags(description || ""); if (description) { for (const tag of tags) { description = description.replace("#" + tag, ""); } } return { type: "recipe", id, meta, name, tags, description, ingredients, preparation, }; } const crud = createCrud({ prefix: `Recipes/`, parse: parseRecipe, hasThumbnails: true, }); export const getAllRecipes = crud.readAll; export const getRecipe = crud.read; export const updateRecipe = crud.update; export const createRecipe = crud.create;