@tmlmt/cooklang-parser 1.0.7 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/classes/aisle_config.ts","../src/classes/section.ts","../src/regex.ts","../src/units.ts","../src/parser_helpers.ts","../src/classes/recipe.ts","../src/classes/shopping_list.ts"],"sourcesContent":["import { AisleConfig } from \"./classes/aisle_config\";\nimport { Recipe } from \"./classes/recipe\";\nimport { ShoppingList } from \"./classes/shopping_list\";\nimport type {\n Metadata,\n MetadataExtract,\n Ingredient,\n Timer,\n TextItem,\n IngredientItem,\n CookwareItem,\n TimerItem,\n Item,\n Step,\n Note,\n Cookware,\n CategorizedIngredients,\n AddedRecipe,\n AisleIngredient,\n AisleCategory,\n} from \"./types\";\n\nexport {\n Recipe,\n ShoppingList,\n AisleConfig,\n Metadata,\n MetadataExtract,\n Ingredient,\n Timer,\n TextItem,\n IngredientItem,\n CookwareItem,\n TimerItem,\n Item,\n Step,\n Note,\n Cookware,\n CategorizedIngredients,\n AddedRecipe,\n AisleIngredient,\n AisleCategory,\n};\n","import type { AisleCategory, AisleIngredient } from \"../types\";\n\n/**\n * Represents the aisle configuration for a shopping list.\n * @category Classes\n */\nexport class AisleConfig {\n /**\n * The categories of aisles.\n * @see {@link AisleCategory}\n */\n categories: AisleCategory[] = [];\n\n /**\n * Creates a new AisleConfig instance.\n * @param config - The aisle configuration to parse.\n */\n constructor(config?: string) {\n if (config) {\n this.parse(config);\n }\n }\n\n /**\n * Parses an aisle configuration from a string.\n * @param config - The aisle configuration to parse.\n */\n parse(config: string) {\n let currentCategory: AisleCategory | null = null;\n const categoryNames = new Set<string>();\n const ingredientNames = new Set<string>();\n\n for (const line of config.split(\"\\n\")) {\n const trimmedLine = line.trim();\n\n if (trimmedLine.length === 0) {\n continue;\n }\n\n if (trimmedLine.startsWith(\"[\") && trimmedLine.endsWith(\"]\")) {\n const categoryName = trimmedLine\n .substring(1, trimmedLine.length - 1)\n .trim();\n\n if (categoryNames.has(categoryName)) {\n throw new Error(`Duplicate category found: ${categoryName}`);\n }\n categoryNames.add(categoryName);\n\n currentCategory = { name: categoryName, ingredients: [] };\n this.categories.push(currentCategory);\n } else {\n if (currentCategory === null) {\n throw new Error(\n `Ingredient found without a category: ${trimmedLine}`,\n );\n }\n\n const aliases = trimmedLine.split(\"|\").map((s) => s.trim());\n for (const alias of aliases) {\n if (ingredientNames.has(alias)) {\n throw new Error(`Duplicate ingredient/alias found: ${alias}`);\n }\n ingredientNames.add(alias);\n }\n\n const ingredient: AisleIngredient = {\n name: aliases[0]!, // We know this exists because trimmedLine is not empty\n aliases: aliases,\n };\n currentCategory.ingredients.push(ingredient);\n }\n }\n }\n}\n","import type { Step, Note } from \"../types\";\n\nexport class Section {\n name: string;\n content: (Step | Note)[] = [];\n\n constructor(name: string = \"\") {\n this.name = name;\n }\n\n isBlank(): boolean {\n return this.name === \"\" && this.content.length === 0;\n }\n}\n","export const metadataRegex = /---\\n(.*?)\\n---/s;\n\nconst multiwordIngredient =\n /@(?<mIngredientModifier>[@\\-&+*!?])?(?<mIngredientName>(?:[^\\s@#~\\[\\]{(.,;:!?]+(?:\\s+[^\\s@#~\\[\\]{(.,;:!?]+)+))(?=\\s*(?:\\{|\\}|\\(\\s*[^)]*\\s*\\)))(?:\\{(?<mIngredientQuantity>\\p{No}|(?:\\p{Nd}+(?:[.,\\/][\\p{Nd}]+)?))?(?:%(?<mIngredientUnits>[^}]+?))?\\})?(?:\\((?<mIngredientPreparation>[^)]*?)\\))?/gu;\nconst singleWordIngredient =\n /@(?<sIngredientModifier>[@\\-&+*!?])?(?<sIngredientName>[^\\s@#~\\[\\]{(.,;:!?]+)(?:\\{(?<sIngredientQuantity>\\p{No}|(?:\\p{Nd}+(?:[.,\\/][\\p{Nd}]+)?))(?:%(?<sIngredientUnits>[^}]+?))?\\})?(?:\\((?<sIngredientPreparation>[^)]*?)\\))?/gu;\n\nconst multiwordCookware =\n /#(?<mCookwareModifier>[\\-&+*!?])?(?<mCookwareName>(?:[^\\s@#~\\[\\]{(.,;:!?]+(?:\\s+[^\\s@#~\\[\\]{(.,;:!?]+)+))(?=\\s*(?:\\{|\\}|\\(\\s*[^)]*\\s*\\)))\\{(?<mCookwareQuantity>.*?)\\}/;\nconst singleWordCookware =\n /#(?<sCookwareModifier>[\\-&+*!?])?(?<sCookwareName>[^\\s@#~\\[\\]{(.,;:!?]+)(?:\\{(?<sCookwareQuantity>.*?)\\})?/u;\n\nconst timer =\n /~(?<timerName>.*?)(?:\\{(?<timerQuantity>.*?)(?:%(?<timerUnits>.+?))?\\})/;\n\nexport const tokensRegex = new RegExp(\n [\n multiwordIngredient,\n singleWordIngredient,\n multiwordCookware,\n singleWordCookware,\n timer,\n ]\n .map((r) => r.source)\n .join(\"|\"),\n \"gu\",\n);\n\nexport const commentRegex = /--.*/g;\nexport const blockCommentRegex = /\\s*\\[\\-.*?\\-\\]\\s*/g;\n\nexport const shoppingListRegex =\n /\\n\\s*\\[(?<name>.+)\\]\\n(?<items>[^]*?)(?:\\n\\n|$)/g;\n","export type UnitType = \"mass\" | \"volume\" | \"count\";\nexport type UnitSystem = \"metric\" | \"imperial\";\n\nexport interface UnitDefinition {\n name: string; // canonical name, e.g. 'g'\n type: UnitType;\n system: UnitSystem;\n aliases: string[]; // e.g. ['gram', 'grams']\n toBase: number; // conversion factor to the base unit of its type\n}\n\nexport interface Quantity {\n value: number | string;\n unit: string;\n}\n\n// Base units: mass -> gram (g), volume -> milliliter (ml)\nconst units: UnitDefinition[] = [\n // Mass (Metric)\n {\n name: \"g\",\n type: \"mass\",\n system: \"metric\",\n aliases: [\"gram\", \"grams\"],\n toBase: 1,\n },\n {\n name: \"kg\",\n type: \"mass\",\n system: \"metric\",\n aliases: [\"kilogram\", \"kilograms\"],\n toBase: 1000,\n },\n // Mass (Imperial)\n {\n name: \"oz\",\n type: \"mass\",\n system: \"imperial\",\n aliases: [\"ounce\", \"ounces\"],\n toBase: 28.3495,\n },\n {\n name: \"lb\",\n type: \"mass\",\n system: \"imperial\",\n aliases: [\"pound\", \"pounds\"],\n toBase: 453.592,\n },\n\n // Volume (Metric)\n {\n name: \"ml\",\n type: \"volume\",\n system: \"metric\",\n aliases: [\"milliliter\", \"milliliters\", \"millilitre\", \"millilitres\"],\n toBase: 1,\n },\n {\n name: \"l\",\n type: \"volume\",\n system: \"metric\",\n aliases: [\"liter\", \"liters\", \"litre\", \"litres\"],\n toBase: 1000,\n },\n {\n name: \"tsp\",\n type: \"volume\",\n system: \"metric\",\n aliases: [\"teaspoon\", \"teaspoons\"],\n toBase: 5,\n },\n {\n name: \"tbsp\",\n type: \"volume\",\n system: \"metric\",\n aliases: [\"tablespoon\", \"tablespoons\"],\n toBase: 15,\n },\n\n // Volume (Imperial)\n {\n name: \"fl-oz\",\n type: \"volume\",\n system: \"imperial\",\n aliases: [\"fluid ounce\", \"fluid ounces\"],\n toBase: 29.5735,\n },\n {\n name: \"cup\",\n type: \"volume\",\n system: \"imperial\",\n aliases: [\"cups\"],\n toBase: 236.588,\n },\n {\n name: \"pint\",\n type: \"volume\",\n system: \"imperial\",\n aliases: [\"pints\"],\n toBase: 473.176,\n },\n {\n name: \"quart\",\n type: \"volume\",\n system: \"imperial\",\n aliases: [\"quarts\"],\n toBase: 946.353,\n },\n {\n name: \"gallon\",\n type: \"volume\",\n system: \"imperial\",\n aliases: [\"gallons\"],\n toBase: 3785.41,\n },\n\n // Count units (no conversion, but recognized as a type)\n {\n name: \"piece\",\n type: \"count\",\n system: \"metric\",\n aliases: [\"pieces\"],\n toBase: 1,\n },\n];\n\nconst unitMap = new Map<string, UnitDefinition>();\nfor (const unit of units) {\n unitMap.set(unit.name.toLowerCase(), unit);\n for (const alias of unit.aliases) {\n unitMap.set(alias.toLowerCase(), unit);\n }\n}\n\nexport function normalizeUnit(unit: string): UnitDefinition | undefined {\n return unitMap.get(unit.toLowerCase().trim());\n}\n\n/**\n * Adds two quantities, returning the result in the most appropriate unit.\n */\nexport function addQuantities(q1: Quantity, q2: Quantity): Quantity {\n const unit1Def = normalizeUnit(q1.unit);\n const unit2Def = normalizeUnit(q2.unit);\n\n if (isNaN(Number(q1.value))) {\n throw new Error(\n `Cannot add quantity to string-quantified value: ${q1.value}`,\n );\n }\n if (isNaN(Number(q2.value))) {\n throw new Error(\n `Cannot add quantity to string-quantified value: ${q2.value}`,\n );\n }\n\n // If one unit is empty, assume it's of the same type as the other\n if (q1.unit === \"\" && unit2Def) {\n return {\n value:\n Math.round(((q1.value as number) + (q2.value as number)) * 100) / 100,\n unit: q2.unit,\n };\n }\n if (q2.unit === \"\" && unit1Def) {\n return {\n value:\n Math.round(((q1.value as number) + (q2.value as number)) * 100) / 100,\n unit: q1.unit,\n };\n }\n\n // If both units are the same (even if unknown, e.g. \"cloves\")\n if (q1.unit.toLowerCase() === q2.unit.toLowerCase()) {\n return {\n value:\n Math.round(((q1.value as number) + (q2.value as number)) * 100) / 100,\n unit: q1.unit,\n };\n }\n\n // If both are known and compatible\n if (unit1Def && unit2Def) {\n if (unit1Def.type !== unit2Def.type) {\n throw new Error(\n `Cannot add quantities of different types: ${unit1Def.type} (${q1.unit}) and ${unit2Def.type} (${q2.unit})`,\n );\n }\n\n // Convert both to base unit value\n const baseValue1 = (q1.value as number) * unit1Def.toBase;\n const baseValue2 = (q2.value as number) * unit2Def.toBase;\n const totalBaseValue = baseValue1 + baseValue2;\n\n let targetUnitDef: UnitDefinition;\n\n // Rule: If systems differ, convert to the largest metric unit.\n if (unit1Def.system !== unit2Def.system) {\n const metricUnitDef = unit1Def.system === \"metric\" ? unit1Def : unit2Def;\n targetUnitDef = units\n .filter((u) => u.type === metricUnitDef.type && u.system === \"metric\")\n .reduce((prev, current) =>\n prev.toBase > current.toBase ? prev : current,\n );\n } else {\n // Rule: Same system, use the biggest of the two input units.\n targetUnitDef = unit1Def.toBase >= unit2Def.toBase ? unit1Def : unit2Def;\n }\n\n const finalValue = totalBaseValue / targetUnitDef.toBase;\n\n return {\n value: Math.round(finalValue * 100) / 100,\n unit: targetUnitDef.name,\n };\n }\n\n // Otherwise, units are different and at least one is unknown.\n throw new Error(\n `Cannot add quantities with incompatible or unknown units: ${q1.unit} and ${q2.unit}`,\n );\n}\n","import type { MetadataExtract, Metadata } from \"./types\";\nimport { metadataRegex } from \"./regex\";\nimport { Section as SectionObject } from \"./classes/section\";\nimport type { Ingredient, Note, Step, Cookware } from \"./types\";\nimport { addQuantities } from \"./units\";\n\n/**\n * Finds an item in a list or adds it if not present, then returns its index.\n * @param list The list to search in.\n * @param finder A predicate to find the item.\n * @param creator A function to create the item if not found.\n * @returns The index of the item in the list.\n */\nexport function findOrPush<T>(\n list: T[],\n finder: (elem: T) => boolean,\n creator: () => T,\n): number {\n let index = list.findIndex(finder);\n if (index === -1) {\n index = list.push(creator()) - 1;\n }\n return index;\n}\n\n/**\n * Pushes a pending note to the section content if it's not empty.\n * @param section The current section object.\n * @param note The note content.\n * @returns An empty string if the note was pushed, otherwise the original note.\n */\nexport function flushPendingNote(\n section: SectionObject,\n note: Note[\"note\"],\n): Note[\"note\"] {\n if (note.length > 0) {\n section.content.push({ note });\n return \"\";\n }\n return note;\n}\n\n/**\n * Pushes pending step items and a pending note to the section content.\n * @param section The current section object.\n * @param items The list of step items. This array will be cleared.\n * @returns true if the items were pushed, otherwise false.\n */\nexport function flushPendingItems(\n section: SectionObject,\n items: Step[\"items\"],\n): boolean {\n if (items.length > 0) {\n section.content.push({ items: [...items] });\n items.length = 0;\n return true;\n }\n return false;\n}\n\n/**\n * Finds an ingredient in the list (case-insensitively) and updates it, or adds it if not present.\n * This function mutates the `ingredients` array.\n * @param ingredients The list of ingredients.\n * @param newIngredient The ingredient to find or add.\n * @param isReference Whether this is a reference ingredient (`&` modifier).\n * @returns The index of the ingredient in the list.\n */\nexport function findAndUpsertIngredient(\n ingredients: Ingredient[],\n newIngredient: Ingredient,\n isReference: boolean,\n): number {\n const { name, quantity, unit } = newIngredient;\n\n // New ingredient\n if (isReference) {\n const index = ingredients.findIndex(\n (i) => i.name.toLowerCase() === name.toLowerCase(),\n );\n\n if (index === -1) {\n throw new Error(\n `Referenced ingredient \"${name}\" not found. A referenced ingredient must be declared before being referenced with '&'.`,\n );\n }\n\n // Ingredient already exists, update it\n const existingIngredient = ingredients[index]!;\n if (quantity !== undefined) {\n const currentQuantity = {\n value: existingIngredient.quantity ?? 0,\n unit: existingIngredient.unit ?? \"\",\n };\n const newQuantity = { value: quantity, unit: unit ?? \"\" };\n\n const total = addQuantities(currentQuantity, newQuantity);\n existingIngredient.quantity = total.value;\n existingIngredient.unit = total.unit || undefined;\n }\n return index;\n }\n\n // Not a reference, so add as a new ingredient.\n return ingredients.push(newIngredient) - 1;\n}\n\nexport function findAndUpsertCookware(\n cookware: Cookware[],\n newCookware: Cookware,\n isReference: boolean,\n): number {\n const { name } = newCookware;\n\n if (isReference) {\n const index = cookware.findIndex(\n (i) => i.name.toLowerCase() === name.toLowerCase(),\n );\n\n if (index === -1) {\n throw new Error(\n `Referenced cookware \"${name}\" not found. A referenced cookware must be declared before being referenced with '&'.`,\n );\n }\n\n return index;\n }\n\n return cookware.push(newCookware) - 1;\n}\n\nexport function parseNumber(input_str: string): number {\n const clean_str = String(input_str).replace(\",\", \".\");\n if (!clean_str.startsWith(\"/\") && clean_str.includes(\"/\")) {\n const [num, den] = clean_str.split(\"/\").map(Number);\n return num! / den!;\n }\n return Number(clean_str);\n}\n\nexport function parseSimpleMetaVar(content: string, varName: string) {\n const varMatch = content.match(\n new RegExp(`^${varName}:\\\\s*(.*(?:\\\\r?\\\\n\\\\s+.*)*)+`, \"m\"),\n );\n return varMatch\n ? varMatch[1]?.trim().replace(/\\s*\\r?\\n\\s+/g, \" \")\n : undefined;\n}\n\nexport function parseScalingMetaVar(content: string, varName: string) {\n const varMatch = content.match(\n new RegExp(`^${varName}:[\\\\t ]*(([^,\\\\n]*),? ?(?:.*)?)`, \"m\"),\n );\n if (!varMatch) return undefined;\n if (isNaN(Number(varMatch[2]?.trim()))) {\n throw new Error(\"Scaling variables should be numbers\");\n }\n return [Number(varMatch[2]?.trim()), varMatch[1]?.trim()];\n}\n\nexport function parseListMetaVar(content: string, varName: string) {\n // Handle both inline and YAML-style tags\n const listMatch = content.match(\n new RegExp(\n `^${varName}:\\\\s*(?:\\\\[([^\\\\]]*)\\\\]|((?:\\\\r?\\\\n\\\\s*-\\\\s*.+)+))`,\n \"m\",\n ),\n );\n if (!listMatch) return undefined;\n\n if (listMatch[1] !== undefined) {\n // Inline list: tags: [one, two, three]\n return listMatch[1].split(\",\").map((tag) => tag.trim());\n } else if (listMatch[2]) {\n // YAML list:\n // tags:\n // - one\n // - two\n return listMatch[2]\n .split(\"\\n\")\n .filter((line) => line.trim() !== \"\")\n .map((line) => line.replace(/^\\s*-\\s*/, \"\").trim());\n }\n}\n\nexport function extractMetadata(content: string): MetadataExtract {\n const metadata: Metadata = {};\n let servings: number | undefined = undefined;\n\n // Is there front-matter at all?\n const metadataContent = content.match(metadataRegex)?.[1];\n if (!metadataContent) {\n return { metadata };\n }\n\n // String metadata variables\n for (const metaVar of [\n \"title\",\n \"source\",\n \"source.name\",\n \"source.url\",\n \"author\",\n \"source.author\",\n \"prep time\",\n \"time.prep\",\n \"cook time\",\n \"time.cook\",\n \"time required\",\n \"time\",\n \"duration\",\n \"locale\",\n \"introduction\",\n \"description\",\n \"course\",\n \"category\",\n \"diet\",\n \"cuisine\",\n \"difficulty\",\n \"image\",\n \"picture\",\n ] as (keyof Metadata)[]) {\n const stringMetaValue: any = parseSimpleMetaVar(metadataContent, metaVar);\n if (stringMetaValue) metadata[metaVar] = stringMetaValue;\n }\n\n // String metadata variables\n for (const metaVar of [\"servings\", \"yield\", \"serves\"] as (keyof Metadata)[]) {\n const scalingMetaValue: any = parseScalingMetaVar(metadataContent, metaVar);\n if (scalingMetaValue && scalingMetaValue[1]) {\n metadata[metaVar] = scalingMetaValue[1];\n servings = scalingMetaValue[0];\n }\n }\n\n // List metadata variables\n for (const metaVar of [\"tags\", \"images\", \"pictures\"] as (keyof Metadata)[]) {\n const listMetaValue: any = parseListMetaVar(metadataContent, metaVar);\n if (listMetaValue) metadata[metaVar] = listMetaValue;\n }\n\n return { metadata, servings };\n}\n","import type {\n Metadata,\n Ingredient,\n Timer,\n Step,\n Note,\n Cookware,\n MetadataExtract,\n} from \"../types\";\nimport { Section } from \"./section\";\nimport {\n tokensRegex,\n commentRegex,\n blockCommentRegex,\n metadataRegex,\n} from \"../regex\";\nimport {\n findOrPush,\n flushPendingItems,\n flushPendingNote,\n findAndUpsertIngredient,\n findAndUpsertCookware,\n parseNumber,\n extractMetadata,\n} from \"../parser_helpers\";\n\n/**\n * Represents a recipe.\n * @category Classes\n */\nexport class Recipe {\n /**\n * The recipe's metadata.\n * @see {@link Metadata}\n */\n metadata: Metadata = {};\n /**\n * The recipe's ingredients.\n * @see {@link Ingredient}\n */\n ingredients: Ingredient[] = [];\n /**\n * The recipe's sections.\n * @see {@link Section}\n */\n sections: Section[] = [];\n /**\n * The recipe's cookware.\n * @see {@link Cookware}\n */\n cookware: Cookware[] = [];\n /**\n * The recipe's timers.\n * @see {@link Timer}\n */\n timers: Timer[] = [];\n /**\n * The recipe's servings. Used for scaling\n */\n servings?: number;\n\n /**\n * Creates a new Recipe instance.\n * @param content - The recipe content to parse.\n */\n constructor(content?: string) {\n if (content) {\n this.parse(content);\n }\n }\n\n /**\n * Parses a recipe from a string.\n * @param content - The recipe content to parse.\n */\n parse(content: string) {\n const cleanContent = content\n .replace(metadataRegex, \"\")\n .replace(commentRegex, \"\")\n .replace(blockCommentRegex, \"\")\n .trim()\n .split(/\\r\\n?|\\n/);\n\n const { metadata, servings }: MetadataExtract = extractMetadata(content);\n this.metadata = metadata;\n this.servings = servings;\n\n let blankLineBefore = true;\n let section: Section = new Section();\n const items: Step[\"items\"] = [];\n let note: Note[\"note\"] = \"\";\n let inNote = false;\n\n for (const line of cleanContent) {\n if (line.trim().length === 0) {\n flushPendingItems(section, items);\n note = flushPendingNote(section, note);\n blankLineBefore = true;\n inNote = false;\n continue;\n }\n\n if (line.startsWith(\"=\")) {\n flushPendingItems(section, items);\n note = flushPendingNote(section, note);\n\n if (this.sections.length === 0 && section.isBlank()) {\n section.name = line.substring(1).trim();\n } else {\n if (!section.isBlank()) {\n this.sections.push(section);\n }\n section = new Section(line.substring(1).trim());\n }\n blankLineBefore = true;\n inNote = false;\n continue;\n }\n\n if (blankLineBefore && line.startsWith(\">\")) {\n flushPendingItems(section, items);\n note = flushPendingNote(section, note);\n note += line.substring(1).trim();\n inNote = true;\n blankLineBefore = false;\n continue;\n }\n\n if (inNote) {\n if (line.startsWith(\">\")) {\n note += \" \" + line.substring(1).trim();\n } else {\n note += \" \" + line.trim();\n }\n blankLineBefore = false;\n continue;\n }\n\n note = flushPendingNote(section, note);\n\n let cursor = 0;\n for (const match of line.matchAll(tokensRegex)) {\n const idx = match.index;\n if (idx > cursor) {\n items.push({ type: \"text\", value: line.slice(cursor, idx) });\n }\n\n const groups = match.groups!;\n\n if (groups.mIngredientName || groups.sIngredientName) {\n const name = (groups.mIngredientName || groups.sIngredientName)!;\n const quantityRaw =\n groups.mIngredientQuantity || groups.sIngredientQuantity;\n const units = groups.mIngredientUnits || groups.sIngredientUnits;\n const preparation =\n groups.mIngredientPreparation || groups.sIngredientPreparation;\n const modifier =\n groups.mIngredientModifier || groups.sIngredientModifier;\n const optional = modifier === \"?\";\n const hidden = modifier === \"-\";\n const reference = modifier === \"&\";\n const isRecipe = modifier === \"@\";\n const quantity = quantityRaw ? parseNumber(quantityRaw) : undefined;\n\n const idxInList = findAndUpsertIngredient(\n this.ingredients,\n {\n name,\n quantity,\n unit: units,\n optional,\n hidden,\n preparation,\n isRecipe,\n },\n reference,\n );\n\n items.push({ type: \"ingredient\", value: idxInList });\n } else if (groups.mCookwareName || groups.sCookwareName) {\n const name = (groups.mCookwareName || groups.sCookwareName)!;\n const modifier = groups.mCookwareModifier || groups.sCookwareModifier;\n const optional = modifier === \"?\";\n const hidden = modifier === \"-\";\n const reference = modifier === \"&\";\n\n const idxInList = findAndUpsertCookware(\n this.cookware,\n { name, optional, hidden },\n reference,\n );\n items.push({ type: \"cookware\", value: idxInList });\n } else if (groups.timerQuantity !== undefined) {\n const durationStr = groups.timerQuantity.trim();\n const unit = (groups.timerUnits || \"\").trim();\n if (!unit) {\n throw new Error(\"Timer missing units\");\n }\n const name = groups.timerName || undefined;\n const duration = parseNumber(durationStr);\n const timerObj: Timer = {\n name,\n duration,\n unit,\n };\n const idxInList = findOrPush(\n this.timers,\n (t) =>\n t.name === timerObj.name &&\n t.duration === timerObj.duration &&\n t.unit === timerObj.unit,\n () => timerObj,\n );\n items.push({ type: \"timer\", value: idxInList });\n }\n\n cursor = idx + match[0].length;\n }\n\n if (cursor < line.length) {\n items.push({ type: \"text\", value: line.slice(cursor) });\n }\n\n blankLineBefore = false;\n }\n\n // End of content reached: pushing all temporarily saved elements\n flushPendingItems(section, items);\n note = flushPendingNote(section, note);\n if (!section.isBlank()) {\n this.sections.push(section);\n }\n }\n\n /**\n * Scales the recipe to a new number of servings.\n * @param newServings - The new number of servings.\n * @returns A new Recipe instance with the scaled ingredients.\n */\n scaleTo(newServings: number): Recipe {\n const originalServings = this.getServings();\n\n if (originalServings === undefined || originalServings === 0) {\n throw new Error(\"Error scaling recipe: no initial servings value set\");\n }\n\n const factor = newServings / originalServings;\n return this.scaleBy(factor);\n }\n\n /**\n * Scales the recipe by a factor.\n * @param factor - The factor to scale the recipe by.\n * @returns A new Recipe instance with the scaled ingredients.\n */\n scaleBy(factor: number): Recipe {\n const newRecipe = this.clone();\n\n const originalServings = newRecipe.getServings();\n\n if (originalServings === undefined || originalServings === 0) {\n throw new Error(\"Error scaling recipe: no initial servings value set\");\n }\n\n newRecipe.ingredients = newRecipe.ingredients\n .map((ingredient) => {\n if (ingredient.quantity && !isNaN(Number(ingredient.quantity))) {\n (ingredient.quantity as number) *= factor;\n }\n return ingredient;\n })\n .filter((ingredient) => ingredient.quantity !== null);\n\n newRecipe.servings = originalServings * factor;\n\n if (newRecipe.metadata.servings && this.metadata.servings) {\n const servingsValue = parseFloat(this.metadata.servings);\n if (!isNaN(servingsValue)) {\n newRecipe.metadata.servings = String(servingsValue * factor);\n }\n }\n\n if (newRecipe.metadata.yield && this.metadata.yield) {\n const yieldValue = parseFloat(this.metadata.yield);\n if (!isNaN(yieldValue)) {\n newRecipe.metadata.yield = String(yieldValue * factor);\n }\n }\n\n if (newRecipe.metadata.serves && this.metadata.serves) {\n const servesValue = parseFloat(this.metadata.serves);\n if (!isNaN(servesValue)) {\n newRecipe.metadata.serves = String(servesValue * factor);\n }\n }\n\n return newRecipe;\n }\n\n private getServings(): number | undefined {\n if (this.servings) {\n return this.servings;\n }\n return undefined;\n }\n\n /**\n * Clones the recipe.\n * @returns A new Recipe instance with the same properties.\n */\n clone(): Recipe {\n const newRecipe = new Recipe();\n // deep copy\n newRecipe.metadata = JSON.parse(JSON.stringify(this.metadata));\n newRecipe.ingredients = JSON.parse(JSON.stringify(this.ingredients));\n newRecipe.sections = JSON.parse(JSON.stringify(this.sections));\n newRecipe.cookware = JSON.parse(JSON.stringify(this.cookware));\n newRecipe.timers = JSON.parse(JSON.stringify(this.timers));\n newRecipe.servings = this.servings;\n return newRecipe;\n }\n}\n","import { AisleConfig } from \"./aisle_config\";\nimport { Recipe } from \"./recipe\";\nimport type {\n Ingredient,\n CategorizedIngredients,\n AddedRecipe,\n AddedIngredient,\n} from \"../types\";\nimport { addQuantities } from \"../units\";\n\n/**\n * Represents a shopping list.\n * @category Classes\n */\nexport class ShoppingList {\n /**\n * The ingredients in the shopping list.\n * @see {@link Ingredient}\n */\n ingredients: Ingredient[] = [];\n /**\n * The recipes in the shopping list.\n * @see {@link AddedRecipe}\n */\n recipes: AddedRecipe[] = [];\n /**\n * The aisle configuration for the shopping list.\n * @see {@link AisleConfig}\n */\n aisle_config?: AisleConfig;\n /**\n * The categorized ingredients in the shopping list.\n * @see {@link CategorizedIngredients}\n */\n categories?: CategorizedIngredients;\n\n /**\n * Creates a new ShoppingList instance.\n * @param aisle_config_str - The aisle configuration to parse.\n */\n constructor(aisle_config_str?: string) {\n if (aisle_config_str) {\n this.set_aisle_config(aisle_config_str);\n }\n }\n\n private calculate_ingredients() {\n this.ingredients = [];\n for (const { recipe, factor } of this.recipes) {\n const scaledRecipe = factor === 1 ? recipe : recipe.scaleBy(factor);\n for (const ingredient of scaledRecipe.ingredients) {\n if (ingredient.hidden) {\n continue;\n }\n\n const existingIngredient = this.ingredients.find(\n (i) => i.name === ingredient.name,\n );\n\n let addSeparate = false;\n try {\n if (existingIngredient) {\n if (existingIngredient.quantity && ingredient.quantity) {\n const newQuantity = addQuantities(\n {\n value: existingIngredient.quantity,\n unit: existingIngredient.unit ?? \"\",\n },\n {\n value: ingredient.quantity,\n unit: ingredient.unit ?? \"\",\n },\n );\n existingIngredient.quantity = newQuantity.value;\n if (newQuantity.unit) {\n existingIngredient.unit = newQuantity.unit;\n }\n } else if (ingredient.quantity) {\n existingIngredient.quantity = ingredient.quantity;\n if (ingredient.unit) {\n existingIngredient.unit = ingredient.unit;\n }\n }\n }\n } catch {\n // Cannot add quantities, adding as separate ingredients\n addSeparate = true;\n }\n\n if (!existingIngredient || addSeparate) {\n const newIngredient: AddedIngredient = { name: ingredient.name };\n if (ingredient.quantity) {\n newIngredient.quantity = ingredient.quantity;\n }\n if (ingredient.unit) {\n newIngredient.unit = ingredient.unit;\n }\n this.ingredients.push(newIngredient);\n }\n }\n }\n }\n\n /**\n * Adds a recipe to the shopping list.\n * @param recipe - The recipe to add.\n * @param factor - The factor to scale the recipe by.\n */\n add_recipe(recipe: Recipe, factor: number = 1) {\n this.recipes.push({ recipe, factor });\n this.calculate_ingredients();\n this.categorize();\n }\n\n /**\n * Removes a recipe from the shopping list.\n * @param index - The index of the recipe to remove.\n */\n remove_recipe(index: number) {\n if (index < 0 || index >= this.recipes.length) {\n throw new Error(\"Index out of bounds\");\n }\n this.recipes.splice(index, 1);\n this.calculate_ingredients();\n this.categorize();\n }\n\n /**\n * Sets the aisle configuration for the shopping list.\n * @param config - The aisle configuration to parse.\n */\n set_aisle_config(config: string) {\n this.aisle_config = new AisleConfig(config);\n this.categorize();\n }\n\n /**\n * Categorizes the ingredients in the shopping list\n * Will use the aisle config if any, otherwise all ingredients will be placed in the \"other\" category\n */\n categorize() {\n if (!this.aisle_config) {\n this.categories = { other: this.ingredients };\n return;\n }\n\n const categories: CategorizedIngredients = { other: [] };\n for (const category of this.aisle_config.categories) {\n categories[category.name] = [];\n }\n\n for (const ingredient of this.ingredients) {\n let found = false;\n for (const category of this.aisle_config.categories) {\n for (const aisleIngredient of category.ingredients) {\n if (aisleIngredient.aliases.includes(ingredient.name)) {\n categories[category.name]!.push(ingredient);\n found = true;\n break;\n }\n }\n if (found) {\n break;\n }\n }\n if (!found) {\n categories.other!.push(ingredient);\n }\n }\n\n this.categories = categories;\n }\n}\n"],"mappings":"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","names":["units"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/classes/aisle_config.ts","../src/classes/section.ts","../node_modules/.pnpm/human-regex@2.1.5_patch_hash=6d6bd9e233f99785a7c2187fd464edc114b76d47001dbb4eb6b5d72168de7460/node_modules/human-regex/src/human-regex.ts","../src/regex.ts","../src/units.ts","../src/parser_helpers.ts","../src/classes/recipe.ts","../src/classes/shopping_list.ts"],"sourcesContent":["import { AisleConfig } from \"./classes/aisle_config\";\nimport { Recipe } from \"./classes/recipe\";\nimport { ShoppingList } from \"./classes/shopping_list\";\nimport { Section } from \"./classes/section\";\n\nimport type {\n Metadata,\n MetadataExtract,\n Ingredient,\n FixedValue,\n Range,\n DecimalValue,\n FractionValue,\n TextValue,\n Timer,\n TextItem,\n IngredientItem,\n CookwareItem,\n TimerItem,\n Item,\n Step,\n Note,\n Cookware,\n CategorizedIngredients,\n AddedRecipe,\n AisleIngredient,\n AisleCategory,\n} from \"./types\";\n\nexport {\n Recipe,\n ShoppingList,\n AisleConfig,\n Metadata,\n MetadataExtract,\n Ingredient,\n FixedValue,\n Range,\n DecimalValue,\n FractionValue,\n TextValue,\n Timer,\n TextItem,\n IngredientItem,\n CookwareItem,\n TimerItem,\n Item,\n Step,\n Note,\n Cookware,\n CategorizedIngredients,\n AddedRecipe,\n AisleIngredient,\n AisleCategory,\n Section,\n};\n","import type { AisleCategory, AisleIngredient } from \"../types\";\n\n/**\n * Represents the aisle configuration for a shopping list.\n * @category Classes\n */\nexport class AisleConfig {\n /**\n * The categories of aisles.\n * @see {@link AisleCategory}\n */\n categories: AisleCategory[] = [];\n\n /**\n * Creates a new AisleConfig instance.\n * @param config - The aisle configuration to parse.\n */\n constructor(config?: string) {\n if (config) {\n this.parse(config);\n }\n }\n\n /**\n * Parses an aisle configuration from a string.\n * @param config - The aisle configuration to parse.\n */\n parse(config: string) {\n let currentCategory: AisleCategory | null = null;\n const categoryNames = new Set<string>();\n const ingredientNames = new Set<string>();\n\n for (const line of config.split(\"\\n\")) {\n const trimmedLine = line.trim();\n\n if (trimmedLine.length === 0) {\n continue;\n }\n\n if (trimmedLine.startsWith(\"[\") && trimmedLine.endsWith(\"]\")) {\n const categoryName = trimmedLine\n .substring(1, trimmedLine.length - 1)\n .trim();\n\n if (categoryNames.has(categoryName)) {\n throw new Error(`Duplicate category found: ${categoryName}`);\n }\n categoryNames.add(categoryName);\n\n currentCategory = { name: categoryName, ingredients: [] };\n this.categories.push(currentCategory);\n } else {\n if (currentCategory === null) {\n throw new Error(\n `Ingredient found without a category: ${trimmedLine}`,\n );\n }\n\n const aliases = trimmedLine.split(\"|\").map((s) => s.trim());\n for (const alias of aliases) {\n if (ingredientNames.has(alias)) {\n throw new Error(`Duplicate ingredient/alias found: ${alias}`);\n }\n ingredientNames.add(alias);\n }\n\n const ingredient: AisleIngredient = {\n name: aliases[0]!, // We know this exists because trimmedLine is not empty\n aliases: aliases,\n };\n currentCategory.ingredients.push(ingredient);\n }\n }\n }\n}\n","import type { Step, Note } from \"../types\";\n\n/**\n * Represents a recipe section\n * Wrapped as a Class and not as a simple type to expose some useful helper\n * classes (e.g. `isBlank()`)\n * @category Types\n */\nexport class Section {\n /** The name of the section. Can be an empty string for the default (first) section. */\n name: string;\n /** An array of steps and notes that make up the content of the section. */\n content: (Step | Note)[] = [];\n\n /**\n * Creates an instance of Section.\n * @param name - The name of the section. Defaults to an empty string.\n */\n constructor(name: string = \"\") {\n this.name = name;\n }\n\n /**\n * Checks if the section is blank (has no name and no content).\n * Used during recipe parsing\n * @returns `true` if the section is blank, otherwise `false`.\n */\n isBlank(): boolean {\n return this.name === \"\" && this.content.length === 0;\n }\n}\n","type PartialBut<T, K extends keyof T> = Partial<T> & Pick<T, K>;\n\nconst escapeCache = new Map<string, string>();\n\nconst Flags = {\n GLOBAL: \"g\",\n NON_SENSITIVE: \"i\",\n MULTILINE: \"m\",\n DOT_ALL: \"s\",\n UNICODE: \"u\",\n STICKY: \"y\",\n} as const;\n\nconst Ranges = Object.freeze({\n digit: \"0-9\",\n lowercaseLetter: \"a-z\",\n uppercaseLetter: \"A-Z\",\n letter: \"a-zA-Z\",\n alphanumeric: \"a-zA-Z0-9\",\n anyCharacter: \".\",\n});\n\ntype RangeKeys = keyof typeof Ranges;\n\nconst Quantifiers = Object.freeze({\n zeroOrMore: \"*\",\n oneOrMore: \"+\",\n optional: \"?\",\n});\n\ntype Quantifiers =\n | \"exactly\"\n | \"atLeast\"\n | \"atMost\"\n | \"between\"\n | \"oneOrMore\"\n | \"zeroOrMore\"\n | \"repeat\";\ntype QuantifierMethods = Quantifiers | \"optional\" | \"lazy\";\n\ntype WithLazy = HumanRegex;\ntype Base = Omit<HumanRegex, \"lazy\">;\ntype AtStart = Omit<Base, QuantifierMethods | \"endGroup\">;\ntype AfterAnchor = Omit<Base, QuantifierMethods | \"or\">;\ntype SimpleQuantifier = Omit<Base, Quantifiers>;\ntype LazyQuantifier = Omit<WithLazy, Quantifiers>;\n\nclass HumanRegex {\n private parts: string[];\n private flags: Set<string>;\n\n constructor() {\n this.parts = [];\n this.flags = new Set<string>();\n }\n\n digit(): Base {\n return this.add(\"\\\\d\");\n }\n\n special(): Base {\n return this.add(\"(?=.*[!@#$%^&*])\");\n }\n\n word(): Base {\n return this.add(\"\\\\w\");\n }\n\n whitespace(): Base {\n return this.add(\"\\\\s\");\n }\n\n nonWhitespace(): Base {\n return this.add(\"\\\\S\");\n }\n\n literal(text: string): this {\n return this.add(escapeLiteral(text));\n }\n\n or(): AfterAnchor {\n return this.add(\"|\");\n }\n\n range(name: RangeKeys): Base {\n const range = Ranges[name];\n if (!range) throw new Error(`Unknown range: ${name}`);\n return this.add(`[${range}]`);\n }\n\n notRange(name: RangeKeys): Base {\n const range = Ranges[name];\n if (!range) throw new Error(`Unknown range: ${name}`);\n return this.add(`[^${range}]`);\n }\n\n anyOf(chars: string): Base {\n return this.add(`[${chars}]`);\n }\n\n notAnyOf(chars: string): Base {\n return this.add(`[^${chars}]`);\n }\n\n lazy(): Base {\n const lastPart = this.parts.pop();\n if (!lastPart) throw new Error(\"No quantifier to make lazy\");\n return this.add(`${lastPart}?`);\n }\n\n letter(): Base {\n return this.add(\"[a-zA-Z]\");\n }\n\n anyCharacter(): Base {\n return this.add(\".\");\n }\n\n newline(): Base {\n return this.add(\"(?:\\\\r\\\\n|\\\\r|\\\\n)\"); // Windows: \\r\\n, Unix: \\n, Old Macs: \\r\n }\n\n negativeLookahead(pattern: string): Base {\n return this.add(`(?!${pattern})`);\n }\n\n positiveLookahead(pattern: string): Base {\n return this.add(`(?=${pattern})`);\n }\n\n positiveLookbehind(pattern: string): Base {\n return this.add(`(?<=${pattern})`);\n }\n\n negativeLookbehind(pattern: string): Base {\n return this.add(`(?<!${pattern})`);\n }\n\n hasSpecialCharacter(): Base {\n return this.add(\"(?=.*[!@#$%^&*])\");\n }\n\n hasDigit(): Base {\n return this.add(\"(?=.*\\\\d)\");\n }\n\n hasLetter(): Base {\n return this.add(\"(?=.*[a-zA-Z])\");\n }\n\n optional(): SimpleQuantifier {\n return this.add(Quantifiers.optional);\n }\n\n exactly(n: number): SimpleQuantifier {\n return this.add(`{${n}}`);\n }\n\n atLeast(n: number): LazyQuantifier {\n return this.add(`{${n},}`);\n }\n\n atMost(n: number): LazyQuantifier {\n return this.add(`{0,${n}}`);\n }\n\n between(min: number, max: number): LazyQuantifier {\n return this.add(`{${min},${max}}`);\n }\n\n oneOrMore(): LazyQuantifier {\n return this.add(Quantifiers.oneOrMore);\n }\n\n zeroOrMore(): LazyQuantifier {\n return this.add(Quantifiers.zeroOrMore);\n }\n\n startNamedGroup(name: string): AfterAnchor {\n return this.add(`(?<${name}>`);\n }\n\n startGroup(): AfterAnchor {\n return this.add(\"(?:\");\n }\n\n startCaptureGroup(): AfterAnchor {\n return this.add(\"(\");\n }\n\n wordBoundary(): Base {\n return this.add(\"\\\\b\");\n }\n\n nonWordBoundary(): Base {\n return this.add(\"\\\\B\");\n }\n\n endGroup(): Base {\n return this.add(\")\");\n }\n\n startAnchor(): AfterAnchor {\n return this.add(\"^\");\n }\n\n endAnchor(): AfterAnchor {\n return this.add(\"$\");\n }\n\n global(): this {\n this.flags.add(Flags.GLOBAL);\n return this;\n }\n\n nonSensitive(): this {\n this.flags.add(Flags.NON_SENSITIVE);\n return this;\n }\n\n multiline(): this {\n this.flags.add(Flags.MULTILINE);\n return this;\n }\n\n dotAll(): this {\n this.flags.add(Flags.DOT_ALL);\n return this;\n }\n\n sticky(): this {\n this.flags.add(Flags.STICKY);\n return this;\n }\n\n unicodeChar(variant?: \"u\" | \"l\" | \"t\" | \"m\" | \"o\"): Base {\n this.flags.add(Flags.UNICODE);\n const validVariants = new Set([\"u\", \"l\", \"t\", \"m\", \"o\"] as const);\n\n if (variant !== undefined && !validVariants.has(variant)) {\n throw new Error(`Invalid Unicode letter variant: ${variant}`);\n }\n\n return this.add(`\\\\p{L${variant ?? \"\"}}`);\n }\n\n unicodeDigit(): Base {\n this.flags.add(Flags.UNICODE);\n return this.add(\"\\\\p{N}\");\n }\n\n unicodePunctuation(): Base {\n this.flags.add(Flags.UNICODE);\n return this.add(\"\\\\p{P}\");\n }\n\n unicodeSymbol(): Base {\n this.flags.add(Flags.UNICODE);\n return this.add(\"\\\\p{S}\");\n }\n\n repeat(count: number): Base {\n if (this.parts.length === 0) {\n throw new Error(\"No pattern to repeat\");\n }\n\n const lastPart = this.parts.pop();\n this.parts.push(`(${lastPart}){${count}}`);\n return this;\n }\n\n ipv4Octet(): Base {\n return this.add(\"(25[0-5]|2[0-4]\\\\d|1\\\\d\\\\d|[1-9]\\\\d|\\\\d)\");\n }\n\n protocol(): Base {\n return this.add(\"https?://\");\n }\n\n www(): Base {\n return this.add(\"(www\\\\.)?\");\n }\n\n tld(): Base {\n return this.add(\"(com|org|net)\");\n }\n\n path(): Base {\n return this.add(\"(/\\\\w+)*\");\n }\n\n private add(part: string): this {\n this.parts.push(part);\n return this;\n }\n\n toString(): string {\n return this.parts.join(\"\");\n }\n\n toRegExp(): RegExp {\n return new RegExp(this.toString(), [...this.flags].join(\"\"));\n }\n}\n\nfunction escapeLiteral(text: string): string {\n if (!escapeCache.has(text)) {\n escapeCache.set(text, text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"));\n }\n return escapeCache.get(text)!;\n}\n\nconst createRegex = (): AtStart => new HumanRegex();\n\nconst Patterns = (() => {\n const createCachedPattern = (\n builder: () => PartialBut<HumanRegex, \"toRegExp\">\n ) => {\n const regex = builder().toRegExp();\n return () => new RegExp(regex.source, regex.flags);\n };\n\n return {\n email: createCachedPattern(() =>\n createRegex()\n .startAnchor()\n .word()\n .oneOrMore()\n .literal(\"@\")\n .word()\n .oneOrMore()\n .startGroup()\n .literal(\".\")\n .word()\n .oneOrMore()\n .endGroup()\n .zeroOrMore()\n .literal(\".\")\n .letter()\n .atLeast(2)\n .endAnchor()\n ),\n url: createCachedPattern(() =>\n createRegex()\n .startAnchor()\n .protocol()\n .www()\n .word()\n .oneOrMore()\n .literal(\".\")\n .tld()\n .path()\n .endAnchor()\n ),\n phoneInternational: createCachedPattern(() =>\n createRegex()\n .startAnchor()\n .literal(\"+\")\n .digit()\n .between(1, 3)\n .literal(\"-\")\n .digit()\n .between(3, 14)\n .endAnchor()\n ),\n };\n})();\n\nexport { createRegex, Patterns, Flags, Ranges, Quantifiers };\n","import { createRegex } from \"human-regex\";\n\nexport const metadataRegex = createRegex()\n .literal(\"---\").newline()\n .startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup()\n .newline().literal(\"---\")\n .dotAll().toRegExp();\n\nconst nonWordChar = \"\\\\s@#~\\\\[\\\\]{(.,;:!?\"\n\nconst multiwordIngredient = createRegex()\n .literal(\"@\")\n .startNamedGroup(\"mIngredientModifier\")\n .anyOf(\"@\\\\-&?\")\n .endGroup().optional()\n .startNamedGroup(\"mIngredientName\")\n .notAnyOf(nonWordChar).oneOrMore()\n .startGroup()\n .whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore()\n .endGroup().oneOrMore()\n .endGroup()\n .positiveLookahead(\"\\\\s*(?:\\\\{[^\\\\}]*\\\\}|\\\\([^)]*\\\\))\")\n .startGroup()\n .literal(\"{\")\n .startNamedGroup(\"mIngredientQuantity\")\n .notAnyOf(\"}%\").oneOrMore()\n .endGroup().optional()\n .startGroup()\n .literal(\"%\")\n .startNamedGroup(\"mIngredientUnits\")\n .notAnyOf(\"}\").oneOrMore().lazy()\n .endGroup()\n .endGroup().optional()\n .literal(\"}\")\n .endGroup().optional()\n .startGroup()\n .literal(\"(\")\n .startNamedGroup(\"mIngredientPreparation\")\n .notAnyOf(\")\").oneOrMore().lazy()\n .endGroup()\n .literal(\")\")\n .endGroup().optional()\n .toRegExp();\n\nconst singleWordIngredient = createRegex()\n .literal(\"@\")\n .startNamedGroup(\"sIngredientModifier\")\n .anyOf(\"@\\\\-&?\")\n .endGroup().optional()\n .startNamedGroup(\"sIngredientName\")\n .notAnyOf(nonWordChar).oneOrMore()\n .endGroup()\n .startGroup()\n .literal(\"{\")\n .startNamedGroup(\"sIngredientQuantity\")\n .notAnyOf(\"}%\").oneOrMore()\n .endGroup().optional()\n .startGroup()\n .literal(\"%\")\n .startNamedGroup(\"sIngredientUnits\")\n .notAnyOf(\"}\").oneOrMore().lazy()\n .endGroup()\n .endGroup().optional()\n .literal(\"}\")\n .endGroup().optional()\n .startGroup()\n .literal(\"(\")\n .startNamedGroup(\"sIngredientPreparation\")\n .notAnyOf(\")\").oneOrMore().lazy()\n .endGroup()\n .literal(\")\")\n .endGroup().optional()\n .toRegExp();\n\nconst multiwordCookware = createRegex()\n .literal(\"#\")\n .startNamedGroup(\"mCookwareModifier\")\n .anyOf(\"\\\\-&?\")\n .endGroup().optional()\n .startNamedGroup(\"mCookwareName\")\n .notAnyOf(nonWordChar).oneOrMore()\n .startGroup()\n .whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore()\n .endGroup().oneOrMore()\n .endGroup().positiveLookahead(\"\\\\s*(?:\\\\{[^\\\\}]*\\\\})\")\n .literal(\"{\")\n .startNamedGroup(\"mCookwareQuantity\")\n .anyCharacter().zeroOrMore().lazy()\n .endGroup()\n .literal(\"}\")\n .toRegExp();\n\nconst singleWordCookware = createRegex()\n .literal(\"#\")\n .startNamedGroup(\"sCookwareModifier\")\n .anyOf(\"\\\\-&?\")\n .endGroup().optional()\n .startNamedGroup(\"sCookwareName\")\n .notAnyOf(nonWordChar).oneOrMore()\n .endGroup()\n .startGroup()\n .literal(\"{\")\n .startNamedGroup(\"sCookwareQuantity\")\n .anyCharacter().zeroOrMore().lazy()\n .endGroup()\n .literal(\"}\")\n .endGroup().optional()\n .toRegExp();\n\nconst timer = createRegex()\n .literal(\"~\")\n .startNamedGroup(\"timerName\")\n .anyCharacter().zeroOrMore().lazy()\n .endGroup()\n .literal(\"{\")\n .startNamedGroup(\"timerQuantity\")\n .anyCharacter().oneOrMore().lazy()\n .endGroup()\n .startGroup()\n .literal(\"%\")\n .startNamedGroup(\"timerUnits\")\n .anyCharacter().oneOrMore().lazy()\n .endGroup()\n .endGroup().optional()\n .literal(\"}\")\n .toRegExp()\n\nexport const tokensRegex = new RegExp(\n [\n multiwordIngredient,\n singleWordIngredient,\n multiwordCookware,\n singleWordCookware,\n timer,\n ]\n .map((r) => r.source)\n .join(\"|\"),\n \"gu\",\n);\n\nexport const commentRegex = createRegex()\n .literal(\"--\")\n .anyCharacter().zeroOrMore()\n .global()\n .toRegExp();\n\nexport const blockCommentRegex = createRegex()\n .whitespace().zeroOrMore()\n .literal(\"[-\")\n .anyCharacter().zeroOrMore().lazy()\n .literal(\"-]\")\n .whitespace().zeroOrMore()\n .global()\n .toRegExp();\n\nexport const shoppingListRegex = createRegex()\n .literal(\"[\")\n .startNamedGroup(\"name\")\n .anyCharacter().oneOrMore()\n .endGroup()\n .literal(\"]\")\n .newline()\n .startNamedGroup(\"items\")\n .anyCharacter().zeroOrMore().lazy()\n .endGroup()\n .startGroup()\n .newline().newline()\n .or()\n .endAnchor()\n .endGroup()\n .global()\n .toRegExp()\n\nexport const rangeRegex = createRegex()\n .startAnchor()\n .digit().oneOrMore()\n .startGroup()\n .anyOf(\".,/\").exactly(1)\n .digit().oneOrMore()\n .endGroup().optional()\n .literal(\"-\")\n .startGroup()\n .anyOf(\".,/\").exactly(1)\n .digit().oneOrMore()\n .endGroup().optional()\n .endAnchor()\n .toRegExp()\n\nexport const numberLikeRegex = createRegex()\n .startAnchor()\n .digit().oneOrMore()\n .startGroup()\n .anyOf(\".,/\").exactly(1)\n .digit().oneOrMore()\n .endGroup().optional()\n .endAnchor()\n .toRegExp()","import type { FixedValue, Range, DecimalValue, FractionValue } from \"./types\";\nexport type UnitType = \"mass\" | \"volume\" | \"count\";\nexport type UnitSystem = \"metric\" | \"imperial\";\n\nexport interface UnitDefinition {\n name: string; // canonical name, e.g. 'g'\n type: UnitType;\n system: UnitSystem;\n aliases: string[]; // e.g. ['gram', 'grams']\n toBase: number; // conversion factor to the base unit of its type\n}\n\nexport interface Quantity {\n value: FixedValue | Range;\n unit: string;\n}\n\n// Base units: mass -> gram (g), volume -> milliliter (ml)\nconst units: UnitDefinition[] = [\n // Mass (Metric)\n {\n name: \"g\",\n type: \"mass\",\n system: \"metric\",\n aliases: [\"gram\", \"grams\"],\n toBase: 1,\n },\n {\n name: \"kg\",\n type: \"mass\",\n system: \"metric\",\n aliases: [\"kilogram\", \"kilograms\"],\n toBase: 1000,\n },\n // Mass (Imperial)\n {\n name: \"oz\",\n type: \"mass\",\n system: \"imperial\",\n aliases: [\"ounce\", \"ounces\"],\n toBase: 28.3495,\n },\n {\n name: \"lb\",\n type: \"mass\",\n system: \"imperial\",\n aliases: [\"pound\", \"pounds\"],\n toBase: 453.592,\n },\n\n // Volume (Metric)\n {\n name: \"ml\",\n type: \"volume\",\n system: \"metric\",\n aliases: [\"milliliter\", \"milliliters\", \"millilitre\", \"millilitres\"],\n toBase: 1,\n },\n {\n name: \"l\",\n type: \"volume\",\n system: \"metric\",\n aliases: [\"liter\", \"liters\", \"litre\", \"litres\"],\n toBase: 1000,\n },\n {\n name: \"tsp\",\n type: \"volume\",\n system: \"metric\",\n aliases: [\"teaspoon\", \"teaspoons\"],\n toBase: 5,\n },\n {\n name: \"tbsp\",\n type: \"volume\",\n system: \"metric\",\n aliases: [\"tablespoon\", \"tablespoons\"],\n toBase: 15,\n },\n\n // Volume (Imperial)\n {\n name: \"fl-oz\",\n type: \"volume\",\n system: \"imperial\",\n aliases: [\"fluid ounce\", \"fluid ounces\"],\n toBase: 29.5735,\n },\n {\n name: \"cup\",\n type: \"volume\",\n system: \"imperial\",\n aliases: [\"cups\"],\n toBase: 236.588,\n },\n {\n name: \"pint\",\n type: \"volume\",\n system: \"imperial\",\n aliases: [\"pints\"],\n toBase: 473.176,\n },\n {\n name: \"quart\",\n type: \"volume\",\n system: \"imperial\",\n aliases: [\"quarts\"],\n toBase: 946.353,\n },\n {\n name: \"gallon\",\n type: \"volume\",\n system: \"imperial\",\n aliases: [\"gallons\"],\n toBase: 3785.41,\n },\n\n // Count units (no conversion, but recognized as a type)\n {\n name: \"piece\",\n type: \"count\",\n system: \"metric\",\n aliases: [\"pieces\"],\n toBase: 1,\n },\n];\n\nconst unitMap = new Map<string, UnitDefinition>();\nfor (const unit of units) {\n unitMap.set(unit.name.toLowerCase(), unit);\n for (const alias of unit.aliases) {\n unitMap.set(alias.toLowerCase(), unit);\n }\n}\n\nexport function normalizeUnit(unit: string): UnitDefinition | undefined {\n return unitMap.get(unit.toLowerCase().trim());\n}\n\nexport class CannotAddTextValueError extends Error {\n constructor() {\n super(\"Cannot add a quantity with a text value.\");\n this.name = \"CannotAddTextValueError\";\n }\n}\n\nexport class IncompatibleUnitsError extends Error {\n constructor(unit1: string, unit2: string) {\n super(\n `Cannot add quantities with incompatible or unknown units: ${unit1} and ${unit2}`,\n );\n this.name = \"IncompatibleUnitsError\";\n }\n}\n\nfunction gcd(a: number, b: number): number {\n return b === 0 ? a : gcd(b, a % b);\n}\n\nexport function simplifyFraction(\n num: number,\n den: number,\n): DecimalValue | FractionValue {\n if (den === 0) {\n throw new Error(\"Denominator cannot be zero.\");\n }\n\n const commonDivisor = gcd(Math.abs(num), Math.abs(den));\n let simplifiedNum = num / commonDivisor;\n let simplifiedDen = den / commonDivisor;\n if (simplifiedDen < 0) {\n simplifiedNum = -simplifiedNum;\n simplifiedDen = -simplifiedDen;\n }\n\n if (simplifiedDen === 1) {\n return { type: \"decimal\", value: simplifiedNum };\n } else {\n return { type: \"fraction\", num: simplifiedNum, den: simplifiedDen };\n }\n}\n\nexport function multiplyNumericValue(\n v: DecimalValue | FractionValue,\n factor: number,\n): DecimalValue | FractionValue {\n if (v.type === \"decimal\") {\n return { type: \"decimal\", value: v.value * factor };\n }\n return simplifyFraction(v.num * factor, v.den);\n}\n\nexport function addNumericValues(\n val1: DecimalValue | FractionValue,\n val2: DecimalValue | FractionValue,\n): DecimalValue | FractionValue {\n let num1: number;\n let den1: number;\n let num2: number;\n let den2: number;\n\n if (val1.type === \"decimal\") {\n num1 = val1.value;\n den1 = 1;\n } else {\n num1 = val1.num;\n den1 = val1.den;\n }\n\n if (val2.type === \"decimal\") {\n num2 = val2.value;\n den2 = 1;\n } else {\n num2 = val2.num;\n den2 = val2.den;\n }\n\n // We only return a fraction where both input values are fractions themselves\n if (val1.type === \"fraction\" && val2.type === \"fraction\") {\n const commonDen = den1 * den2;\n const sumNum = num1 * den2 + num2 * den1;\n return simplifyFraction(sumNum, commonDen);\n } else {\n return { type: \"decimal\", value: num1 / den1 + num2 / den2 };\n }\n}\n\nconst toRoundedDecimal = (v: DecimalValue | FractionValue): DecimalValue => {\n const value = v.type === \"decimal\" ? v.value : v.num / v.den;\n return { type: \"decimal\", value: Math.floor(value * 100) / 100 };\n};\n\nexport function multiplyQuantityValue(\n value: FixedValue | Range,\n factor: number,\n): FixedValue | Range {\n if (value.type === \"fixed\") {\n return {\n type: \"fixed\",\n value: toRoundedDecimal(\n multiplyNumericValue(\n value.value as DecimalValue | FractionValue,\n factor,\n ),\n ),\n };\n }\n\n return {\n type: \"range\",\n min: toRoundedDecimal(\n multiplyNumericValue(value.min as DecimalValue | FractionValue, factor),\n ),\n max: toRoundedDecimal(\n multiplyNumericValue(value.max as DecimalValue | FractionValue, factor),\n ),\n };\n}\n\nconst convertQuantityValue = (\n value: FixedValue | Range,\n def: UnitDefinition,\n targetDef: UnitDefinition,\n): FixedValue | Range => {\n if (def.name === targetDef.name) return value;\n\n const factor = def.toBase / targetDef.toBase;\n\n return multiplyQuantityValue(value, factor);\n};\n\n/**\n * Adds two quantities, returning the result in the most appropriate unit.\n */\nexport function addQuantities(q1: Quantity, q2: Quantity): Quantity {\n const v1 = q1.value;\n const v2 = q2.value;\n\n // Case 1: one of the two values is a text, we throw an error we can catch on the other end\n if (\n (v1.type === \"fixed\" && v1.value.type === \"text\") ||\n (v2.type === \"fixed\" && v2.value.type === \"text\")\n ) {\n throw new CannotAddTextValueError();\n }\n\n const unit1Def = normalizeUnit(q1.unit);\n const unit2Def = normalizeUnit(q2.unit);\n\n const addQuantityValuesAndSetUnit = (\n val1: FixedValue | Range,\n val2: FixedValue | Range,\n unit: string,\n ): Quantity => {\n if (val1.type === \"fixed\" && val2.type === \"fixed\") {\n const res = addNumericValues(\n val1.value as DecimalValue | FractionValue,\n val2.value as DecimalValue | FractionValue,\n );\n return { value: { type: \"fixed\", value: res }, unit };\n }\n const r1 =\n val1.type === \"range\"\n ? val1\n : { type: \"range\", min: val1.value, max: val1.value };\n const r2 =\n val2.type === \"range\"\n ? val2\n : { type: \"range\", min: val2.value, max: val2.value };\n const newMin = addNumericValues(\n r1.min as DecimalValue | FractionValue,\n r2.min as DecimalValue | FractionValue,\n );\n const newMax = addNumericValues(\n r1.max as DecimalValue | FractionValue,\n r2.max as DecimalValue | FractionValue,\n );\n return { value: { type: \"range\", min: newMin, max: newMax }, unit };\n };\n\n // Case 2: one of the two values doesn't have a unit, we consider its value to be 0 and the unit to be that of the other one\n if (q1.unit === \"\" && unit2Def) {\n return addQuantityValuesAndSetUnit(v1, v2, q2.unit); // Prefer q2's unit\n }\n if (q2.unit === \"\" && unit1Def) {\n return addQuantityValuesAndSetUnit(v1, v2, q1.unit); // Prefer q1's unit\n }\n\n // Case 3: the two quantities have the exact same unit\n if (q1.unit.toLowerCase() === q2.unit.toLowerCase()) {\n return addQuantityValuesAndSetUnit(v1, v2, q1.unit);\n }\n\n // Case 4: the two quantities do not have the same unit\n if (unit1Def && unit2Def) {\n // Case 4.1: different unit type => we can't add quantities\n\n if (unit1Def.type !== unit2Def.type) {\n throw new IncompatibleUnitsError(\n `${unit1Def.type} (${q1.unit})`,\n `${unit2Def.type} (${q2.unit})`,\n );\n }\n\n let targetUnitDef: UnitDefinition;\n\n // Case 4.2: same unit type but different system => we convert to metric\n if (unit1Def.system !== unit2Def.system) {\n const metricUnitDef = unit1Def.system === \"metric\" ? unit1Def : unit2Def;\n targetUnitDef = units\n .filter((u) => u.type === metricUnitDef.type && u.system === \"metric\")\n .reduce((prev, current) =>\n prev.toBase > current.toBase ? prev : current,\n );\n }\n // Case 4.3: same unit type, same system but different unit => we use the biggest unit of the two\n else {\n targetUnitDef = unit1Def.toBase >= unit2Def.toBase ? unit1Def : unit2Def;\n }\n const convertedV1 = convertQuantityValue(v1, unit1Def, targetUnitDef);\n const convertedV2 = convertQuantityValue(v2, unit2Def, targetUnitDef);\n\n return addQuantityValuesAndSetUnit(\n convertedV1,\n convertedV2,\n targetUnitDef.name,\n );\n }\n\n throw new IncompatibleUnitsError(q1.unit, q2.unit);\n}\n","import type {\n MetadataExtract,\n Metadata,\n FixedValue,\n Range,\n TextValue,\n DecimalValue,\n FractionValue,\n} from \"./types\";\nimport { metadataRegex, rangeRegex, numberLikeRegex } from \"./regex\";\nimport { Section as SectionObject } from \"./classes/section\";\nimport type { Ingredient, Note, Step, Cookware } from \"./types\";\nimport {\n addQuantities,\n CannotAddTextValueError,\n IncompatibleUnitsError,\n Quantity,\n} from \"./units\";\n\n/**\n * Finds an item in a list or adds it if not present, then returns its index.\n * @param list The list to search in.\n * @param finder A predicate to find the item.\n * @param creator A function to create the item if not found.\n * @returns The index of the item in the list.\n */\nexport function findOrPush<T>(\n list: T[],\n finder: (elem: T) => boolean,\n creator: () => T,\n): number {\n let index = list.findIndex(finder);\n if (index === -1) {\n index = list.push(creator()) - 1;\n }\n return index;\n}\n\n/**\n * Pushes a pending note to the section content if it's not empty.\n * @param section The current section object.\n * @param note The note content.\n * @returns An empty string if the note was pushed, otherwise the original note.\n */\nexport function flushPendingNote(\n section: SectionObject,\n note: Note[\"note\"],\n): Note[\"note\"] {\n if (note.length > 0) {\n section.content.push({ note });\n return \"\";\n }\n return note;\n}\n\n/**\n * Pushes pending step items and a pending note to the section content.\n * @param section The current section object.\n * @param items The list of step items. This array will be cleared.\n * @returns true if the items were pushed, otherwise false.\n */\nexport function flushPendingItems(\n section: SectionObject,\n items: Step[\"items\"],\n): boolean {\n if (items.length > 0) {\n section.content.push({ items: [...items] });\n items.length = 0;\n return true;\n }\n return false;\n}\n\n/**\n * Finds an ingredient in the list (case-insensitively) and updates it, or adds it if not present.\n * This function mutates the `ingredients` array.\n * @param ingredients The list of ingredients.\n * @param newIngredient The ingredient to find or add.\n * @param isReference Whether this is a reference ingredient (`&` modifier).\n * @returns The index of the ingredient in the list.\n */\nexport function findAndUpsertIngredient(\n ingredients: Ingredient[],\n newIngredient: Ingredient,\n isReference: boolean,\n): number {\n const { name, quantity, unit } = newIngredient;\n\n // New ingredient\n if (isReference) {\n const index = ingredients.findIndex(\n (i) => i.name.toLowerCase() === name.toLowerCase(),\n );\n\n if (index === -1) {\n throw new Error(\n `Referenced ingredient \"${name}\" not found. A referenced ingredient must be declared before being referenced with '&'.`,\n );\n }\n\n // Ingredient already exists, update it\n const existingIngredient = ingredients[index]!;\n if (quantity !== undefined) {\n const currentQuantity: Quantity = {\n value: existingIngredient.quantity ?? {\n type: \"fixed\",\n value: { type: \"decimal\", value: 0 },\n },\n unit: existingIngredient.unit ?? \"\",\n };\n const newQuantity = { value: quantity, unit: unit ?? \"\" };\n\n try {\n const total = addQuantities(currentQuantity, newQuantity);\n existingIngredient.quantity = total.value;\n existingIngredient.unit = total.unit || undefined;\n } catch (e) {\n if (\n e instanceof IncompatibleUnitsError ||\n e instanceof CannotAddTextValueError\n ) {\n // Addition not possible, so add as a new ingredient.\n return ingredients.push(newIngredient) - 1;\n }\n }\n }\n return index;\n }\n\n // Not a reference, so add as a new ingredient.\n return ingredients.push(newIngredient) - 1;\n}\n\nexport function findAndUpsertCookware(\n cookware: Cookware[],\n newCookware: Cookware,\n isReference: boolean,\n): number {\n const { name } = newCookware;\n\n if (isReference) {\n const index = cookware.findIndex(\n (i) => i.name.toLowerCase() === name.toLowerCase(),\n );\n\n if (index === -1) {\n throw new Error(\n `Referenced cookware \"${name}\" not found. A referenced cookware must be declared before being referenced with '&'.`,\n );\n }\n\n return index;\n }\n\n return cookware.push(newCookware) - 1;\n}\n\n// Parser when we know the input is either a number-like value\nconst parseFixedValue = (\n input_str: string,\n): TextValue | DecimalValue | FractionValue => {\n if (!numberLikeRegex.test(input_str)) {\n return { type: \"text\", value: input_str };\n }\n\n // After this we know that s is either a fraction or a decimal value\n const s = input_str.trim().replace(\",\", \".\");\n\n // fraction\n if (s.includes(\"/\")) {\n const parts = s.split(\"/\");\n\n const num = Number(parts[0]);\n const den = Number(parts[1]);\n\n return { type: \"fraction\", num, den };\n }\n\n // decimal\n return { type: \"decimal\", value: Number(s) };\n};\n\nexport function parseQuantityInput(input_str: string): FixedValue | Range {\n const clean_str = String(input_str).trim();\n\n if (rangeRegex.test(clean_str)) {\n const range_parts = clean_str.split(\"-\");\n // As we've tested for it, we know that we have Number-like Quantities to parse\n const min = parseFixedValue(range_parts[0]!.trim()) as\n | DecimalValue\n | FractionValue;\n const max = parseFixedValue(range_parts[1]!.trim()) as\n | DecimalValue\n | FractionValue;\n return { type: \"range\", min, max };\n }\n\n return { type: \"fixed\", value: parseFixedValue(clean_str) };\n}\n\nexport function parseSimpleMetaVar(content: string, varName: string) {\n const varMatch = content.match(\n new RegExp(`^${varName}:\\\\s*(.*(?:\\\\r?\\\\n\\\\s+.*)*)+`, \"m\"),\n );\n return varMatch\n ? varMatch[1]?.trim().replace(/\\s*\\r?\\n\\s+/g, \" \")\n : undefined;\n}\n\nexport function parseScalingMetaVar(content: string, varName: string) {\n const varMatch = content.match(\n new RegExp(`^${varName}:[\\\\t ]*(([^,\\\\n]*),? ?(?:.*)?)`, \"m\"),\n );\n if (!varMatch) return undefined;\n if (isNaN(Number(varMatch[2]?.trim()))) {\n throw new Error(\"Scaling variables should be numbers\");\n }\n return [Number(varMatch[2]?.trim()), varMatch[1]?.trim()];\n}\n\nexport function parseListMetaVar(content: string, varName: string) {\n // Handle both inline and YAML-style tags\n const listMatch = content.match(\n new RegExp(\n `^${varName}:\\\\s*(?:\\\\[([^\\\\]]*)\\\\]|((?:\\\\r?\\\\n\\\\s*-\\\\s*.+)+))`,\n \"m\",\n ),\n );\n if (!listMatch) return undefined;\n\n if (listMatch[1] !== undefined) {\n // Inline list: tags: [one, two, three]\n return listMatch[1].split(\",\").map((tag) => tag.trim());\n } else if (listMatch[2]) {\n // YAML list:\n // tags:\n // - one\n // - two\n return listMatch[2]\n .split(\"\\n\")\n .filter((line) => line.trim() !== \"\")\n .map((line) => line.replace(/^\\s*-\\s*/, \"\").trim());\n }\n}\n\nexport function extractMetadata(content: string): MetadataExtract {\n const metadata: Metadata = {};\n let servings: number | undefined = undefined;\n\n // Is there front-matter at all?\n const metadataContent = content.match(metadataRegex)?.[1];\n if (!metadataContent) {\n return { metadata };\n }\n\n // String metadata variables\n for (const metaVar of [\n \"title\",\n \"source\",\n \"source.name\",\n \"source.url\",\n \"author\",\n \"source.author\",\n \"prep time\",\n \"time.prep\",\n \"cook time\",\n \"time.cook\",\n \"time required\",\n \"time\",\n \"duration\",\n \"locale\",\n \"introduction\",\n \"description\",\n \"course\",\n \"category\",\n \"diet\",\n \"cuisine\",\n \"difficulty\",\n \"image\",\n \"picture\",\n ] as (keyof Metadata)[]) {\n const stringMetaValue: any = parseSimpleMetaVar(metadataContent, metaVar);\n if (stringMetaValue) metadata[metaVar] = stringMetaValue;\n }\n\n // String metadata variables\n for (const metaVar of [\"servings\", \"yield\", \"serves\"] as (keyof Metadata)[]) {\n const scalingMetaValue: any = parseScalingMetaVar(metadataContent, metaVar);\n if (scalingMetaValue && scalingMetaValue[1]) {\n metadata[metaVar] = scalingMetaValue[1];\n servings = scalingMetaValue[0];\n }\n }\n\n // List metadata variables\n for (const metaVar of [\"tags\", \"images\", \"pictures\"] as (keyof Metadata)[]) {\n const listMetaValue: any = parseListMetaVar(metadataContent, metaVar);\n if (listMetaValue) metadata[metaVar] = listMetaValue;\n }\n\n return { metadata, servings };\n}\n","import type {\n Metadata,\n Ingredient,\n IngredientItem,\n Timer,\n Step,\n Note,\n Cookware,\n MetadataExtract,\n} from \"../types\";\nimport { Section } from \"./section\";\nimport {\n tokensRegex,\n commentRegex,\n blockCommentRegex,\n metadataRegex,\n} from \"../regex\";\nimport {\n findOrPush,\n flushPendingItems,\n flushPendingNote,\n findAndUpsertIngredient,\n findAndUpsertCookware,\n parseQuantityInput,\n extractMetadata,\n} from \"../parser_helpers\";\nimport { multiplyQuantityValue } from \"../units\";\n\n/**\n * Represents a recipe.\n * @category Classes\n */\nexport class Recipe {\n /**\n * The recipe's metadata.\n * @see {@link Metadata}\n */\n metadata: Metadata = {};\n /**\n * The recipe's ingredients.\n * @see {@link Ingredient}\n */\n ingredients: Ingredient[] = [];\n /**\n * The recipe's sections.\n * @see {@link Section}\n */\n sections: Section[] = [];\n /**\n * The recipe's cookware.\n * @see {@link Cookware}\n */\n cookware: Cookware[] = [];\n /**\n * The recipe's timers.\n * @see {@link Timer}\n */\n timers: Timer[] = [];\n /**\n * The recipe's servings. Used for scaling\n */\n servings?: number;\n\n /**\n * Creates a new Recipe instance.\n * @param content - The recipe content to parse.\n */\n constructor(content?: string) {\n if (content) {\n this.parse(content);\n }\n }\n\n /**\n * Parses a recipe from a string.\n * @param content - The recipe content to parse.\n */\n parse(content: string) {\n const cleanContent = content\n .replace(metadataRegex, \"\")\n .replace(commentRegex, \"\")\n .replace(blockCommentRegex, \"\")\n .trim()\n .split(/\\r\\n?|\\n/);\n\n const { metadata, servings }: MetadataExtract = extractMetadata(content);\n this.metadata = metadata;\n this.servings = servings;\n\n let blankLineBefore = true;\n let section: Section = new Section();\n const items: Step[\"items\"] = [];\n let note: Note[\"note\"] = \"\";\n let inNote = false;\n\n for (const line of cleanContent) {\n if (line.trim().length === 0) {\n flushPendingItems(section, items);\n note = flushPendingNote(section, note);\n blankLineBefore = true;\n inNote = false;\n continue;\n }\n\n if (line.startsWith(\"=\")) {\n flushPendingItems(section, items);\n note = flushPendingNote(section, note);\n\n if (this.sections.length === 0 && section.isBlank()) {\n section.name = line.substring(1).trim();\n } else {\n if (!section.isBlank()) {\n this.sections.push(section);\n }\n section = new Section(line.substring(1).trim());\n }\n blankLineBefore = true;\n inNote = false;\n continue;\n }\n\n if (blankLineBefore && line.startsWith(\">\")) {\n flushPendingItems(section, items);\n note = flushPendingNote(section, note);\n note += line.substring(1).trim();\n inNote = true;\n blankLineBefore = false;\n continue;\n }\n\n if (inNote) {\n if (line.startsWith(\">\")) {\n note += \" \" + line.substring(1).trim();\n } else {\n note += \" \" + line.trim();\n }\n blankLineBefore = false;\n continue;\n }\n\n note = flushPendingNote(section, note);\n\n let cursor = 0;\n for (const match of line.matchAll(tokensRegex)) {\n const idx = match.index;\n if (idx > cursor) {\n items.push({ type: \"text\", value: line.slice(cursor, idx) });\n }\n\n const groups = match.groups!;\n\n if (groups.mIngredientName || groups.sIngredientName) {\n const name = (groups.mIngredientName || groups.sIngredientName)!;\n const quantityRaw =\n groups.mIngredientQuantity || groups.sIngredientQuantity;\n const units = groups.mIngredientUnits || groups.sIngredientUnits;\n const preparation =\n groups.mIngredientPreparation || groups.sIngredientPreparation;\n const modifier =\n groups.mIngredientModifier || groups.sIngredientModifier;\n const optional = modifier === \"?\";\n const hidden = modifier === \"-\";\n const reference = modifier === \"&\";\n const isRecipe = modifier === \"@\";\n const quantity = quantityRaw\n ? parseQuantityInput(quantityRaw)\n : undefined;\n\n const idxInList = findAndUpsertIngredient(\n this.ingredients,\n {\n name,\n quantity,\n unit: units,\n optional,\n hidden,\n preparation,\n isRecipe,\n },\n reference,\n );\n\n const newItem: IngredientItem = {\n type: \"ingredient\",\n value: idxInList,\n };\n if (reference) {\n newItem.partialQuantity = quantity;\n newItem.partialUnit = units;\n newItem.partialPreparation = preparation;\n }\n\n items.push(newItem);\n } else if (groups.mCookwareName || groups.sCookwareName) {\n const name = (groups.mCookwareName || groups.sCookwareName)!;\n const modifier = groups.mCookwareModifier || groups.sCookwareModifier;\n const quantityRaw =\n groups.mCookwareQuantity || groups.sCookwareQuantity;\n const optional = modifier === \"?\";\n const hidden = modifier === \"-\";\n const reference = modifier === \"&\";\n const quantity = quantityRaw\n ? parseQuantityInput(quantityRaw)\n : undefined;\n\n const idxInList = findAndUpsertCookware(\n this.cookware,\n { name, quantity, optional, hidden },\n reference,\n );\n items.push({ type: \"cookware\", value: idxInList });\n } else if (groups.timerQuantity !== undefined) {\n const durationStr = groups.timerQuantity.trim();\n const unit = (groups.timerUnits || \"\").trim();\n if (!unit) {\n throw new Error(\"Timer missing units\");\n }\n const name = groups.timerName || undefined;\n const duration = parseQuantityInput(durationStr);\n const timerObj: Timer = {\n name,\n duration,\n unit,\n };\n const idxInList = findOrPush(\n this.timers,\n (t) =>\n t.name === timerObj.name &&\n t.duration === timerObj.duration &&\n t.unit === timerObj.unit,\n () => timerObj,\n );\n items.push({ type: \"timer\", value: idxInList });\n }\n\n cursor = idx + match[0].length;\n }\n\n if (cursor < line.length) {\n items.push({ type: \"text\", value: line.slice(cursor) });\n }\n\n blankLineBefore = false;\n }\n\n // End of content reached: pushing all temporarily saved elements\n flushPendingItems(section, items);\n note = flushPendingNote(section, note);\n if (!section.isBlank()) {\n this.sections.push(section);\n }\n }\n\n /**\n * Scales the recipe to a new number of servings.\n * @param newServings - The new number of servings.\n * @returns A new Recipe instance with the scaled ingredients.\n */\n scaleTo(newServings: number): Recipe {\n const originalServings = this.getServings();\n\n if (originalServings === undefined || originalServings === 0) {\n throw new Error(\"Error scaling recipe: no initial servings value set\");\n }\n\n const factor = newServings / originalServings;\n return this.scaleBy(factor);\n }\n\n /**\n * Scales the recipe by a factor.\n * @param factor - The factor to scale the recipe by.\n * @returns A new Recipe instance with the scaled ingredients.\n */\n scaleBy(factor: number): Recipe {\n const newRecipe = this.clone();\n\n const originalServings = newRecipe.getServings();\n\n if (originalServings === undefined || originalServings === 0) {\n throw new Error(\"Error scaling recipe: no initial servings value set\");\n }\n\n newRecipe.ingredients = newRecipe.ingredients\n .map((ingredient) => {\n if (\n ingredient.quantity &&\n !(\n ingredient.quantity.type === \"fixed\" &&\n ingredient.quantity.value.type === \"text\"\n )\n ) {\n ingredient.quantity = multiplyQuantityValue(\n ingredient.quantity,\n factor,\n );\n }\n return ingredient;\n })\n .filter((ingredient) => ingredient.quantity !== null);\n\n newRecipe.servings = originalServings * factor;\n\n if (newRecipe.metadata.servings && this.metadata.servings) {\n const servingsValue = parseFloat(this.metadata.servings);\n if (!isNaN(servingsValue)) {\n newRecipe.metadata.servings = String(servingsValue * factor);\n }\n }\n\n if (newRecipe.metadata.yield && this.metadata.yield) {\n const yieldValue = parseFloat(this.metadata.yield);\n if (!isNaN(yieldValue)) {\n newRecipe.metadata.yield = String(yieldValue * factor);\n }\n }\n\n if (newRecipe.metadata.serves && this.metadata.serves) {\n const servesValue = parseFloat(this.metadata.serves);\n if (!isNaN(servesValue)) {\n newRecipe.metadata.serves = String(servesValue * factor);\n }\n }\n\n return newRecipe;\n }\n\n /**\n * Gets the number of servings for the recipe.\n * @private\n * @returns The number of servings, or undefined if not set.\n */\n private getServings(): number | undefined {\n if (this.servings) {\n return this.servings;\n }\n return undefined;\n }\n\n /**\n * Clones the recipe.\n * @returns A new Recipe instance with the same properties.\n */\n clone(): Recipe {\n const newRecipe = new Recipe();\n // deep copy\n newRecipe.metadata = JSON.parse(JSON.stringify(this.metadata));\n newRecipe.ingredients = JSON.parse(JSON.stringify(this.ingredients));\n newRecipe.sections = JSON.parse(JSON.stringify(this.sections));\n newRecipe.cookware = JSON.parse(JSON.stringify(this.cookware));\n newRecipe.timers = JSON.parse(JSON.stringify(this.timers));\n newRecipe.servings = this.servings;\n return newRecipe;\n }\n}\n","import { AisleConfig } from \"./aisle_config\";\nimport { Recipe } from \"./recipe\";\nimport type {\n Ingredient,\n CategorizedIngredients,\n AddedRecipe,\n AddedIngredient,\n} from \"../types\";\nimport { addQuantities, type Quantity } from \"../units\";\n\n/**\n * Represents a shopping list.\n * @category Classes\n */\nexport class ShoppingList {\n /**\n * The ingredients in the shopping list.\n * @see {@link Ingredient}\n */\n ingredients: Ingredient[] = [];\n /**\n * The recipes in the shopping list.\n * @see {@link AddedRecipe}\n */\n recipes: AddedRecipe[] = [];\n /**\n * The aisle configuration for the shopping list.\n * @see {@link AisleConfig}\n */\n aisle_config?: AisleConfig;\n /**\n * The categorized ingredients in the shopping list.\n * @see {@link CategorizedIngredients}\n */\n categories?: CategorizedIngredients;\n\n /**\n * Creates a new ShoppingList instance.\n * @param aisle_config_str - The aisle configuration to parse.\n */\n constructor(aisle_config_str?: string) {\n if (aisle_config_str) {\n this.set_aisle_config(aisle_config_str);\n }\n }\n\n private calculate_ingredients() {\n this.ingredients = [];\n for (const { recipe, factor } of this.recipes) {\n const scaledRecipe = factor === 1 ? recipe : recipe.scaleBy(factor);\n for (const ingredient of scaledRecipe.ingredients) {\n if (ingredient.hidden) {\n continue;\n }\n\n const existingIngredient = this.ingredients.find(\n (i) => i.name === ingredient.name,\n );\n\n let addSeparate = false;\n try {\n if (existingIngredient) {\n if (existingIngredient.quantity && ingredient.quantity) {\n const newQuantity: Quantity = addQuantities(\n {\n value: existingIngredient.quantity,\n unit: existingIngredient.unit ?? \"\",\n },\n {\n value: ingredient.quantity,\n unit: ingredient.unit ?? \"\",\n },\n );\n existingIngredient.quantity = newQuantity.value;\n if (newQuantity.unit) {\n existingIngredient.unit = newQuantity.unit;\n }\n } else if (ingredient.quantity) {\n existingIngredient.quantity = ingredient.quantity;\n if (ingredient.unit) {\n existingIngredient.unit = ingredient.unit;\n }\n }\n }\n } catch {\n // Cannot add quantities, adding as separate ingredients\n addSeparate = true;\n }\n\n if (!existingIngredient || addSeparate) {\n const newIngredient: AddedIngredient = { name: ingredient.name };\n if (ingredient.quantity) {\n newIngredient.quantity = ingredient.quantity;\n }\n if (ingredient.unit) {\n newIngredient.unit = ingredient.unit;\n }\n this.ingredients.push(newIngredient);\n }\n }\n }\n }\n\n /**\n * Adds a recipe to the shopping list.\n * @param recipe - The recipe to add.\n * @param factor - The factor to scale the recipe by.\n */\n add_recipe(recipe: Recipe, factor: number = 1) {\n this.recipes.push({ recipe, factor });\n this.calculate_ingredients();\n this.categorize();\n }\n\n /**\n * Removes a recipe from the shopping list.\n * @param index - The index of the recipe to remove.\n */\n remove_recipe(index: number) {\n if (index < 0 || index >= this.recipes.length) {\n throw new Error(\"Index out of bounds\");\n }\n this.recipes.splice(index, 1);\n this.calculate_ingredients();\n this.categorize();\n }\n\n /**\n * Sets the aisle configuration for the shopping list.\n * @param config - The aisle configuration to parse.\n */\n set_aisle_config(config: string) {\n this.aisle_config = new AisleConfig(config);\n this.categorize();\n }\n\n /**\n * Categorizes the ingredients in the shopping list\n * Will use the aisle config if any, otherwise all ingredients will be placed in the \"other\" category\n */\n categorize() {\n if (!this.aisle_config) {\n this.categories = { other: this.ingredients };\n return;\n }\n\n const categories: CategorizedIngredients = { other: [] };\n for (const category of this.aisle_config.categories) {\n categories[category.name] = [];\n }\n\n for (const ingredient of this.ingredients) {\n let found = false;\n for (const category of this.aisle_config.categories) {\n for (const aisleIngredient of category.ingredients) {\n if (aisleIngredient.aliases.includes(ingredient.name)) {\n categories[category.name]!.push(ingredient);\n found = true;\n break;\n }\n }\n if (found) {\n break;\n }\n }\n if (!found) {\n categories.other!.push(ingredient);\n }\n }\n\n this.categories = categories;\n }\n}\n"],"mappings":"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","names":["escapeCache","Map","Flags","GLOBAL","NON_SENSITIVE","MULTILINE","DOT_ALL","UNICODE","STICKY","Ranges","Object","freeze","digit","lowercaseLetter","uppercaseLetter","letter","alphanumeric","anyCharacter","Quantifiers","zeroOrMore","oneOrMore","optional","HumanRegex","constructor","this","parts","flags","Set","add","special","word","whitespace","nonWhitespace","text","has","set","replace","get","or","name","range","Error","chars","lazy","lastPart","pop","newline","pattern","hasSpecialCharacter","hasDigit","hasLetter","n","min","max","startGroup","startCaptureGroup","wordBoundary","nonWordBoundary","endGroup","startAnchor","endAnchor","global","nonSensitive","multiline","dotAll","sticky","variant","validVariants","unicodeDigit","unicodePunctuation","unicodeSymbol","count","length","push","ipv4Octet","protocol","www","tld","path","part","toString","join","toRegExp","RegExp","createRegex","Patterns","createCachedPattern","builder","regex","source","email","literal","atLeast","url","phoneInternational","between","r","a","i","e","units","t","i"]}
package/dist/index.d.cts CHANGED
@@ -1,7 +1,24 @@
1
+ /**
2
+ * Represents a recipe section
3
+ * Wrapped as a Class and not as a simple type to expose some useful helper
4
+ * classes (e.g. `isBlank()`)
5
+ * @category Types
6
+ */
1
7
  declare class Section {
8
+ /** The name of the section. Can be an empty string for the default (first) section. */
2
9
  name: string;
10
+ /** An array of steps and notes that make up the content of the section. */
3
11
  content: (Step | Note)[];
12
+ /**
13
+ * Creates an instance of Section.
14
+ * @param name - The name of the section. Defaults to an empty string.
15
+ */
4
16
  constructor(name?: string);
17
+ /**
18
+ * Checks if the section is blank (has no name and no content).
19
+ * Used during recipe parsing
20
+ * @returns `true` if the section is blank, otherwise `false`.
21
+ */
5
22
  isBlank(): boolean;
6
23
  }
7
24
 
@@ -61,6 +78,11 @@ declare class Recipe {
61
78
  * @returns A new Recipe instance with the scaled ingredients.
62
79
  */
63
80
  scaleBy(factor: number): Recipe;
81
+ /**
82
+ * Gets the number of servings for the recipe.
83
+ * @private
84
+ * @returns The number of servings, or undefined if not set.
85
+ */
64
86
  private getServings;
65
87
  /**
66
88
  * Clones the recipe.
@@ -146,6 +168,51 @@ interface MetadataExtract {
146
168
  /** The number of servings the recipe makes. Used for scaling */
147
169
  servings?: number;
148
170
  }
171
+ /**
172
+ * Represents a quantity described by text, e.g. "a pinch"
173
+ * @category Types
174
+ */
175
+ interface TextValue {
176
+ type: "text";
177
+ value: string;
178
+ }
179
+ /**
180
+ * Represents a quantity described by a decimal number, e.g. "1.5"
181
+ * @category Types
182
+ */
183
+ interface DecimalValue {
184
+ type: "decimal";
185
+ value: number;
186
+ }
187
+ /**
188
+ * Represents a quantity described by a fraction, e.g. "1/2"
189
+ * @category Types
190
+ */
191
+ interface FractionValue {
192
+ type: "fraction";
193
+ /** The numerator of the fraction */
194
+ num: number;
195
+ /** The denominator of the fraction */
196
+ den: number;
197
+ }
198
+ /**
199
+ * Represents a single, fixed quantity.
200
+ * This can be a text, decimal, or fraction.
201
+ * @category Types
202
+ */
203
+ interface FixedValue {
204
+ type: "fixed";
205
+ value: TextValue | DecimalValue | FractionValue;
206
+ }
207
+ /**
208
+ * Represents a range of quantities, e.g. "1-2"
209
+ * @category Types
210
+ */
211
+ interface Range {
212
+ type: "range";
213
+ min: DecimalValue | FractionValue;
214
+ max: DecimalValue | FractionValue;
215
+ }
149
216
  /**
150
217
  * Represents an ingredient in a recipe.
151
218
  * @category Types
@@ -154,7 +221,7 @@ interface Ingredient {
154
221
  /** The name of the ingredient. */
155
222
  name: string;
156
223
  /** The quantity of the ingredient. */
157
- quantity?: number | string;
224
+ quantity?: FixedValue | Range;
158
225
  /** The unit of the ingredient. */
159
226
  unit?: string;
160
227
  /** The preparation of the ingredient. */
@@ -174,7 +241,7 @@ interface Timer {
174
241
  /** The name of the timer. */
175
242
  name?: string;
176
243
  /** The duration of the timer. */
177
- duration: number;
244
+ duration: FixedValue | Range;
178
245
  /** The unit of the timer. */
179
246
  unit: string;
180
247
  }
@@ -197,6 +264,12 @@ interface IngredientItem {
197
264
  type: "ingredient";
198
265
  /** The value of the item. */
199
266
  value: number;
267
+ /** If this is a referenced ingredient, quantity specific to this instance */
268
+ partialQuantity?: FixedValue | Range;
269
+ /** If this is a referenced ingredient, unit specific to this instance */
270
+ partialUnit?: string;
271
+ /** If this is a referenced ingredient, preparation specific to this instance */
272
+ partialPreparation?: string;
200
273
  }
201
274
  /**
202
275
  * Represents a cookware item in a recipe step.
@@ -246,6 +319,8 @@ interface Note {
246
319
  interface Cookware {
247
320
  /** The name of the cookware. */
248
321
  name: string;
322
+ /** The quantity of cookware */
323
+ quantity?: FixedValue | Range;
249
324
  /** Whether the cookware is optional. */
250
325
  optional?: boolean;
251
326
  /** Whether the cookware is hidden. */
@@ -366,4 +441,4 @@ declare class ShoppingList {
366
441
  categorize(): void;
367
442
  }
368
443
 
369
- export { type AddedRecipe, type AisleCategory, AisleConfig, type AisleIngredient, type CategorizedIngredients, type Cookware, type CookwareItem, type Ingredient, type IngredientItem, type Item, type Metadata, type MetadataExtract, type Note, Recipe, ShoppingList, type Step, type TextItem, type Timer, type TimerItem };
444
+ export { type AddedRecipe, type AisleCategory, AisleConfig, type AisleIngredient, type CategorizedIngredients, type Cookware, type CookwareItem, type DecimalValue, type FixedValue, type FractionValue, type Ingredient, type IngredientItem, type Item, type Metadata, type MetadataExtract, type Note, type Range, Recipe, Section, ShoppingList, type Step, type TextItem, type TextValue, type Timer, type TimerItem };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,24 @@
1
+ /**
2
+ * Represents a recipe section
3
+ * Wrapped as a Class and not as a simple type to expose some useful helper
4
+ * classes (e.g. `isBlank()`)
5
+ * @category Types
6
+ */
1
7
  declare class Section {
8
+ /** The name of the section. Can be an empty string for the default (first) section. */
2
9
  name: string;
10
+ /** An array of steps and notes that make up the content of the section. */
3
11
  content: (Step | Note)[];
12
+ /**
13
+ * Creates an instance of Section.
14
+ * @param name - The name of the section. Defaults to an empty string.
15
+ */
4
16
  constructor(name?: string);
17
+ /**
18
+ * Checks if the section is blank (has no name and no content).
19
+ * Used during recipe parsing
20
+ * @returns `true` if the section is blank, otherwise `false`.
21
+ */
5
22
  isBlank(): boolean;
6
23
  }
7
24
 
@@ -61,6 +78,11 @@ declare class Recipe {
61
78
  * @returns A new Recipe instance with the scaled ingredients.
62
79
  */
63
80
  scaleBy(factor: number): Recipe;
81
+ /**
82
+ * Gets the number of servings for the recipe.
83
+ * @private
84
+ * @returns The number of servings, or undefined if not set.
85
+ */
64
86
  private getServings;
65
87
  /**
66
88
  * Clones the recipe.
@@ -146,6 +168,51 @@ interface MetadataExtract {
146
168
  /** The number of servings the recipe makes. Used for scaling */
147
169
  servings?: number;
148
170
  }
171
+ /**
172
+ * Represents a quantity described by text, e.g. "a pinch"
173
+ * @category Types
174
+ */
175
+ interface TextValue {
176
+ type: "text";
177
+ value: string;
178
+ }
179
+ /**
180
+ * Represents a quantity described by a decimal number, e.g. "1.5"
181
+ * @category Types
182
+ */
183
+ interface DecimalValue {
184
+ type: "decimal";
185
+ value: number;
186
+ }
187
+ /**
188
+ * Represents a quantity described by a fraction, e.g. "1/2"
189
+ * @category Types
190
+ */
191
+ interface FractionValue {
192
+ type: "fraction";
193
+ /** The numerator of the fraction */
194
+ num: number;
195
+ /** The denominator of the fraction */
196
+ den: number;
197
+ }
198
+ /**
199
+ * Represents a single, fixed quantity.
200
+ * This can be a text, decimal, or fraction.
201
+ * @category Types
202
+ */
203
+ interface FixedValue {
204
+ type: "fixed";
205
+ value: TextValue | DecimalValue | FractionValue;
206
+ }
207
+ /**
208
+ * Represents a range of quantities, e.g. "1-2"
209
+ * @category Types
210
+ */
211
+ interface Range {
212
+ type: "range";
213
+ min: DecimalValue | FractionValue;
214
+ max: DecimalValue | FractionValue;
215
+ }
149
216
  /**
150
217
  * Represents an ingredient in a recipe.
151
218
  * @category Types
@@ -154,7 +221,7 @@ interface Ingredient {
154
221
  /** The name of the ingredient. */
155
222
  name: string;
156
223
  /** The quantity of the ingredient. */
157
- quantity?: number | string;
224
+ quantity?: FixedValue | Range;
158
225
  /** The unit of the ingredient. */
159
226
  unit?: string;
160
227
  /** The preparation of the ingredient. */
@@ -174,7 +241,7 @@ interface Timer {
174
241
  /** The name of the timer. */
175
242
  name?: string;
176
243
  /** The duration of the timer. */
177
- duration: number;
244
+ duration: FixedValue | Range;
178
245
  /** The unit of the timer. */
179
246
  unit: string;
180
247
  }
@@ -197,6 +264,12 @@ interface IngredientItem {
197
264
  type: "ingredient";
198
265
  /** The value of the item. */
199
266
  value: number;
267
+ /** If this is a referenced ingredient, quantity specific to this instance */
268
+ partialQuantity?: FixedValue | Range;
269
+ /** If this is a referenced ingredient, unit specific to this instance */
270
+ partialUnit?: string;
271
+ /** If this is a referenced ingredient, preparation specific to this instance */
272
+ partialPreparation?: string;
200
273
  }
201
274
  /**
202
275
  * Represents a cookware item in a recipe step.
@@ -246,6 +319,8 @@ interface Note {
246
319
  interface Cookware {
247
320
  /** The name of the cookware. */
248
321
  name: string;
322
+ /** The quantity of cookware */
323
+ quantity?: FixedValue | Range;
249
324
  /** Whether the cookware is optional. */
250
325
  optional?: boolean;
251
326
  /** Whether the cookware is hidden. */
@@ -366,4 +441,4 @@ declare class ShoppingList {
366
441
  categorize(): void;
367
442
  }
368
443
 
369
- export { type AddedRecipe, type AisleCategory, AisleConfig, type AisleIngredient, type CategorizedIngredients, type Cookware, type CookwareItem, type Ingredient, type IngredientItem, type Item, type Metadata, type MetadataExtract, type Note, Recipe, ShoppingList, type Step, type TextItem, type Timer, type TimerItem };
444
+ export { type AddedRecipe, type AisleCategory, AisleConfig, type AisleIngredient, type CategorizedIngredients, type Cookware, type CookwareItem, type DecimalValue, type FixedValue, type FractionValue, type Ingredient, type IngredientItem, type Item, type Metadata, type MetadataExtract, type Note, type Range, Recipe, Section, ShoppingList, type Step, type TextItem, type TextValue, type Timer, type TimerItem };