feat: add support for alternative ingredients; implement JSON storage and parsing logic
Test Suite / test (24.15.0) (push) Has been cancelled
Test Suite / test (24.15.0) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { parseRecipeMarkdown } from './recipe-parser';
|
||||
|
||||
describe('parseRecipeMarkdown — ingrediensformat', () => {
|
||||
const parse = (line: string) => {
|
||||
const md = `# Test\n## Ingredienser\n- ${line}\n## Instruktioner\n1. Gör det.`;
|
||||
return parseRecipeMarkdown(md).ingredients[0];
|
||||
};
|
||||
|
||||
it('vanlig rad: kvantitet enhet namn', () => {
|
||||
const ing = parse('400 g kycklingfilé');
|
||||
expect(ing).toMatchObject({ quantity: 400, unit: 'g', rawName: 'kycklingfilé' });
|
||||
});
|
||||
|
||||
it('bråk: 1 1/2 dl grädde', () => {
|
||||
const ing = parse('1 1/2 dl grädde');
|
||||
expect(ing.quantity).toBeCloseTo(1.5);
|
||||
expect(ing).toMatchObject({ unit: 'dl', rawName: 'grädde' });
|
||||
});
|
||||
|
||||
it('bråk utan heltal: 1/2 dl mjölk', () => {
|
||||
const ing = parse('1/2 dl mjölk');
|
||||
expect(ing.quantity).toBeCloseTo(0.5);
|
||||
expect(ing).toMatchObject({ unit: 'dl', rawName: 'mjölk' });
|
||||
});
|
||||
|
||||
it('intervall: 600 - ca 700 g kycklingfilé', () => {
|
||||
const ing = parse('600 - ca 700 g kycklingfilé');
|
||||
expect(ing.quantity).toBeCloseTo(650);
|
||||
expect(ing).toMatchObject({ unit: 'g', rawName: 'kycklingfilé' });
|
||||
});
|
||||
|
||||
it('intervall: ca 600-700 g kycklingfilé', () => {
|
||||
const ing = parse('ca 600-700 g kycklingfilé');
|
||||
expect(ing.quantity).toBeCloseTo(650);
|
||||
expect(ing).toMatchObject({ unit: 'g' });
|
||||
});
|
||||
|
||||
it('intervall med alternativt namn: 600 - ca 700 g kycklingfilé eller kycklinginnerfilé', () => {
|
||||
const ing = parse('600 - ca 700 g kycklingfilé eller kycklinginnerfilé');
|
||||
expect(ing.quantity).toBeCloseTo(650);
|
||||
expect(ing).toMatchObject({ unit: 'g', rawName: 'kycklingfilé eller kycklinginnerfilé' });
|
||||
});
|
||||
|
||||
it('parentes-not: 2 dl grädde (eller crème fraiche)', () => {
|
||||
const ing = parse('2 dl grädde (eller crème fraiche)');
|
||||
expect(ing).toMatchObject({ quantity: 2, unit: 'dl', rawName: 'grädde', note: 'eller crème fraiche' });
|
||||
});
|
||||
|
||||
it('utan enhet: 3 ägg', () => {
|
||||
const ing = parse('3 ägg');
|
||||
expect(ing).toMatchObject({ quantity: 3, unit: 'st', rawName: 'ägg' });
|
||||
});
|
||||
|
||||
it('bara namn: salt', () => {
|
||||
const ing = parse('salt');
|
||||
expect(ing).toMatchObject({ quantity: 0, unit: '', rawName: 'salt' });
|
||||
});
|
||||
|
||||
it('decimalkomma: 2,5 dl mjölk', () => {
|
||||
const ing = parse('2,5 dl mjölk');
|
||||
expect(ing.quantity).toBeCloseTo(2.5);
|
||||
expect(ing).toMatchObject({ unit: 'dl', rawName: 'mjölk' });
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ export interface ParsedIngredient {
|
||||
quantity: number;
|
||||
unit: string;
|
||||
note: string | null;
|
||||
alternatives: string[]; // uppdelning av "ris eller couscous" → ["ris", "couscous"]
|
||||
}
|
||||
|
||||
export interface ParsedRecipe {
|
||||
@@ -38,8 +39,10 @@ const INSTRUCTION_HEADING_RE = /instruktion|tillagning|gör så här|steg|tillv
|
||||
const BULLET_RE = /^[-*]\s+/;
|
||||
const PAREN_NOTE_RE = /\(([^)]+)\)\s*$/;
|
||||
const FRACTION_RE = /^(\d+)?\s*(\d+)\s*\/\s*([\d.]+)\s+(\S+)\s+(.+)$/;
|
||||
const RANGE_RE = /^(?:ca\.?\s+)?(\d+(?:[.,]\d+)?)\s*[-–]\s*(?:ca\.?\s+)?(\d+(?:[.,]\d+)?)\s+(\S+)\s+(.+)$/i;
|
||||
const QTY_UNIT_NAME_RE = /^(\d+(?:[.,]\d+)?)\s+(\S+)\s+(.+)$/;
|
||||
const QTY_NAME_RE = /^(\d+(?:[.,]\d+)?)\s+(.+)$/;
|
||||
const ALTERNATIVES_RE = /\s+eller\s+/i;
|
||||
|
||||
// ============================================================================
|
||||
// Parser Functions
|
||||
@@ -136,6 +139,12 @@ export function parseRecipeMarkdown(markdown: string): ParsedRecipe {
|
||||
function parseIngredientLine(text: string): ParsedIngredient {
|
||||
const trimmed = text.trim();
|
||||
|
||||
// Hjälpfunktion: splittar rawName på " eller " och returnerar alternatives
|
||||
const toAlternatives = (rawName: string): string[] =>
|
||||
ALTERNATIVES_RE.test(rawName)
|
||||
? rawName.split(ALTERNATIVES_RE).map((s) => s.trim()).filter(Boolean)
|
||||
: [rawName];
|
||||
|
||||
// Extrahera eventuell parentes-not i slutet
|
||||
let note: string | null = null;
|
||||
let main = trimmed;
|
||||
@@ -152,7 +161,21 @@ function parseIngredientLine(text: string): ParsedIngredient {
|
||||
const quantity = whole + parseFloat(fractionMatch[2]) / parseFloat(fractionMatch[3]);
|
||||
const candidateUnit = fractionMatch[4].toLowerCase();
|
||||
if (KNOWN_UNITS.has(candidateUnit)) {
|
||||
return { quantity, unit: candidateUnit, rawName: fractionMatch[5].trim(), note };
|
||||
const rawName = fractionMatch[5].trim();
|
||||
return { quantity, unit: candidateUnit, rawName, note, alternatives: toAlternatives(rawName) };
|
||||
}
|
||||
}
|
||||
|
||||
// Försök matcha intervall: "600 - ca 700 g kycklingfilé" eller "ca 600–700 g"
|
||||
// Använder medelvärdet av intervallet som kvantitet.
|
||||
const rangeMatch = main.match(RANGE_RE);
|
||||
if (rangeMatch) {
|
||||
const candidateUnit = rangeMatch[3].toLowerCase();
|
||||
if (KNOWN_UNITS.has(candidateUnit)) {
|
||||
const lo = parseNumber(rangeMatch[1]);
|
||||
const hi = parseNumber(rangeMatch[2]);
|
||||
const rawName = rangeMatch[4].trim();
|
||||
return { quantity: (lo + hi) / 2, unit: candidateUnit, rawName, note, alternatives: toAlternatives(rawName) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,20 +184,23 @@ function parseIngredientLine(text: string): ParsedIngredient {
|
||||
if (fullMatch) {
|
||||
const candidateUnit = fullMatch[2].toLowerCase();
|
||||
if (KNOWN_UNITS.has(candidateUnit)) {
|
||||
return { quantity: parseNumber(fullMatch[1]), unit: candidateUnit, rawName: fullMatch[3].trim(), note };
|
||||
const rawName = fullMatch[3].trim();
|
||||
return { quantity: parseNumber(fullMatch[1]), unit: candidateUnit, rawName, note, alternatives: toAlternatives(rawName) };
|
||||
}
|
||||
// Inte känd enhet — behandla "kvantitet ord1 ord2..." utan enhet
|
||||
return { quantity: parseNumber(fullMatch[1]), unit: 'st', rawName: fullMatch[2] + ' ' + fullMatch[3], note };
|
||||
const rawName = fullMatch[2] + ' ' + fullMatch[3];
|
||||
return { quantity: parseNumber(fullMatch[1]), unit: 'st', rawName, note, alternatives: toAlternatives(rawName) };
|
||||
}
|
||||
|
||||
// Försök matcha "kvantitet namn" utan enhet — t.ex. "3 ägg"
|
||||
const noUnitMatch = main.match(QTY_NAME_RE);
|
||||
if (noUnitMatch) {
|
||||
return { quantity: parseNumber(noUnitMatch[1]), unit: 'st', rawName: noUnitMatch[2].trim(), note };
|
||||
const rawName = noUnitMatch[2].trim();
|
||||
return { quantity: parseNumber(noUnitMatch[1]), unit: 'st', rawName, note, alternatives: toAlternatives(rawName) };
|
||||
}
|
||||
|
||||
// Bara ett namn, ingen kvantitet — t.ex. "salt"
|
||||
return { quantity: 0, unit: '', rawName: main, note };
|
||||
return { quantity: 0, unit: '', rawName: main, note, alternatives: toAlternatives(main) };
|
||||
}
|
||||
|
||||
function parseNumber(s: string): number {
|
||||
|
||||
Reference in New Issue
Block a user