import { type DocumentChild, getTextOfChild, getTextOfRange, parseDocument, parseFrontmatter, renderMarkdown, } from "@lib/documents.ts"; import { parseIngredient } from "npm:parse-ingredient"; export type IngredientGroup = { name: string; ingredients: Ingredient[]; }; export type Ingredient = { type: string; unit?: string; amount?: string; }; export type Ingredients = (Ingredient | IngredientGroup)[]; export type Recipe = { id: string; meta?: { link?: string; image?: string; rating?: number; portion?: number; }; name: string; description?: string; ingredients: Ingredients; preparation?: 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"], }, 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 = parseFrontmatter(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); } const description = getTextOfRange(groups[0], original); const ingredients = parseIngredients(groups[1]); const preparation = getTextOfRange(groups[2], original); return { id, meta, name, description: description ? renderMarkdown(description) : "", ingredients, preparation: preparation ? renderMarkdown(preparation) : "", }; }