diff --git a/backend/src/quick-import/parsers/base.parser.ts b/backend/src/quick-import/parsers/base.parser.ts index b448e63f..e8b65e35 100644 --- a/backend/src/quick-import/parsers/base.parser.ts +++ b/backend/src/quick-import/parsers/base.parser.ts @@ -59,15 +59,25 @@ export abstract class RecipeParser { cleaned = cleaned.replace(/\s*\([^)]*\)/, '').trim(); } - // Hantera bråkdelar: "1/2" eller "1 / 2" - const fractionMatch = cleaned.match(/^([\d.]+)\s*\/\s*([\d.]+)/); + // Hantera bråkdelar: "1/2" eller "1 1/2" eller "1 1 / 2" + // Regex: (optional whole)? numerator / denominator + const fractionMatch = cleaned.match(/^(\d+)?\s*(\d+)\s*\/\s*([\d.]+)/); let quantity = 0; let remainingText = cleaned; if (fractionMatch) { - const numerator = parseFloat(fractionMatch[1]); - const denominator = parseFloat(fractionMatch[2]); - quantity = numerator / denominator; + if (fractionMatch[1]) { + // Heltal + bråk: "1 1/2" + const whole = parseFloat(fractionMatch[1]); + const numerator = parseFloat(fractionMatch[2]); + const denominator = parseFloat(fractionMatch[3]); + quantity = whole + (numerator / denominator); + } else { + // Bara bråk: "1/2" + const numerator = parseFloat(fractionMatch[2]); + const denominator = parseFloat(fractionMatch[3]); + quantity = numerator / denominator; + } remainingText = cleaned.substring(fractionMatch[0].length).trim(); } else { const numberMatch = remainingText.match(/^([\d.,]+)/); diff --git a/backend/src/quick-import/parsers/generic.parser.ts b/backend/src/quick-import/parsers/generic.parser.ts index 898ff28e..04d86b76 100644 --- a/backend/src/quick-import/parsers/generic.parser.ts +++ b/backend/src/quick-import/parsers/generic.parser.ts @@ -42,6 +42,7 @@ export class GenericRecipeParser extends RecipeParser { private extractFromJsonLd(recipe: any): ParsedRecipe { const name = recipe.name || ''; + const description = recipe.description || ''; const ingredients: Array<{ quantity: number; unit: string; name: string; note?: string }> = []; if (recipe.recipeIngredient && Array.isArray(recipe.recipeIngredient)) { @@ -71,6 +72,7 @@ export class GenericRecipeParser extends RecipeParser { return { name, + description, ingredients, instructions, }; @@ -90,6 +92,15 @@ export class GenericRecipeParser extends RecipeParser { name = titleMatch[1].trim(); } + // Försöka extrahera beskrivning från meta-taggar + let description = ''; + const descMatch = html.match( + / = []; @@ -129,6 +140,7 @@ export class GenericRecipeParser extends RecipeParser { return { name, + description, ingredients, instructions, }; diff --git a/backend/src/quick-import/parsers/ica.parser.ts b/backend/src/quick-import/parsers/ica.parser.ts index ae058194..361a710f 100644 --- a/backend/src/quick-import/parsers/ica.parser.ts +++ b/backend/src/quick-import/parsers/ica.parser.ts @@ -45,6 +45,9 @@ export class IcaRecipeParser extends RecipeParser { // Extrahera titel const name = recipe.name || ''; + // Extrahera beskrivning + const description = recipe.description || ''; + // Extrahera ingredienser const ingredients: Array<{ quantity: number; unit: string; name: string; note?: string }> = []; if (recipe.recipeIngredient && Array.isArray(recipe.recipeIngredient)) { @@ -75,6 +78,7 @@ export class IcaRecipeParser extends RecipeParser { return { name, + description, ingredients, instructions, }; @@ -96,6 +100,15 @@ export class IcaRecipeParser extends RecipeParser { } } + // Extrahera beskrivning från meta-taggar + let description = ''; + const descMatch = html.match( + / = []; const ingredientRegex = /]*class="[^"]*ingredient[^"]*"[^>]*>([^<]+)<\/li>/gi; @@ -117,6 +130,7 @@ export class IcaRecipeParser extends RecipeParser { return { name, + description, ingredients, instructions, }; diff --git a/backend/src/recipes/recipes.service.ts b/backend/src/recipes/recipes.service.ts index 05168035..0998d727 100644 --- a/backend/src/recipes/recipes.service.ts +++ b/backend/src/recipes/recipes.service.ts @@ -529,7 +529,7 @@ function parseRecipeMarkdown(markdown: string): ParsedRecipe { 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)) { + } else if (/instruktion|tillagning|gör så här|steg|tillväg|metod/.test(heading)) { currentSection = 'instructions'; } else { currentSection = 'none'; @@ -570,12 +570,21 @@ function parseRecipeMarkdown(markdown: string): ParsedRecipe { * Parsar en ingrediensrad, t.ex.: * "400 g kycklingfilé" * "2 dl grädde (eller crème fraiche)" + * "1 1/2 dl crème fraiche" + * "1 polka- eller gulbeta" * "1 kruka basilika" * "salt" */ function parseIngredientLine(text: string): ParsedIngredient { const trimmed = text.trim(); + // Kända enheter + const knownUnits = [ + 'g', 'kg', 'hg', 'mg', 'ml', 'dl', 'l', 'tl', + 'st', 'tsk', 'msk', 'krm', 'matsled', 'tesled', + 'pris', 'portion', 'burk', 'förp', 'paket', + ]; + // Extrahera eventuell parentes-not i slutet let note: string | null = null; let main = trimmed; @@ -585,13 +594,44 @@ function parseIngredientLine(text: string): ParsedIngredient { main = trimmed.slice(0, parenMatch.index).trim(); } + // Försök matcha bråk först: "1 1/2 dl crème fraiche" eller "1/2 dl" + const fractionMatch = main.match(/^(\d+)?\s*(\d+)\s*\/\s*([\d.]+)\s+(\S+)\s+(.*)$/); + if (fractionMatch) { + let quantity = 0; + if (fractionMatch[1]) { + quantity = parseFloat(fractionMatch[1]) + parseFloat(fractionMatch[2]) / parseFloat(fractionMatch[3]); + } else { + quantity = parseFloat(fractionMatch[2]) / parseFloat(fractionMatch[3]); + } + const candidateUnit = fractionMatch[4].toLowerCase(); + if (knownUnits.includes(candidateUnit)) { + return { + quantity, + unit: candidateUnit, + rawName: fractionMatch[5].trim(), + note, + }; + } + } + // 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) { + const candidateUnit = fullMatch[2].toLowerCase(); + // Validera att det andra ordet är en känd enhet + if (knownUnits.includes(candidateUnit)) { + return { + quantity: parseNumber(fullMatch[1]), + unit: candidateUnit, + rawName: fullMatch[3].trim(), + note, + }; + } + // Om inte känd enhet, behandla som "kvantitet namn" utan enhet return { quantity: parseNumber(fullMatch[1]), - unit: fullMatch[2], - rawName: fullMatch[3].trim(), + unit: 'st', + rawName: fullMatch[2] + ' ' + fullMatch[3], note, }; } diff --git a/frontend/app/recipes/write/WriteRecipePage.tsx b/frontend/app/recipes/write/WriteRecipePage.tsx index e1a06a00..16e6a9e3 100644 --- a/frontend/app/recipes/write/WriteRecipePage.tsx +++ b/frontend/app/recipes/write/WriteRecipePage.tsx @@ -311,243 +311,263 @@ Stek löken i lite smör. Tillsätt köttfärsen...`} {/* STEG 2: Granskning */} {step === 'review' && parsed && ( -
- {/* Debug Panel - Import Output */} -
setShowDebugPanel((e.target as HTMLDetailsElement).open)} - style={{ - border: '1px solid #ddd', - borderRadius: '8px', - padding: '1rem', - background: '#f9f9f9', - }} - > - - 🔍 Import Debug Output {showDebugPanel ? '▼' : '▶'} - -
- {/* Raw Markdown */} +
+ {/* Vänster: Receptdetaljer + Ingredienser */} +
+ {/* Receptdetaljer */} +
+

Receptdetaljer

+
-

Raw Markdown:

+ + setEditedName(e.target.value)} + required + style={{ width: '100%', padding: '0.75rem', border: '1px solid #ddd', borderRadius: '4px', fontSize: '1rem', boxSizing: 'border-box' }} + /> +
+ +
+ +