feat: Implement PDF recipe parser and quick import service for file and URL inputs
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Parser för PDF-filer
|
||||
* Använder pdf-parse för att extrahera text från PDF-dokument
|
||||
*/
|
||||
import { RecipeParser, ParsedRecipe } from './base.parser';
|
||||
import * as pdf from 'pdf-parse';
|
||||
|
||||
export class PdfRecipeParser extends RecipeParser {
|
||||
canHandle(url: string): boolean {
|
||||
// Denna parser hanterar PDF-filer
|
||||
const normalized = url.toLowerCase();
|
||||
return normalized.endsWith('.pdf');
|
||||
}
|
||||
|
||||
async parse(fileBuffer: Buffer): Promise<ParsedRecipe> {
|
||||
console.log('[PdfParser] Parsing PDF file...');
|
||||
|
||||
try {
|
||||
// Extrahera text från PDF
|
||||
const data = await pdf(fileBuffer);
|
||||
const text = data.text;
|
||||
console.log('[PdfParser] Extraherad text längd:', text.length);
|
||||
|
||||
// Parsa texten till receptstruktur
|
||||
return this.parseRecipeText(text);
|
||||
} catch (err) {
|
||||
console.error('[PdfParser] Fel vid PDF-parsing:', err);
|
||||
throw new Error('Kunde inte tolka PDF-filen. Kontrollera att det är ett giltigt recept.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsar råtext från PDF till strukturerat recept
|
||||
* Försöker identifiera receptnamn, ingredienser och instruktioner
|
||||
*/
|
||||
private parseRecipeText(text: string): ParsedRecipe {
|
||||
const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
|
||||
|
||||
let name = 'Okänt recept';
|
||||
let description = '';
|
||||
const ingredients: Array<{ quantity: number; unit: string; name: string; note?: string }> = [];
|
||||
let instructions = '';
|
||||
let currentSection: 'name' | 'description' | 'ingredients' | 'instructions' | null = null;
|
||||
|
||||
// Försök hitta receptnamn (stor text i början)
|
||||
const titleMatch = text.match(/^[A-ZÅÄÖ\s]+/i);
|
||||
if (titleMatch) {
|
||||
name = titleMatch[0].trim();
|
||||
}
|
||||
|
||||
// Analysera texten rad för rad
|
||||
for (const line of lines) {
|
||||
// Hoppa över tomma rader
|
||||
if (!line || line.length === 0) continue;
|
||||
|
||||
// Detektera sektioner
|
||||
if (line.toLowerCase().includes('ingredienser')) {
|
||||
currentSection = 'ingredients';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.toLowerCase().includes('tillvägagångssätt') ||
|
||||
line.toLowerCase().includes('instruktioner') ||
|
||||
line.toLowerCase().includes('gör så här')) {
|
||||
currentSection = 'instructions';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Samla in innehåll baserat på aktuell sektion
|
||||
switch (currentSection) {
|
||||
case 'ingredients':
|
||||
if (line.toLowerCase().includes('tillvägagångssätt') ||
|
||||
line.toLowerCase().includes('instruktioner')) {
|
||||
currentSection = 'instructions';
|
||||
break;
|
||||
}
|
||||
|
||||
// Parsa ingrediensrad
|
||||
const ingredient = this.parseIngredientLine(line);
|
||||
if (ingredient) {
|
||||
ingredients.push(ingredient);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'instructions':
|
||||
if (instructions.length > 0) {
|
||||
instructions += '\n';
|
||||
}
|
||||
instructions += line;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Om vi inte har hittat ingredienser än, kan detta vara beskrivning
|
||||
if (ingredients.length === 0 && !description.includes(line)) {
|
||||
if (description.length > 0) {
|
||||
description += ' ';
|
||||
}
|
||||
description += line;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Om vi inte hittade något receptnamn, försök använda första meningsfulla raden
|
||||
if (name === 'Okänt recept' && lines.length > 0) {
|
||||
name = lines[0].length > 50 ? lines[0].substring(0, 50) + '...' : lines[0];
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
description: description || undefined,
|
||||
ingredients,
|
||||
instructions: instructions || undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user