export interface ParsedIngredient { rawName: string; quantity: number; unit: string; note: string | null; } export interface ParsedRecipe { name: string; description: string; instructions: string; ingredients: ParsedIngredient[]; } /** * Parsar ett recept i Markdown-format och extraherar namn, beskrivning, * instruktioner och ingredienser. * * Förväntat format: * # Receptnamn * Beskrivning (valfritt stycke efter titeln) * * ## Ingredienser * - 400 g kycklingfilé * - 2 dl grädde (eller crème fraiche) * * ## Instruktioner * 1. Stek kycklingen … */ export function parseRecipeMarkdown(markdown: string): ParsedRecipe { const lines = markdown.split('\n'); let name = ''; let description = ''; let instructions = ''; const ingredients: ParsedIngredient[] = []; let currentSection: 'none' | 'description' | 'ingredients' | 'instructions' = 'none'; const descriptionLines: string[] = []; const instructionLines: string[] = []; for (const line of lines) { const trimmed = line.trim(); // H1 — receptnamn if (/^#\s+/.test(trimmed) && !trimmed.startsWith('##')) { name = trimmed.replace(/^#\s+/, '').trim(); currentSection = 'description'; continue; } // H2 — sektionsrubriker if (/^##\s+/.test(trimmed)) { const heading = trimmed.replace(/^##\s+/, '').trim().toLowerCase(); if (/ingrediens/.test(heading)) { currentSection = 'ingredients'; } else if (/instruktion|tillagning|gör så här|steg/.test(heading)) { currentSection = 'instructions'; } else { currentSection = 'none'; } continue; } // Samla rader beroende på sektion switch (currentSection) { case 'description': if (trimmed.length > 0) { descriptionLines.push(trimmed); } break; case 'ingredients': if (/^[-*]\s+/.test(trimmed)) { const ingredientText = trimmed.replace(/^[-*]\s+/, ''); ingredients.push(parseIngredientLine(ingredientText)); } break; case 'instructions': if (trimmed.length > 0) { instructionLines.push(trimmed); } break; } } description = descriptionLines.join('\n'); instructions = instructionLines.join('\n'); return { name, description, instructions, ingredients }; } /** * Parsar en ingrediensrad, t.ex.: * "400 g kycklingfilé" * "2 dl grädde (eller crème fraiche)" * "1 kruka basilika" * "salt" */ function parseIngredientLine(text: string): ParsedIngredient { const trimmed = text.trim(); // Extrahera eventuell parentes-not i slutet let note: string | null = null; let main = trimmed; const parenMatch = trimmed.match(/\(([^)]+)\)\s*$/); if (parenMatch) { note = parenMatch[1].trim(); main = trimmed.slice(0, parenMatch.index).trim(); } // Försök matcha "kvantitet enhet namn" — t.ex. "400 g kycklingfilé" eller "2.5 dl grädde" const fullMatch = main.match(/^(\d+(?:[.,]\d+)?)\s+(\S+)\s+(.+)$/); if (fullMatch) { return { quantity: parseNumber(fullMatch[1]), unit: fullMatch[2], rawName: fullMatch[3].trim(), note, }; } // Försök matcha "kvantitet namn" utan enhet — t.ex. "3 ägg" const noUnitMatch = main.match(/^(\d+(?:[.,]\d+)?)\s+(.+)$/); if (noUnitMatch) { return { quantity: parseNumber(noUnitMatch[1]), unit: 'st', rawName: noUnitMatch[2].trim(), note, }; } // Bara ett namn, ingen kvantitet — t.ex. "salt" return { quantity: 0, unit: '', rawName: main, note, }; } function parseNumber(s: string): number { return parseFloat(s.replace(',', '.')); }