124 lines
5.1 KiB
JavaScript
124 lines
5.1 KiB
JavaScript
"use strict";
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
exports.parseRecipeMarkdown = parseRecipeMarkdown;
|
||
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 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;
|
||
function parseRecipeMarkdown(markdown) {
|
||
const lines = markdown.split('\n');
|
||
let name = '';
|
||
let description = '';
|
||
let instructions = '';
|
||
const ingredients = [];
|
||
let currentSection = 'none';
|
||
const descriptionLines = [];
|
||
const instructionLines = [];
|
||
for (const line of lines) {
|
||
const trimmed = line.trim();
|
||
if (H1_RE.test(trimmed) && !trimmed.startsWith('##')) {
|
||
name = trimmed.replace(H1_RE, '').trim();
|
||
currentSection = 'description';
|
||
continue;
|
||
}
|
||
if (H2_RE.test(trimmed)) {
|
||
const heading = trimmed.replace(H2_RE, '').trim().toLowerCase();
|
||
if (INGREDIENT_HEADING_RE.test(heading)) {
|
||
currentSection = 'ingredients';
|
||
}
|
||
else if (INSTRUCTION_HEADING_RE.test(heading)) {
|
||
currentSection = 'instructions';
|
||
}
|
||
else {
|
||
currentSection = 'none';
|
||
}
|
||
continue;
|
||
}
|
||
switch (currentSection) {
|
||
case 'description':
|
||
if (trimmed.length > 0) {
|
||
descriptionLines.push(trimmed);
|
||
}
|
||
break;
|
||
case 'ingredients':
|
||
if (BULLET_RE.test(trimmed)) {
|
||
const ingredientText = trimmed.replace(BULLET_RE, '');
|
||
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 };
|
||
}
|
||
function parseIngredientLine(text) {
|
||
const trimmed = text.trim();
|
||
const toAlternatives = (rawName) => ALTERNATIVES_RE.test(rawName)
|
||
? rawName.split(ALTERNATIVES_RE).map((s) => s.trim()).filter(Boolean)
|
||
: [rawName];
|
||
let note = null;
|
||
let main = trimmed;
|
||
const parenMatch = trimmed.match(PAREN_NOTE_RE);
|
||
if (parenMatch) {
|
||
note = parenMatch[1].trim();
|
||
main = trimmed.slice(0, parenMatch.index).trim();
|
||
}
|
||
const fractionMatch = main.match(FRACTION_RE);
|
||
if (fractionMatch) {
|
||
const whole = fractionMatch[1] ? parseFloat(fractionMatch[1]) : 0;
|
||
const quantity = whole + parseFloat(fractionMatch[2]) / parseFloat(fractionMatch[3]);
|
||
const candidateUnit = fractionMatch[4].toLowerCase();
|
||
if (KNOWN_UNITS.has(candidateUnit)) {
|
||
const rawName = fractionMatch[5].trim();
|
||
return { quantity, unit: candidateUnit, rawName, note, alternatives: toAlternatives(rawName) };
|
||
}
|
||
}
|
||
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) };
|
||
}
|
||
}
|
||
const fullMatch = main.match(QTY_UNIT_NAME_RE);
|
||
if (fullMatch) {
|
||
const candidateUnit = fullMatch[2].toLowerCase();
|
||
if (KNOWN_UNITS.has(candidateUnit)) {
|
||
const rawName = fullMatch[3].trim();
|
||
return { quantity: parseNumber(fullMatch[1]), unit: candidateUnit, rawName, note, alternatives: toAlternatives(rawName) };
|
||
}
|
||
const rawName = fullMatch[2] + ' ' + fullMatch[3];
|
||
return { quantity: parseNumber(fullMatch[1]), unit: 'st', rawName, note, alternatives: toAlternatives(rawName) };
|
||
}
|
||
const noUnitMatch = main.match(QTY_NAME_RE);
|
||
if (noUnitMatch) {
|
||
const rawName = noUnitMatch[2].trim();
|
||
return { quantity: parseNumber(noUnitMatch[1]), unit: 'st', rawName, note, alternatives: toAlternatives(rawName) };
|
||
}
|
||
return { quantity: 0, unit: '', rawName: main, note, alternatives: toAlternatives(main) };
|
||
}
|
||
function parseNumber(s) {
|
||
return parseFloat(s.replace(',', '.'));
|
||
}
|
||
//# sourceMappingURL=recipe-parser.js.map
|