parse-ingredient
Version:
Recipe ingredient parser with support for mixed numbers and vulgar fractions
1 lines • 44.3 kB
Source Map (JSON)
{"version":3,"file":"parse-ingredient.production.mjs","names":[],"sources":["../src/constants.ts","../src/unitLookup.ts","../src/convertUnit.ts","../src/parseIngredient.ts"],"sourcesContent":["import { numericRegex } from 'numeric-quantity';\nimport { ParseIngredientOptions, UnitOfMeasureDefinitions } from './types';\n\n// --- i18n Utilities ---\n\n/**\n * Escapes special regex characters in a string.\n */\nexport const escapeRegex = (str: string): string => str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\n/**\n * Builds a regex that matches any of the given patterns at the start of a string,\n * followed by whitespace. Strings are escaped and treated as literal prefixes.\n * RegExp patterns have their source extracted and combined.\n */\nexport const buildPrefixPatternRegex = (patterns: (string | RegExp)[]): RegExp | null => {\n if (patterns.length === 0) return null;\n const parts = patterns.map(p =>\n p instanceof RegExp ? `(?:${p.source})` : `(?:${escapeRegex(p)})\\\\s`\n );\n return new RegExp(`^(?:${parts.join('|')})`, 'iu');\n};\n\n/**\n * Builds a regex source string for range separators (dashes and word separators).\n * Always includes dash characters (-, –, —), plus any custom word separators.\n */\nexport const buildRangeSeparatorSource = (words: (string | RegExp)[]): string => {\n const wordParts = words.map(w =>\n w instanceof RegExp ? `(?:${w.source})` : `(?:${escapeRegex(w)})`\n );\n // Always include dashes, then word separators followed by whitespace\n return `(-|–|—|(?:${wordParts.join('|')})\\\\s)`;\n};\n\n/**\n * Builds a regex that matches range separators at the start of a string.\n */\nexport const buildRangeSeparatorRegex = (words: (string | RegExp)[]): RegExp =>\n new RegExp(`^${buildRangeSeparatorSource(words)}`, 'iu');\n\n/**\n * Builds a regex that matches any of the given words or patterns at the start of a string.\n * Strings are matched as whole words followed by whitespace.\n * RegExp patterns are used as-is for more complex matching (e.g., French elisions).\n */\nexport const buildStripPrefixRegex = (patterns: (string | RegExp)[]): RegExp | null => {\n if (patterns.length === 0) return null;\n const parts = patterns.map(p =>\n p instanceof RegExp ? `(?:${p.source})` : `(?:${escapeRegex(p)})\\\\s+`\n );\n return new RegExp(`^(?:${parts.join('|')})`, 'iu');\n};\n\n/**\n * Builds a regex that matches approximation/modifier prefixes at the start of\n * quantity expressions (e.g., \"about\", \"ca.\", \"bis zu\"), followed by optional whitespace.\n */\nexport const buildLeadingQuantityPrefixRegex = (patterns: (string | RegExp)[]): RegExp | null => {\n if (patterns.length === 0) return null;\n // Uses `\\s*` (optional whitespace) instead of `\\s+` (required whitespace, as in\n // buildStripPrefixRegex) because quantity prefixes like \"ca.\" may appear directly\n // adjacent to the number (e.g., \"ca.200g\").\n const parts = patterns.map(p =>\n p instanceof RegExp ? `(?:${p.source})\\\\s*` : `(?:${escapeRegex(p)})\\\\s*`\n );\n return new RegExp(`^(?:${parts.join('|')})`, 'iu');\n};\n\n/**\n * Builds a regex that matches any of the given words at the end of a string,\n * preceded by whitespace. Used for trailing quantity context like \"from\" or \"of\".\n */\nexport const buildTrailingContextRegex = (words: string[]): RegExp =>\n new RegExp(`\\\\s+(?:${words.map(escapeRegex).join('|')})$`, 'iu');\n\n// --- Default i18n Values ---\n\n/**\n * Default group header prefixes (e.g., \"For the icing:\").\n */\nexport const defaultGroupHeaderPatterns = ['For'] as const;\n\n/**\n * Default range separator words (e.g., \"1 to 2\", \"1 or 2\").\n */\nexport const defaultRangeSeparators = ['or', 'to'] as const;\n\n/**\n * Default words to strip from the beginning of descriptions.\n */\nexport const defaultDescriptionStripPrefixes = ['of'] as const;\n\n/**\n * Default words that indicate trailing quantity context.\n */\nexport const defaultTrailingQuantityContext = ['from', 'of'] as const;\n/**\n * Default words/patterns that are stripped from the beginning of quantity expressions.\n */\nexport const defaultLeadingQuantityPrefixes = [] as const;\n\n/**\n * Default options for {@link parseIngredient}.\n */\nexport const defaultOptions: Required<ParseIngredientOptions> = {\n additionalUOMs: {},\n allowLeadingOf: false,\n normalizeUOM: false,\n ignoreUOMs: [],\n decimalSeparator: '.',\n groupHeaderPatterns: defaultGroupHeaderPatterns as unknown as string[],\n rangeSeparators: defaultRangeSeparators as unknown as string[],\n descriptionStripPrefixes: defaultDescriptionStripPrefixes as unknown as (string | RegExp)[],\n trailingQuantityContext: defaultTrailingQuantityContext as unknown as string[],\n leadingQuantityPrefixes: defaultLeadingQuantityPrefixes as unknown as (string | RegExp)[],\n includeMeta: false,\n partialUnitMatching: false,\n} as const;\n\n// --- Legacy Exports (for backward compatibility) ---\n\n/**\n * List of \"for\" equivalents.\n * @deprecated Use `defaultGroupHeaderPatterns` instead.\n */\nexport const fors: typeof defaultGroupHeaderPatterns = defaultGroupHeaderPatterns;\n\n/**\n * Regex to capture \"for\" equivalents.\n * @deprecated Build dynamically using `buildPrefixPatternRegex(options.groupHeaderPatterns)`.\n */\nexport const forsRegEx: RegExp = buildPrefixPatternRegex(\n defaultGroupHeaderPatterns as unknown as string[]\n)!;\n\n/**\n * List of range separators.\n * @deprecated Use `defaultRangeSeparators` instead.\n */\nexport const rangeSeparatorWords: typeof defaultRangeSeparators = defaultRangeSeparators;\n\n/**\n * Regex to capture range separators.\n * @deprecated Build dynamically using `buildRangeSeparatorRegex(options.rangeSeparators)`.\n */\nexport const rangeSeparatorRegEx: RegExp = buildRangeSeparatorRegex(\n defaultRangeSeparators as unknown as string[]\n);\n\n/**\n * Regex to capture the first word of a description to check if it's a unit of measure.\n */\nexport const firstWordRegEx: RegExp =\n /^(fl(?:uid)?(?:\\s+|-)(?:oz|ounces?)|[\\p{L}\\p{N}_]+(?:[./-][\\p{L}\\p{N}_]+|\\([\\p{L}\\p{N}_]+\\))*[-.]?)(.+)?/iu;\n\nconst numericRegexAnywhere = numericRegex.source.replace('^', '').replace(/\\$$/, '');\n\n/**\n * Builds a regex to capture trailing quantity and unit of measure,\n * using the provided range separator words.\n */\nexport const buildTrailingQuantityRegex = (rangeSeparators: (string | RegExp)[]): RegExp => {\n const rangeSeparatorSource = buildRangeSeparatorSource(rangeSeparators);\n return new RegExp(\n `(,|:|-|–|—|x|⨯)?\\\\s*((${numericRegexAnywhere})\\\\s*(${rangeSeparatorSource}))?\\\\s*(${numericRegexAnywhere})\\\\s*(fl(?:uid)?(?:\\\\s+|-)(?:oz|ounces?)|[\\\\p{L}\\\\p{N}_]+(?:[./-][\\\\p{L}\\\\p{N}_]+|\\\\([\\\\p{L}\\\\p{N}_]+\\\\))*[-.]?)?$`,\n 'iu'\n );\n};\n\n/**\n * Regex to capture trailing quantity and unit of measure.\n * @deprecated Build dynamically using `buildTrailingQuantityRegex(options.rangeSeparators)`.\n */\nexport const trailingQuantityRegEx: RegExp = buildTrailingQuantityRegex(\n defaultRangeSeparators as unknown as string[]\n);\n\n/**\n * List of \"of\" equivalents.\n * @deprecated Use `defaultDescriptionStripPrefixes` instead.\n */\nexport const ofs: typeof defaultDescriptionStripPrefixes = defaultDescriptionStripPrefixes;\n\n/**\n * Regex to capture \"of\" equivalents at the beginning of a string.\n * @deprecated Build dynamically using `buildStripPrefixRegex(options.descriptionStripPrefixes)`.\n */\nexport const ofRegEx: RegExp = buildStripPrefixRegex(\n defaultDescriptionStripPrefixes as unknown as string[]\n)!;\n\n/**\n * List of \"from\" equivalents.\n * @deprecated Use `defaultTrailingQuantityContext` instead.\n */\nexport const froms: typeof defaultTrailingQuantityContext = defaultTrailingQuantityContext;\n\n/**\n * Regex to capture \"from\" equivalents at the end of a string.\n * @deprecated Build dynamically using `buildTrailingContextRegex(options.trailingQuantityContext)`.\n */\nexport const fromRegEx: RegExp = buildTrailingContextRegex(\n defaultTrailingQuantityContext as unknown as string[]\n);\n\n/**\n * Default unit of measure specifications.\n */\nexport const unitsOfMeasure: UnitOfMeasureDefinitions = {\n // Count units (no conversion factor)\n bag: {\n short: 'bag',\n plural: 'bags',\n alternates: [] satisfies string[],\n type: 'count',\n },\n box: {\n short: 'box',\n plural: 'boxes',\n alternates: [] satisfies string[],\n type: 'count',\n },\n bunch: {\n short: 'bunch',\n plural: 'bunches',\n alternates: [] satisfies string[],\n type: 'count',\n },\n can: {\n short: 'can',\n plural: 'cans',\n alternates: [] satisfies string[],\n type: 'count',\n },\n carton: {\n short: 'carton',\n plural: 'cartons',\n alternates: [] satisfies string[],\n type: 'count',\n },\n clove: {\n short: 'clove',\n plural: 'cloves',\n alternates: [] satisfies string[],\n type: 'count',\n },\n container: {\n short: 'container',\n plural: 'containers',\n alternates: [] satisfies string[],\n type: 'count',\n },\n dozen: {\n short: 'dz',\n plural: 'dozen',\n alternates: ['dz.'] satisfies string[],\n type: 'count',\n },\n ear: {\n short: 'ear',\n plural: 'ears',\n alternates: [] satisfies string[],\n type: 'count',\n },\n head: {\n short: 'head',\n plural: 'heads',\n alternates: [] satisfies string[],\n type: 'count',\n },\n pack: {\n short: 'pack',\n plural: 'packs',\n alternates: [] satisfies string[],\n type: 'count',\n },\n package: {\n short: 'pkg',\n plural: 'packages',\n alternates: ['pkg.', 'pkgs', 'pkgs.'] satisfies string[],\n type: 'count',\n },\n piece: {\n short: 'piece',\n plural: 'pieces',\n alternates: ['pc', 'pc.', 'pcs', 'pcs.'] satisfies string[],\n type: 'count',\n },\n sprig: {\n short: 'sprig',\n plural: 'sprigs',\n alternates: [] satisfies string[],\n type: 'count',\n },\n stick: {\n short: 'stick',\n plural: 'sticks',\n alternates: [] satisfies string[],\n type: 'count',\n },\n\n // Other units (no conversion factor)\n large: {\n short: 'lg',\n plural: 'large',\n alternates: ['lg', 'lg.'] satisfies string[],\n type: 'other',\n },\n medium: {\n short: 'md',\n plural: 'medium',\n alternates: ['med', 'med.', 'md.'] satisfies string[],\n type: 'other',\n },\n small: {\n short: 'sm',\n plural: 'small',\n alternates: ['sm.'] satisfies string[],\n type: 'other',\n },\n\n // Length units (conversion factor in mm)\n centimeter: {\n short: 'cm',\n plural: 'centimeters',\n alternates: ['cm.'] satisfies string[],\n type: 'length',\n conversionFactor: 10,\n },\n foot: {\n short: 'ft',\n plural: 'feet',\n alternates: ['ft.'] satisfies string[],\n type: 'length',\n conversionFactor: 304.8,\n },\n inch: {\n short: 'in',\n plural: 'inches',\n alternates: ['in.'] satisfies string[],\n type: 'length',\n conversionFactor: 25.4,\n },\n meter: {\n short: 'm',\n plural: 'meters',\n alternates: ['m.'] satisfies string[],\n type: 'length',\n conversionFactor: 1000,\n },\n millimeter: {\n short: 'mm',\n plural: 'millimeters',\n alternates: ['mm.'] satisfies string[],\n type: 'length',\n conversionFactor: 1,\n },\n yard: {\n short: 'yd',\n plural: 'yards',\n alternates: ['yd.', 'yds.'] satisfies string[],\n type: 'length',\n conversionFactor: 914.4,\n },\n\n // Mass units (conversion factor in g)\n gram: {\n short: 'g',\n plural: 'grams',\n alternates: ['g.'] satisfies string[],\n type: 'mass',\n conversionFactor: 1,\n },\n kilogram: {\n short: 'kg',\n plural: 'kilograms',\n alternates: ['kg.'] satisfies string[],\n type: 'mass',\n conversionFactor: 1000,\n },\n milligram: {\n short: 'mg',\n plural: 'milligrams',\n alternates: ['mg.'] satisfies string[],\n type: 'mass',\n conversionFactor: 0.001,\n },\n ounce: {\n short: 'oz',\n plural: 'ounces',\n alternates: ['oz.'] satisfies string[],\n type: 'mass',\n conversionFactor: 28.349523,\n },\n pound: {\n short: 'lb',\n plural: 'pounds',\n alternates: ['lb.', 'lbs', 'lbs.'] satisfies string[],\n type: 'mass',\n conversionFactor: 453.59237,\n },\n\n // Volume units (conversion factor in ml)\n cup: {\n short: 'c',\n plural: 'cups',\n alternates: ['c.', 'C'] satisfies string[],\n type: 'volume',\n conversionFactor: { us: 236.58824, imperial: 284.13063, metric: 250 },\n },\n deciliter: {\n short: 'dl',\n plural: 'deciliters',\n alternates: ['dl.'] satisfies string[],\n type: 'volume',\n conversionFactor: 100,\n },\n 'fluid ounce': {\n short: 'fl oz',\n plural: 'fluid ounces',\n alternates: [\n 'fluidounce',\n 'floz',\n 'fl-oz',\n 'fluid-ounce',\n 'fluid-ounces',\n 'fluidounces',\n 'fl ounce',\n 'fl ounces',\n 'fl-ounce',\n 'fl-ounces',\n 'fluid oz',\n 'fluid-oz',\n ] satisfies string[],\n type: 'volume',\n conversionFactor: { us: 29.57353, imperial: 28.413063 },\n },\n gallon: {\n short: 'gal',\n plural: 'gallons',\n alternates: ['gal.'] satisfies string[],\n type: 'volume',\n conversionFactor: { us: 3785.4118, imperial: 4546.09 },\n },\n liter: {\n short: 'l',\n plural: 'liters',\n alternates: ['l.'] satisfies string[],\n type: 'volume',\n conversionFactor: 1000,\n },\n milliliter: {\n short: 'ml',\n plural: 'milliliters',\n alternates: ['mL', 'ml.', 'mL.'] satisfies string[],\n type: 'volume',\n conversionFactor: 1,\n },\n pint: {\n short: 'pt',\n plural: 'pints',\n alternates: ['pt.'] satisfies string[],\n type: 'volume',\n conversionFactor: { us: 473.17647, imperial: 568.26125 },\n },\n quart: {\n short: 'qt',\n plural: 'quarts',\n alternates: ['qt.', 'qts', 'qts.'] satisfies string[],\n type: 'volume',\n conversionFactor: { us: 946.35295, imperial: 1136.5225 },\n },\n tablespoon: {\n short: 'tbsp',\n plural: 'tablespoons',\n alternates: ['tbsp.', 'T', 'Tbsp.', 'Tbsp', 'tablespoonful'] satisfies string[],\n type: 'volume',\n conversionFactor: { us: 14.786765, imperial: 15, metric: 15 },\n },\n teaspoon: {\n short: 'tsp',\n plural: 'teaspoons',\n alternates: ['tsp.', 't', 'teaspoonful'] satisfies string[],\n type: 'volume',\n conversionFactor: { us: 4.9289216, imperial: 5, metric: 5 },\n },\n\n // Volume units without conversion factor (imprecise)\n dash: {\n short: 'dash',\n plural: 'dashes',\n alternates: [] satisfies string[],\n type: 'volume',\n },\n drop: {\n short: 'drop',\n plural: 'drops',\n alternates: [] satisfies string[],\n type: 'volume',\n },\n pinch: {\n short: 'pinch',\n plural: 'pinches',\n alternates: [] satisfies string[],\n type: 'volume',\n },\n} as const;\n","import { unitsOfMeasure } from './constants';\nimport { UnitOfMeasureDefinitions } from './types';\n\n/**\n * Result of building unit lookup maps.\n */\nexport interface UnitLookupMaps {\n /** Case-sensitive map (exact matches only) */\n caseSensitive: Map<string, string>;\n /** Case-insensitive map (lowercase keys) */\n caseInsensitive: Map<string, string>;\n}\n\n/**\n * Builds Maps for unit lookup. Returns both case-sensitive and case-insensitive maps.\n * The case-sensitive map should be checked first to handle cases like 'T' (tablespoon)\n * vs 't' (teaspoon).\n */\nexport const buildUnitLookupMaps = (\n additionalUOMs: UnitOfMeasureDefinitions = {}\n): UnitLookupMaps => {\n const caseSensitive = new Map<string, string>();\n const caseInsensitive = new Map<string, string>();\n\n // Helper to add versions to maps (first one wins for case-insensitive)\n const addToMaps = (id: string, def: UnitOfMeasureDefinitions[string]) => {\n const versions = [id, def.short, def.plural, ...def.alternates];\n for (const version of versions) {\n // For case-sensitive, later entries override (so additionalUOMs wins)\n caseSensitive.set(version, id);\n // For case-insensitive, later entries also override\n caseInsensitive.set(version.toLowerCase(), id);\n }\n };\n\n // Process default UOMs first\n for (const [id, def] of Object.entries(unitsOfMeasure)) {\n addToMaps(id, def);\n }\n\n // Process additionalUOMs second so they override defaults\n for (const [id, def] of Object.entries(additionalUOMs)) {\n addToMaps(id, def);\n }\n\n return { caseSensitive, caseInsensitive };\n};\n\n/**\n * Looks up a unit ID from the maps, trying case-sensitive first.\n */\nexport const lookupUnit = (unit: string, maps: UnitLookupMaps): string | null =>\n maps.caseSensitive.get(unit) ?? maps.caseInsensitive.get(unit.toLowerCase()) ?? null;\n\n/**\n * Cached lookup maps for the default unitsOfMeasure (no additionalUOMs).\n * Lazily initialized on first use.\n */\nlet defaultLookupMaps: UnitLookupMaps | null = null;\n\n/**\n * Gets the default lookup maps, creating them if needed.\n */\nexport const getDefaultUnitLookupMaps = (): UnitLookupMaps =>\n defaultLookupMaps ?? (defaultLookupMaps = buildUnitLookupMaps());\n\n/**\n * Collects all known UOM strings from the lookup maps, sorted longest-first.\n */\nexport const collectUOMStrings = (maps: UnitLookupMaps): string[] => {\n const keys = [...maps.caseSensitive.keys()];\n keys.sort((a, b) => b.length - a.length);\n return keys;\n};\n","import { unitsOfMeasure } from './constants';\nimport {\n MultiSystemConversionFactor,\n ParseIngredientOptions,\n UnitOfMeasureDefinitions,\n UnitSystem,\n} from './types';\nimport { buildUnitLookupMaps, getDefaultUnitLookupMaps, lookupUnit } from './unitLookup';\n\nexport type IdentifyUnitOptions = Pick<ParseIngredientOptions, 'additionalUOMs' | 'ignoreUOMs'>;\n\n/**\n * Identifies a unit of measure from a string, returning the canonical unit ID.\n * Matches against the unit ID, short form, plural form, and all alternates.\n * Case-sensitive matches are tried first (e.g., 'T' = tablespoon, 't' = teaspoon),\n * then falls back to case-insensitive matching.\n *\n * @returns The canonical unit ID (e.g., 'cup'), or `null` if the unit is not recognized\n * or is in the `ignoreUOMs` list.\n *\n * @example\n * ```ts\n * identifyUnit('cups') // 'cup'\n * identifyUnit('c') // 'cup'\n * identifyUnit('T') // 'tablespoon'\n * identifyUnit('t') // 'teaspoon'\n * identifyUnit('tbsp') // 'tablespoon'\n * identifyUnit('unknown') // null\n * identifyUnit('large', { ignoreUOMs: ['large'] }) // null\n * ```\n */\nexport const identifyUnit = (\n /** The unit string to identify (e.g., 'cups', 'c', 'C', 'cup'). */\n unit: string,\n /** Options for unit identification. */\n options: IdentifyUnitOptions = {}\n): string | null => {\n const { additionalUOMs = {}, ignoreUOMs = [] } = options;\n\n // Check if the unit should be ignored (case-insensitive)\n if (ignoreUOMs.length > 0) {\n const unitLC = unit.toLowerCase();\n if (ignoreUOMs.some(ignored => ignored.toLowerCase() === unitLC)) {\n return null;\n }\n }\n\n const hasAdditionalUOMs = Object.keys(additionalUOMs).length > 0;\n const maps = hasAdditionalUOMs ? buildUnitLookupMaps(additionalUOMs) : getDefaultUnitLookupMaps();\n\n return lookupUnit(unit, maps);\n};\n\n/**\n * Options for {@link convertUnit}.\n */\nexport interface ConvertUnitOptions {\n /**\n * The measurement system to use when units have different US/Imperial conversion factors.\n *\n * @default 'us'\n */\n fromSystem?: UnitSystem;\n /**\n * The measurement system to use when units have different US/Imperial conversion factors.\n *\n * @default 'us'\n */\n toSystem?: UnitSystem;\n /**\n * Additional unit definitions to use for conversion.\n * These are merged with the default {@link unitsOfMeasure}.\n */\n additionalUOMs?: UnitOfMeasureDefinitions;\n}\n\n/**\n * Gets the conversion factor for a unit, handling both single and multi-system factors.\n */\nconst getConversionFactor = (\n factor: number | MultiSystemConversionFactor | undefined,\n system: UnitSystem\n): number | null =>\n factor === undefined ? null : typeof factor === 'number' ? factor : (factor[system] ?? null);\n\n/**\n * Converts a value from one unit to another.\n *\n * @returns The converted value, or `null` if conversion is not possible\n * (incompatible types, missing conversion factors, or unknown units).\n *\n * @example\n * ```ts\n * convertUnit(1, 'cup', 'milliliter') // ~236.588 (US)\n * convertUnit(1, 'cup', 'milliliter', { fromSystem: 'imperial' }) // ~284.131\n * convertUnit(1, 'pound', 'gram') // ~453.592\n * convertUnit(1, 'cup', 'gram') // null (incompatible types)\n * ```\n */\nexport const convertUnit = (\n /** The numeric value to convert. */\n value: number,\n /** The unit to convert from (unit ID, short, plural, or alternate spelling). */\n fromUnit: string,\n /** The unit to convert to (unit ID, short, plural, or alternate spelling). */\n toUnit: string,\n /** Conversion options. */\n options: ConvertUnitOptions = {}\n): number | null => {\n const { fromSystem = 'us', toSystem = 'us', additionalUOMs = {} } = options;\n // Normalize system names to lowercase for case-insensitive matching\n const normalizedFromSystem = fromSystem.toLowerCase() as UnitSystem;\n const normalizedToSystem = toSystem.toLowerCase() as UnitSystem;\n const mergedUOMs = { ...unitsOfMeasure, ...additionalUOMs };\n\n const fromUnitID = identifyUnit(fromUnit, { additionalUOMs });\n const toUnitID = identifyUnit(toUnit, { additionalUOMs });\n\n // Unknown units\n if (!fromUnitID || !toUnitID) {\n return null;\n }\n\n // Same unit with same system - return value as-is\n if (fromUnitID === toUnitID && normalizedFromSystem === normalizedToSystem) {\n return value;\n }\n\n const fromDef = mergedUOMs[fromUnitID];\n const toDef = mergedUOMs[toUnitID];\n\n // Incompatible or missing types\n if (!fromDef.type || !toDef.type || fromDef.type !== toDef.type) {\n return null;\n }\n\n const fromFactor = getConversionFactor(fromDef.conversionFactor, normalizedFromSystem);\n const toFactor = getConversionFactor(toDef.conversionFactor, normalizedToSystem);\n\n // Missing conversion factors\n if (fromFactor === null || toFactor === null) {\n return null;\n }\n\n // Convert via base unit: value * fromFactor / toFactor\n const result = (value * fromFactor) / toFactor;\n return Math.round(result * 1e6) / 1e6;\n};\n","import { numericQuantity, NumericQuantityOptions } from 'numeric-quantity';\nimport {\n buildLeadingQuantityPrefixRegex,\n buildPrefixPatternRegex,\n buildRangeSeparatorRegex,\n buildStripPrefixRegex,\n buildTrailingContextRegex,\n buildTrailingQuantityRegex,\n defaultOptions,\n firstWordRegEx,\n} from './constants';\nimport { identifyUnit } from './convertUnit';\nimport type { Ingredient, ParseIngredientOptions } from './types';\nimport { buildUnitLookupMaps, collectUOMStrings, getDefaultUnitLookupMaps } from './unitLookup';\n\nconst newLineRegExp = /\\r?\\n/;\nconst nextWordRegExp = /^([\\p{L}\\p{N}_]+(?:[.-]?[\\p{L}\\p{N}_]+)*[-.]?)(?:\\s+|$)/iu;\n\n/**\n * Repeatedly strips configured quantity prefixes from the start of a string.\n */\nconst stripLeadingQuantityPrefixes = (text: string, prefixRegex: RegExp | null): string => {\n if (!text || !prefixRegex) return text;\n let out = text.trimStart();\n while (out) {\n const match = prefixRegex.exec(out);\n // Break if no match or if the match is zero-length to prevent infinite loops\n // (empty string is falsy, so `!match[0]` catches both null/undefined and \"\")\n if (!match || !match[0]) break;\n out = out.slice(match[0].length).trimStart();\n }\n return out;\n};\n\n/**\n * Parses a string or array of strings into an array of recipe ingredient objects\n */\nexport const parseIngredient = (\n /**\n * The ingredient list, as plain text or an array of strings.\n */\n ingredientText: string | string[],\n /**\n * Configuration options. Defaults to {@link defaultOptions}.\n */\n options: ParseIngredientOptions = defaultOptions\n): Ingredient[] => {\n const opts = { ...defaultOptions, ...options };\n const nqOpts: NumericQuantityOptions | undefined =\n opts.decimalSeparator === ',' ? { decimalSeparator: ',' } : undefined;\n\n // Pre-compute lowercase ignored UOMs for the trailing quantity bail-out check\n const ignoredUOMsLC = opts.ignoreUOMs.map(u => u.toLowerCase());\n\n // Build dynamic regexes from i18n options\n const groupHeaderRegex = buildPrefixPatternRegex(opts.groupHeaderPatterns);\n const rangeSeparatorRegex = buildRangeSeparatorRegex(opts.rangeSeparators);\n const stripPrefixRegex = buildStripPrefixRegex(opts.descriptionStripPrefixes);\n const trailingContextRegex = buildTrailingContextRegex(opts.trailingQuantityContext);\n const trailingQuantityRegex = buildTrailingQuantityRegex(opts.rangeSeparators);\n const leadingQuantityPrefixRegex = buildLeadingQuantityPrefixRegex(opts.leadingQuantityPrefixes);\n\n const uomStrings = opts.partialUnitMatching\n ? collectUOMStrings(\n Object.keys(opts.additionalUOMs).length > 0\n ? buildUnitLookupMaps(opts.additionalUOMs)\n : getDefaultUnitLookupMaps()\n )\n : [];\n\n const ingredientArray = (\n Array.isArray(ingredientText) ? ingredientText : ingredientText.split(newLineRegExp)\n )\n .map((line, index) => ({ line: line.trim(), sourceIndex: index }))\n .filter(({ line }) => Boolean(line));\n\n return ingredientArray.map(({ line, sourceIndex }) => {\n const lineToParse = stripLeadingQuantityPrefixes(line, leadingQuantityPrefixRegex);\n const oIng: Ingredient = {\n quantity: null,\n quantity2: null,\n unitOfMeasureID: null,\n unitOfMeasure: null,\n description: '',\n isGroupHeader: false,\n };\n\n if (opts.includeMeta) {\n oIng.meta = {\n sourceText: line,\n sourceIndex,\n };\n }\n\n // Check if the line begins with either (1) at least one numeric character, or\n // (2) a decimal separator followed by at least one numeric character.\n if (\n lineToParse &&\n (!isNaN(numericQuantity(lineToParse[0], nqOpts)) ||\n (lineToParse[0] === opts.decimalSeparator &&\n !isNaN(numericQuantity(lineToParse.slice(0, 2), nqOpts))))\n ) {\n // See how many of the first seven characters constitute a single value. This will be `quantity`.\n let lenNum = 6;\n let nqResult = NaN;\n\n while (lenNum > 0 && isNaN(nqResult)) {\n nqResult = numericQuantity(lineToParse.substring(0, lenNum).trim(), nqOpts);\n\n if (nqResult > -1) {\n oIng.quantity = nqResult;\n oIng.description = lineToParse.substring(lenNum).trim();\n }\n\n lenNum--;\n }\n } else {\n // The first character is not numeric. First check for trailing quantity/uom.\n const trailingQtyResult = trailingQuantityRegex.exec(lineToParse);\n const trailingQtyMaybeUom = trailingQtyResult?.at(-1)?.toLowerCase();\n\n if (trailingQtyMaybeUom && ignoredUOMsLC.includes(trailingQtyMaybeUom)) {\n // Trailing quantity detected, but bailing out since the UOM should be ignored.\n oIng.description = lineToParse;\n } else if (trailingQtyResult) {\n // Trailing quantity detected with missing or non-ignored UOM.\n // Remove the quantity and unit of measure from the description.\n oIng.description = lineToParse.replace(trailingQuantityRegex, '').trim();\n\n // Trailing quantity/range.\n const firstQty = trailingQtyResult[3];\n const secondQty = trailingQtyResult[12];\n if (!firstQty) {\n oIng.quantity = numericQuantity(secondQty, nqOpts);\n } else {\n oIng.quantity = numericQuantity(firstQty, nqOpts);\n oIng.quantity2 = numericQuantity(secondQty, nqOpts);\n }\n\n // Trailing unit of measure.\n const uomRaw = trailingQtyResult.at(-1);\n if (uomRaw) {\n let uomID: string | null = null;\n let finalUomRaw = uomRaw;\n\n // Greedy: try multi-word unit first (prefer longer match over shorter one)\n if (oIng.description) {\n const descWords = oIng.description.trim().split(/\\s+/);\n if (descWords.length >= 1) {\n const lastDescWord = descWords[descWords.length - 1];\n const twoWordUnit = lastDescWord + ' ' + uomRaw;\n const twoWordID = identifyUnit(twoWordUnit, options);\n\n if (twoWordID) {\n uomID = twoWordID;\n finalUomRaw = twoWordUnit;\n // Remove the last word from description\n oIng.description = descWords.slice(0, -1).join(' ');\n }\n }\n }\n\n // Fall back to single-word match\n if (!uomID) {\n uomID = identifyUnit(uomRaw, options);\n finalUomRaw = uomRaw;\n }\n\n if (uomID) {\n oIng.unitOfMeasureID = uomID;\n oIng.unitOfMeasure = opts.normalizeUOM ? uomID : finalUomRaw;\n } else if (oIng.description.match(trailingContextRegex)) {\n oIng.description += ` ${uomRaw}`;\n }\n }\n } else {\n // The first character is not numeric, and no trailing quantity was detected,\n // so the entire line is the description.\n oIng.description = lineToParse;\n\n // If the line ends with \":\" or matches a group header pattern, it is assumed to be a group header.\n if (oIng.description.endsWith(':') || groupHeaderRegex?.test(oIng.description)) {\n oIng.isGroupHeader = true;\n }\n }\n }\n\n // Now check the description for a `quantity2` at the beginning.\n // First we look for a dash, emdash, endash, or word separator to\n // indicate a range, then process the next seven characters just\n // like we did for `quantity`.\n const q2reMatch = rangeSeparatorRegex.exec(oIng.description);\n if (q2reMatch) {\n const q2reMatchLen = q2reMatch[1].length;\n const q2Portion = stripLeadingQuantityPrefixes(\n oIng.description.substring(q2reMatchLen).trim(),\n leadingQuantityPrefixRegex\n );\n\n // Guard against empty string after prefix stripping (e.g., aggressive\n // prefixes could strip content down to nothing)\n if (q2Portion) {\n const nqResultFirstChar = numericQuantity(q2Portion[0], nqOpts);\n\n if (!isNaN(nqResultFirstChar)) {\n let lenNum = 7;\n let nqResult = NaN;\n\n while (--lenNum > 0 && isNaN(nqResult)) {\n nqResult = numericQuantity(q2Portion.substring(0, lenNum), nqOpts);\n\n if (!isNaN(nqResult)) {\n oIng.quantity2 = nqResult;\n oIng.description = q2Portion.substring(lenNum).trim();\n }\n }\n }\n }\n }\n\n // Check for a known unit of measure\n const firstWordREMatches = firstWordRegEx.exec(oIng.description);\n\n if (firstWordREMatches) {\n const firstWord = firstWordREMatches[1].replace(/\\s+/g, ' ');\n const remainingDesc = (firstWordREMatches[2] ?? '').trim();\n if (remainingDesc) {\n let uomID = identifyUnit(firstWord, options);\n let matchedUnit = firstWord;\n let finalDesc = remainingDesc;\n\n // Try multi-word unit combinations (greedy matching: prefer longer matches over shorter ones)\n const nextWords = remainingDesc.match(nextWordRegExp);\n if (nextWords) {\n const twoWordCombo = firstWord + ' ' + nextWords[1];\n const twoWordID = identifyUnit(twoWordCombo, options);\n\n // If multi-word unit exists, prefer it over single-word match.\n // When no quantity is present, require remaining description\n // (consistent with single-word behavior: \"1 cup\" → description, not UOM).\n const multiWordFinalDesc = remainingDesc.substring(nextWords[0].length).trim();\n if (twoWordID && (oIng.quantity !== null || multiWordFinalDesc)) {\n uomID = twoWordID;\n matchedUnit = twoWordCombo;\n finalDesc = multiWordFinalDesc;\n }\n }\n\n if (uomID) {\n oIng.unitOfMeasureID = uomID;\n oIng.unitOfMeasure = opts.normalizeUOM ? uomID : matchedUnit;\n oIng.description = finalDesc;\n }\n }\n }\n\n // Fallback: scan description for known UOM substrings (for CJK/spaceless text)\n if (!oIng.unitOfMeasureID && opts.partialUnitMatching && oIng.description) {\n const descLower = oIng.description.toLowerCase();\n for (const uomStr of uomStrings) {\n const idx = descLower.indexOf(uomStr.toLowerCase());\n if (idx === -1) continue;\n\n const matchedText = oIng.description.substring(idx, idx + uomStr.length);\n const uomID = identifyUnit(matchedText, options);\n if (!uomID) continue;\n\n const before = oIng.description.substring(0, idx).trim();\n const after = oIng.description.substring(idx + uomStr.length).trim();\n const newDesc = [before, after].filter(Boolean).join(' ');\n\n // Don't extract UOM if it would leave description empty\n // (consistent with \"2 cup\" keeping \"cup\" as description, not UOM)\n if (!newDesc) continue;\n\n oIng.unitOfMeasureID = uomID;\n oIng.unitOfMeasure = opts.normalizeUOM ? uomID : matchedText;\n oIng.description = newDesc;\n break;\n }\n }\n\n if (!opts.allowLeadingOf && stripPrefixRegex && oIng.description.match(stripPrefixRegex)) {\n oIng.description = oIng.description.replace(stripPrefixRegex, '');\n }\n\n return oIng;\n });\n};\n"],"mappings":"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"}