@tmlmt/cooklang-parser 1.0.3
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.
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/index.d.ts +369 -0
- package/dist/index.js +822 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../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 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>[^#~[]+?)\\{(?<mCookwareQuantity>.*?)\\}/;\nconst singleWordCookware =\n /#(?<sCookwareModifier>[\\-&+*!?])?(?<sCookwareName>[^\\s\\t\\s\\p{P}]+)/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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tmlmt/cooklang-parser",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Cooklang parsers and utilities",
|
|
5
|
+
"author": "Thomas Lamant <tom@tmlmt.com>",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.cjs",
|
|
8
|
+
"module": "dist/index.mjs",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@iconify-json/material-symbols": "1.2.32",
|
|
23
|
+
"@types/node": "24.2.1",
|
|
24
|
+
"@typescript-eslint/eslint-plugin": "8.39.0",
|
|
25
|
+
"@typescript-eslint/parser": "8.39.0",
|
|
26
|
+
"@vitest/coverage-v8": "3.2.4",
|
|
27
|
+
"@vitest/ui": "3.2.4",
|
|
28
|
+
"eslint": "9.33.0",
|
|
29
|
+
"eslint-config-prettier": "10.1.8",
|
|
30
|
+
"prettier": "3.6.2",
|
|
31
|
+
"rimraf": "6.0.1",
|
|
32
|
+
"tsup": "8.5.0",
|
|
33
|
+
"typedoc": "0.28.10",
|
|
34
|
+
"typedoc-plugin-markdown": "4.8.1",
|
|
35
|
+
"typedoc-vitepress-theme": "1.1.2",
|
|
36
|
+
"typescript": "5.9.2",
|
|
37
|
+
"vitepress": "2.0.0-alpha.11",
|
|
38
|
+
"vitest": "3.2.4"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"scripts": {
|
|
42
|
+
"clean": "rimraf dist",
|
|
43
|
+
"build": "pnpm run clean && tsup",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:watch": "vitest watch",
|
|
46
|
+
"test:ui": "vitest --ui",
|
|
47
|
+
"test:coverage": "vitest run --coverage",
|
|
48
|
+
"lint": "eslint 'src/**/*.{ts,tsx}' 'test/**/*.{ts,tsx}' --max-warnings=0",
|
|
49
|
+
"format": "prettier --write .",
|
|
50
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
51
|
+
"predocs:dev": "typedoc",
|
|
52
|
+
"docs:dev": "vitepress dev docs",
|
|
53
|
+
"predocs:build": "typedoc",
|
|
54
|
+
"docs:build": "vitepress build docs",
|
|
55
|
+
"predocs:preview": "typedoc",
|
|
56
|
+
"docs:preview": "vitepress preview docs"
|
|
57
|
+
}
|
|
58
|
+
}
|