feat: refactor recipe parsing logic; replace regex literals with constants for improved readability and maintainability
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:
@@ -21,6 +21,26 @@ export interface ParsedRecipe {
|
|||||||
ingredients: ParsedIngredient[];
|
ingredients: ParsedIngredient[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Module-level constants (hoisted for performance)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const KNOWN_UNITS = new Set([
|
||||||
|
'g', 'kg', 'hg', 'mg', 'ml', 'dl', 'l', 'tl',
|
||||||
|
'st', 'tsk', 'msk', 'krm', 'matsled', 'tesled',
|
||||||
|
'pris', 'portion', 'port', 'burk', 'förp', 'paket', 'efter smak', 'klyfta',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const H1_RE = /^#\s+/;
|
||||||
|
const H2_RE = /^##\s+/;
|
||||||
|
const INGREDIENT_HEADING_RE = /ingrediens/;
|
||||||
|
const INSTRUCTION_HEADING_RE = /instruktion|tillagning|gör så här|steg|tillväg|metod/;
|
||||||
|
const BULLET_RE = /^[-*]\s+/;
|
||||||
|
const PAREN_NOTE_RE = /\(([^)]+)\)\s*$/;
|
||||||
|
const FRACTION_RE = /^(\d+)?\s*(\d+)\s*\/\s*([\d.]+)\s+(\S+)\s+(.+)$/;
|
||||||
|
const QTY_UNIT_NAME_RE = /^(\d+(?:[.,]\d+)?)\s+(\S+)\s+(.+)$/;
|
||||||
|
const QTY_NAME_RE = /^(\d+(?:[.,]\d+)?)\s+(.+)$/;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Parser Functions
|
// Parser Functions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -56,18 +76,18 @@ export function parseRecipeMarkdown(markdown: string): ParsedRecipe {
|
|||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
|
|
||||||
// H1 — receptnamn
|
// H1 — receptnamn
|
||||||
if (/^#\s+/.test(trimmed) && !trimmed.startsWith('##')) {
|
if (H1_RE.test(trimmed) && !trimmed.startsWith('##')) {
|
||||||
name = trimmed.replace(/^#\s+/, '').trim();
|
name = trimmed.replace(H1_RE, '').trim();
|
||||||
currentSection = 'description';
|
currentSection = 'description';
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// H2 — sektionsrubriker
|
// H2 — sektionsrubriker
|
||||||
if (/^##\s+/.test(trimmed)) {
|
if (H2_RE.test(trimmed)) {
|
||||||
const heading = trimmed.replace(/^##\s+/, '').trim().toLowerCase();
|
const heading = trimmed.replace(H2_RE, '').trim().toLowerCase();
|
||||||
if (/ingrediens/.test(heading)) {
|
if (INGREDIENT_HEADING_RE.test(heading)) {
|
||||||
currentSection = 'ingredients';
|
currentSection = 'ingredients';
|
||||||
} else if (/instruktion|tillagning|gör så här|steg|tillväg|metod/.test(heading)) {
|
} else if (INSTRUCTION_HEADING_RE.test(heading)) {
|
||||||
currentSection = 'instructions';
|
currentSection = 'instructions';
|
||||||
} else {
|
} else {
|
||||||
currentSection = 'none';
|
currentSection = 'none';
|
||||||
@@ -84,8 +104,8 @@ export function parseRecipeMarkdown(markdown: string): ParsedRecipe {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ingredients':
|
case 'ingredients':
|
||||||
if (/^[-*]\s+/.test(trimmed)) {
|
if (BULLET_RE.test(trimmed)) {
|
||||||
const ingredientText = trimmed.replace(/^[-*]\s+/, '');
|
const ingredientText = trimmed.replace(BULLET_RE, '');
|
||||||
ingredients.push(parseIngredientLine(ingredientText));
|
ingredients.push(parseIngredientLine(ingredientText));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -116,82 +136,45 @@ export function parseRecipeMarkdown(markdown: string): ParsedRecipe {
|
|||||||
function parseIngredientLine(text: string): ParsedIngredient {
|
function parseIngredientLine(text: string): ParsedIngredient {
|
||||||
const trimmed = text.trim();
|
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', 'port', 'burk', 'förp', 'paket', 'efter smak', 'klyfta',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Extrahera eventuell parentes-not i slutet
|
// Extrahera eventuell parentes-not i slutet
|
||||||
let note: string | null = null;
|
let note: string | null = null;
|
||||||
let main = trimmed;
|
let main = trimmed;
|
||||||
const parenMatch = trimmed.match(/\(([^)]+)\)\s*$/);
|
const parenMatch = trimmed.match(PAREN_NOTE_RE);
|
||||||
if (parenMatch) {
|
if (parenMatch) {
|
||||||
note = parenMatch[1].trim();
|
note = parenMatch[1].trim();
|
||||||
main = trimmed.slice(0, parenMatch.index).trim();
|
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"
|
// Försök matcha bråk först: "1 1/2 dl crème fraiche" eller "1/2 dl mjölk"
|
||||||
const fractionMatch = main.match(/^(\d+)?\s*(\d+)\s*\/\s*([\d.]+)\s+(\S+)\s+(.*)$/);
|
const fractionMatch = main.match(FRACTION_RE);
|
||||||
if (fractionMatch) {
|
if (fractionMatch) {
|
||||||
let quantity = 0;
|
const whole = fractionMatch[1] ? parseFloat(fractionMatch[1]) : 0;
|
||||||
if (fractionMatch[1]) {
|
const quantity = whole + parseFloat(fractionMatch[2]) / parseFloat(fractionMatch[3]);
|
||||||
quantity = parseFloat(fractionMatch[1]) + parseFloat(fractionMatch[2]) / parseFloat(fractionMatch[3]);
|
|
||||||
} else {
|
|
||||||
quantity = parseFloat(fractionMatch[2]) / parseFloat(fractionMatch[3]);
|
|
||||||
}
|
|
||||||
const candidateUnit = fractionMatch[4].toLowerCase();
|
const candidateUnit = fractionMatch[4].toLowerCase();
|
||||||
if (knownUnits.includes(candidateUnit)) {
|
if (KNOWN_UNITS.has(candidateUnit)) {
|
||||||
return {
|
return { quantity, unit: candidateUnit, rawName: fractionMatch[5].trim(), note };
|
||||||
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"
|
// 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+(.+)$/);
|
const fullMatch = main.match(QTY_UNIT_NAME_RE);
|
||||||
if (fullMatch) {
|
if (fullMatch) {
|
||||||
const candidateUnit = fullMatch[2].toLowerCase();
|
const candidateUnit = fullMatch[2].toLowerCase();
|
||||||
// Validera att det andra ordet är en känd enhet
|
if (KNOWN_UNITS.has(candidateUnit)) {
|
||||||
if (knownUnits.includes(candidateUnit)) {
|
return { quantity: parseNumber(fullMatch[1]), unit: candidateUnit, rawName: fullMatch[3].trim(), note };
|
||||||
return {
|
|
||||||
quantity: parseNumber(fullMatch[1]),
|
|
||||||
unit: candidateUnit,
|
|
||||||
rawName: fullMatch[3].trim(),
|
|
||||||
note,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
// Om inte känd enhet, behandla som "kvantitet namn" utan enhet
|
// Inte känd enhet — behandla "kvantitet ord1 ord2..." utan enhet
|
||||||
return {
|
return { quantity: parseNumber(fullMatch[1]), unit: 'st', rawName: fullMatch[2] + ' ' + fullMatch[3], note };
|
||||||
quantity: parseNumber(fullMatch[1]),
|
|
||||||
unit: 'st',
|
|
||||||
rawName: fullMatch[2] + ' ' + fullMatch[3],
|
|
||||||
note,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Försök matcha "kvantitet namn" utan enhet — t.ex. "3 ägg"
|
// Försök matcha "kvantitet namn" utan enhet — t.ex. "3 ägg"
|
||||||
const noUnitMatch = main.match(/^(\d+(?:[.,]\d+)?)\s+(.+)$/);
|
const noUnitMatch = main.match(QTY_NAME_RE);
|
||||||
if (noUnitMatch) {
|
if (noUnitMatch) {
|
||||||
return {
|
return { quantity: parseNumber(noUnitMatch[1]), unit: 'st', rawName: noUnitMatch[2].trim(), note };
|
||||||
quantity: parseNumber(noUnitMatch[1]),
|
|
||||||
unit: 'st',
|
|
||||||
rawName: noUnitMatch[2].trim(),
|
|
||||||
note,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bara ett namn, ingen kvantitet — t.ex. "salt"
|
// Bara ett namn, ingen kvantitet — t.ex. "salt"
|
||||||
return {
|
return { quantity: 0, unit: '', rawName: main, note };
|
||||||
quantity: 0,
|
|
||||||
unit: '',
|
|
||||||
rawName: main,
|
|
||||||
note,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseNumber(s: string): number {
|
function parseNumber(s: string): number {
|
||||||
|
|||||||
Reference in New Issue
Block a user