parse-ingredient
Version:
Recipe ingredient parser with support for mixed numbers and vulgar fractions
1 lines • 45.3 kB
Source Map (JSON)
{"version":3,"file":"parse-ingredient.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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"}