UNPKG

parse-ingredient

Version:

Recipe ingredient parser with support for mixed numbers and vulgar fractions

800 lines (799 loc) 24.1 kB
import { numericQuantity, numericRegex } from "numeric-quantity"; //#region src/constants.ts /** * Escapes special regex characters in a string. */ const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); /** * Builds a regex that matches any of the given patterns at the start of a string, * followed by whitespace. Strings are escaped and treated as literal prefixes. * RegExp patterns have their source extracted and combined. */ const buildPrefixPatternRegex = (patterns) => { if (patterns.length === 0) return null; const parts = patterns.map((p) => p instanceof RegExp ? `(?:${p.source})` : `(?:${escapeRegex(p)})\\s`); return new RegExp(`^(?:${parts.join("|")})`, "iu"); }; /** * Builds a regex source string for range separators (dashes and word separators). * Always includes dash characters (-, –, —), plus any custom word separators. */ const buildRangeSeparatorSource = (words) => { return `(-|–|—|(?:${words.map((w) => w instanceof RegExp ? `(?:${w.source})` : `(?:${escapeRegex(w)})`).join("|")})\\s)`; }; /** * Builds a regex that matches range separators at the start of a string. */ const buildRangeSeparatorRegex = (words) => new RegExp(`^${buildRangeSeparatorSource(words)}`, "iu"); /** * Builds a regex that matches any of the given words or patterns at the start of a string. * Strings are matched as whole words followed by whitespace. * RegExp patterns are used as-is for more complex matching (e.g., French elisions). */ const buildStripPrefixRegex = (patterns) => { if (patterns.length === 0) return null; const parts = patterns.map((p) => p instanceof RegExp ? `(?:${p.source})` : `(?:${escapeRegex(p)})\\s+`); return new RegExp(`^(?:${parts.join("|")})`, "iu"); }; /** * Builds a regex that matches approximation/modifier prefixes at the start of * quantity expressions (e.g., "about", "ca.", "bis zu"), followed by optional whitespace. */ const buildLeadingQuantityPrefixRegex = (patterns) => { if (patterns.length === 0) return null; const parts = patterns.map((p) => p instanceof RegExp ? `(?:${p.source})\\s*` : `(?:${escapeRegex(p)})\\s*`); return new RegExp(`^(?:${parts.join("|")})`, "iu"); }; /** * Builds a regex that matches any of the given words at the end of a string, * preceded by whitespace. Used for trailing quantity context like "from" or "of". */ const buildTrailingContextRegex = (words) => new RegExp(`\\s+(?:${words.map(escapeRegex).join("|")})$`, "iu"); /** * Default group header prefixes (e.g., "For the icing:"). */ const defaultGroupHeaderPatterns = ["For"]; /** * Default range separator words (e.g., "1 to 2", "1 or 2"). */ const defaultRangeSeparators = ["or", "to"]; /** * Default words to strip from the beginning of descriptions. */ const defaultDescriptionStripPrefixes = ["of"]; /** * Default words that indicate trailing quantity context. */ const defaultTrailingQuantityContext = ["from", "of"]; /** * Default words/patterns that are stripped from the beginning of quantity expressions. */ const defaultLeadingQuantityPrefixes = []; /** * Default options for {@link parseIngredient}. */ const defaultOptions = { additionalUOMs: {}, allowLeadingOf: false, normalizeUOM: false, ignoreUOMs: [], decimalSeparator: ".", groupHeaderPatterns: defaultGroupHeaderPatterns, rangeSeparators: defaultRangeSeparators, descriptionStripPrefixes: defaultDescriptionStripPrefixes, trailingQuantityContext: defaultTrailingQuantityContext, leadingQuantityPrefixes: defaultLeadingQuantityPrefixes, includeMeta: false, partialUnitMatching: false }; /** * List of "for" equivalents. * @deprecated Use `defaultGroupHeaderPatterns` instead. */ const fors = defaultGroupHeaderPatterns; /** * Regex to capture "for" equivalents. * @deprecated Build dynamically using `buildPrefixPatternRegex(options.groupHeaderPatterns)`. */ const forsRegEx = buildPrefixPatternRegex(defaultGroupHeaderPatterns); /** * List of range separators. * @deprecated Use `defaultRangeSeparators` instead. */ const rangeSeparatorWords = defaultRangeSeparators; /** * Regex to capture range separators. * @deprecated Build dynamically using `buildRangeSeparatorRegex(options.rangeSeparators)`. */ const rangeSeparatorRegEx = buildRangeSeparatorRegex(defaultRangeSeparators); /** * Regex to capture the first word of a description to check if it's a unit of measure. */ const firstWordRegEx = /^(fl(?:uid)?(?:\s+|-)(?:oz|ounces?)|[\p{L}\p{N}_]+(?:[./-][\p{L}\p{N}_]+|\([\p{L}\p{N}_]+\))*[-.]?)(.+)?/iu; const numericRegexAnywhere = numericRegex.source.replace("^", "").replace(/\$$/, ""); /** * Builds a regex to capture trailing quantity and unit of measure, * using the provided range separator words. */ const buildTrailingQuantityRegex = (rangeSeparators) => { const rangeSeparatorSource = buildRangeSeparatorSource(rangeSeparators); return new RegExp(`(,|:|-|–|—|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}_]+\\))*[-.]?)?$`, "iu"); }; /** * Regex to capture trailing quantity and unit of measure. * @deprecated Build dynamically using `buildTrailingQuantityRegex(options.rangeSeparators)`. */ const trailingQuantityRegEx = buildTrailingQuantityRegex(defaultRangeSeparators); /** * List of "of" equivalents. * @deprecated Use `defaultDescriptionStripPrefixes` instead. */ const ofs = defaultDescriptionStripPrefixes; /** * Regex to capture "of" equivalents at the beginning of a string. * @deprecated Build dynamically using `buildStripPrefixRegex(options.descriptionStripPrefixes)`. */ const ofRegEx = buildStripPrefixRegex(defaultDescriptionStripPrefixes); /** * List of "from" equivalents. * @deprecated Use `defaultTrailingQuantityContext` instead. */ const froms = defaultTrailingQuantityContext; /** * Regex to capture "from" equivalents at the end of a string. * @deprecated Build dynamically using `buildTrailingContextRegex(options.trailingQuantityContext)`. */ const fromRegEx = buildTrailingContextRegex(defaultTrailingQuantityContext); /** * Default unit of measure specifications. */ const unitsOfMeasure = { bag: { short: "bag", plural: "bags", alternates: [], type: "count" }, box: { short: "box", plural: "boxes", alternates: [], type: "count" }, bunch: { short: "bunch", plural: "bunches", alternates: [], type: "count" }, can: { short: "can", plural: "cans", alternates: [], type: "count" }, carton: { short: "carton", plural: "cartons", alternates: [], type: "count" }, clove: { short: "clove", plural: "cloves", alternates: [], type: "count" }, container: { short: "container", plural: "containers", alternates: [], type: "count" }, dozen: { short: "dz", plural: "dozen", alternates: ["dz."], type: "count" }, ear: { short: "ear", plural: "ears", alternates: [], type: "count" }, head: { short: "head", plural: "heads", alternates: [], type: "count" }, pack: { short: "pack", plural: "packs", alternates: [], type: "count" }, package: { short: "pkg", plural: "packages", alternates: [ "pkg.", "pkgs", "pkgs." ], type: "count" }, piece: { short: "piece", plural: "pieces", alternates: [ "pc", "pc.", "pcs", "pcs." ], type: "count" }, sprig: { short: "sprig", plural: "sprigs", alternates: [], type: "count" }, stick: { short: "stick", plural: "sticks", alternates: [], type: "count" }, large: { short: "lg", plural: "large", alternates: ["lg", "lg."], type: "other" }, medium: { short: "md", plural: "medium", alternates: [ "med", "med.", "md." ], type: "other" }, small: { short: "sm", plural: "small", alternates: ["sm."], type: "other" }, centimeter: { short: "cm", plural: "centimeters", alternates: ["cm."], type: "length", conversionFactor: 10 }, foot: { short: "ft", plural: "feet", alternates: ["ft."], type: "length", conversionFactor: 304.8 }, inch: { short: "in", plural: "inches", alternates: ["in."], type: "length", conversionFactor: 25.4 }, meter: { short: "m", plural: "meters", alternates: ["m."], type: "length", conversionFactor: 1e3 }, millimeter: { short: "mm", plural: "millimeters", alternates: ["mm."], type: "length", conversionFactor: 1 }, yard: { short: "yd", plural: "yards", alternates: ["yd.", "yds."], type: "length", conversionFactor: 914.4 }, gram: { short: "g", plural: "grams", alternates: ["g."], type: "mass", conversionFactor: 1 }, kilogram: { short: "kg", plural: "kilograms", alternates: ["kg."], type: "mass", conversionFactor: 1e3 }, milligram: { short: "mg", plural: "milligrams", alternates: ["mg."], type: "mass", conversionFactor: .001 }, ounce: { short: "oz", plural: "ounces", alternates: ["oz."], type: "mass", conversionFactor: 28.349523 }, pound: { short: "lb", plural: "pounds", alternates: [ "lb.", "lbs", "lbs." ], type: "mass", conversionFactor: 453.59237 }, cup: { short: "c", plural: "cups", alternates: ["c.", "C"], type: "volume", conversionFactor: { us: 236.58824, imperial: 284.13063, metric: 250 } }, deciliter: { short: "dl", plural: "deciliters", alternates: ["dl."], type: "volume", conversionFactor: 100 }, "fluid ounce": { short: "fl oz", plural: "fluid ounces", alternates: [ "fluidounce", "floz", "fl-oz", "fluid-ounce", "fluid-ounces", "fluidounces", "fl ounce", "fl ounces", "fl-ounce", "fl-ounces", "fluid oz", "fluid-oz" ], type: "volume", conversionFactor: { us: 29.57353, imperial: 28.413063 } }, gallon: { short: "gal", plural: "gallons", alternates: ["gal."], type: "volume", conversionFactor: { us: 3785.4118, imperial: 4546.09 } }, liter: { short: "l", plural: "liters", alternates: ["l."], type: "volume", conversionFactor: 1e3 }, milliliter: { short: "ml", plural: "milliliters", alternates: [ "mL", "ml.", "mL." ], type: "volume", conversionFactor: 1 }, pint: { short: "pt", plural: "pints", alternates: ["pt."], type: "volume", conversionFactor: { us: 473.17647, imperial: 568.26125 } }, quart: { short: "qt", plural: "quarts", alternates: [ "qt.", "qts", "qts." ], type: "volume", conversionFactor: { us: 946.35295, imperial: 1136.5225 } }, tablespoon: { short: "tbsp", plural: "tablespoons", alternates: [ "tbsp.", "T", "Tbsp.", "Tbsp", "tablespoonful" ], type: "volume", conversionFactor: { us: 14.786765, imperial: 15, metric: 15 } }, teaspoon: { short: "tsp", plural: "teaspoons", alternates: [ "tsp.", "t", "teaspoonful" ], type: "volume", conversionFactor: { us: 4.9289216, imperial: 5, metric: 5 } }, dash: { short: "dash", plural: "dashes", alternates: [], type: "volume" }, drop: { short: "drop", plural: "drops", alternates: [], type: "volume" }, pinch: { short: "pinch", plural: "pinches", alternates: [], type: "volume" } }; //#endregion //#region src/unitLookup.ts /** * Builds Maps for unit lookup. Returns both case-sensitive and case-insensitive maps. * The case-sensitive map should be checked first to handle cases like 'T' (tablespoon) * vs 't' (teaspoon). */ const buildUnitLookupMaps = (additionalUOMs = {}) => { const caseSensitive = /* @__PURE__ */ new Map(); const caseInsensitive = /* @__PURE__ */ new Map(); const addToMaps = (id, def) => { const versions = [ id, def.short, def.plural, ...def.alternates ]; for (const version of versions) { caseSensitive.set(version, id); caseInsensitive.set(version.toLowerCase(), id); } }; for (const [id, def] of Object.entries(unitsOfMeasure)) addToMaps(id, def); for (const [id, def] of Object.entries(additionalUOMs)) addToMaps(id, def); return { caseSensitive, caseInsensitive }; }; /** * Looks up a unit ID from the maps, trying case-sensitive first. */ const lookupUnit = (unit, maps) => { var _ref, _maps$caseSensitive$g; return (_ref = (_maps$caseSensitive$g = maps.caseSensitive.get(unit)) !== null && _maps$caseSensitive$g !== void 0 ? _maps$caseSensitive$g : maps.caseInsensitive.get(unit.toLowerCase())) !== null && _ref !== void 0 ? _ref : null; }; /** * Cached lookup maps for the default unitsOfMeasure (no additionalUOMs). * Lazily initialized on first use. */ let defaultLookupMaps = null; /** * Gets the default lookup maps, creating them if needed. */ const getDefaultUnitLookupMaps = () => { var _defaultLookupMaps; return (_defaultLookupMaps = defaultLookupMaps) !== null && _defaultLookupMaps !== void 0 ? _defaultLookupMaps : defaultLookupMaps = buildUnitLookupMaps(); }; /** * Collects all known UOM strings from the lookup maps, sorted longest-first. */ const collectUOMStrings = (maps) => { const keys = [...maps.caseSensitive.keys()]; keys.sort((a, b) => b.length - a.length); return keys; }; //#endregion //#region src/convertUnit.ts /** * Identifies a unit of measure from a string, returning the canonical unit ID. * Matches against the unit ID, short form, plural form, and all alternates. * Case-sensitive matches are tried first (e.g., 'T' = tablespoon, 't' = teaspoon), * then falls back to case-insensitive matching. * * @returns The canonical unit ID (e.g., 'cup'), or `null` if the unit is not recognized * or is in the `ignoreUOMs` list. * * @example * ```ts * identifyUnit('cups') // 'cup' * identifyUnit('c') // 'cup' * identifyUnit('T') // 'tablespoon' * identifyUnit('t') // 'teaspoon' * identifyUnit('tbsp') // 'tablespoon' * identifyUnit('unknown') // null * identifyUnit('large', { ignoreUOMs: ['large'] }) // null * ``` */ const identifyUnit = (unit, options = {}) => { const { additionalUOMs = {}, ignoreUOMs = [] } = options; if (ignoreUOMs.length > 0) { const unitLC = unit.toLowerCase(); if (ignoreUOMs.some((ignored) => ignored.toLowerCase() === unitLC)) return null; } return lookupUnit(unit, Object.keys(additionalUOMs).length > 0 ? buildUnitLookupMaps(additionalUOMs) : getDefaultUnitLookupMaps()); }; /** * Gets the conversion factor for a unit, handling both single and multi-system factors. */ const getConversionFactor = (factor, system) => { var _factor$system; return factor === void 0 ? null : typeof factor === "number" ? factor : (_factor$system = factor[system]) !== null && _factor$system !== void 0 ? _factor$system : null; }; /** * Converts a value from one unit to another. * * @returns The converted value, or `null` if conversion is not possible * (incompatible types, missing conversion factors, or unknown units). * * @example * ```ts * convertUnit(1, 'cup', 'milliliter') // ~236.588 (US) * convertUnit(1, 'cup', 'milliliter', { fromSystem: 'imperial' }) // ~284.131 * convertUnit(1, 'pound', 'gram') // ~453.592 * convertUnit(1, 'cup', 'gram') // null (incompatible types) * ``` */ const convertUnit = (value, fromUnit, toUnit, options = {}) => { const { fromSystem = "us", toSystem = "us", additionalUOMs = {} } = options; const normalizedFromSystem = fromSystem.toLowerCase(); const normalizedToSystem = toSystem.toLowerCase(); const mergedUOMs = { ...unitsOfMeasure, ...additionalUOMs }; const fromUnitID = identifyUnit(fromUnit, { additionalUOMs }); const toUnitID = identifyUnit(toUnit, { additionalUOMs }); if (!fromUnitID || !toUnitID) return null; if (fromUnitID === toUnitID && normalizedFromSystem === normalizedToSystem) return value; const fromDef = mergedUOMs[fromUnitID]; const toDef = mergedUOMs[toUnitID]; if (!fromDef.type || !toDef.type || fromDef.type !== toDef.type) return null; const fromFactor = getConversionFactor(fromDef.conversionFactor, normalizedFromSystem); const toFactor = getConversionFactor(toDef.conversionFactor, normalizedToSystem); if (fromFactor === null || toFactor === null) return null; const result = value * fromFactor / toFactor; return Math.round(result * 1e6) / 1e6; }; //#endregion //#region src/parseIngredient.ts const newLineRegExp = /\r?\n/; const nextWordRegExp = /^([\p{L}\p{N}_]+(?:[.-]?[\p{L}\p{N}_]+)*[-.]?)(?:\s+|$)/iu; /** * Repeatedly strips configured quantity prefixes from the start of a string. */ const stripLeadingQuantityPrefixes = (text, prefixRegex) => { if (!text || !prefixRegex) return text; let out = text.trimStart(); while (out) { const match = prefixRegex.exec(out); if (!match || !match[0]) break; out = out.slice(match[0].length).trimStart(); } return out; }; /** * Parses a string or array of strings into an array of recipe ingredient objects */ const parseIngredient = (ingredientText, options = defaultOptions) => { const opts = { ...defaultOptions, ...options }; const nqOpts = opts.decimalSeparator === "," ? { decimalSeparator: "," } : void 0; const ignoredUOMsLC = opts.ignoreUOMs.map((u) => u.toLowerCase()); const groupHeaderRegex = buildPrefixPatternRegex(opts.groupHeaderPatterns); const rangeSeparatorRegex = buildRangeSeparatorRegex(opts.rangeSeparators); const stripPrefixRegex = buildStripPrefixRegex(opts.descriptionStripPrefixes); const trailingContextRegex = buildTrailingContextRegex(opts.trailingQuantityContext); const trailingQuantityRegex = buildTrailingQuantityRegex(opts.rangeSeparators); const leadingQuantityPrefixRegex = buildLeadingQuantityPrefixRegex(opts.leadingQuantityPrefixes); const uomStrings = opts.partialUnitMatching ? collectUOMStrings(Object.keys(opts.additionalUOMs).length > 0 ? buildUnitLookupMaps(opts.additionalUOMs) : getDefaultUnitLookupMaps()) : []; return (Array.isArray(ingredientText) ? ingredientText : ingredientText.split(newLineRegExp)).map((line, index) => ({ line: line.trim(), sourceIndex: index })).filter(({ line }) => Boolean(line)).map(({ line, sourceIndex }) => { const lineToParse = stripLeadingQuantityPrefixes(line, leadingQuantityPrefixRegex); const oIng = { quantity: null, quantity2: null, unitOfMeasureID: null, unitOfMeasure: null, description: "", isGroupHeader: false }; if (opts.includeMeta) oIng.meta = { sourceText: line, sourceIndex }; if (lineToParse && (!isNaN(numericQuantity(lineToParse[0], nqOpts)) || lineToParse[0] === opts.decimalSeparator && !isNaN(numericQuantity(lineToParse.slice(0, 2), nqOpts)))) { let lenNum = 6; let nqResult = NaN; while (lenNum > 0 && isNaN(nqResult)) { nqResult = numericQuantity(lineToParse.substring(0, lenNum).trim(), nqOpts); if (nqResult > -1) { oIng.quantity = nqResult; oIng.description = lineToParse.substring(lenNum).trim(); } lenNum--; } } else { var _trailingQtyResult$at; const trailingQtyResult = trailingQuantityRegex.exec(lineToParse); const trailingQtyMaybeUom = trailingQtyResult === null || trailingQtyResult === void 0 || (_trailingQtyResult$at = trailingQtyResult.at(-1)) === null || _trailingQtyResult$at === void 0 ? void 0 : _trailingQtyResult$at.toLowerCase(); if (trailingQtyMaybeUom && ignoredUOMsLC.includes(trailingQtyMaybeUom)) oIng.description = lineToParse; else if (trailingQtyResult) { oIng.description = lineToParse.replace(trailingQuantityRegex, "").trim(); const firstQty = trailingQtyResult[3]; const secondQty = trailingQtyResult[12]; if (!firstQty) oIng.quantity = numericQuantity(secondQty, nqOpts); else { oIng.quantity = numericQuantity(firstQty, nqOpts); oIng.quantity2 = numericQuantity(secondQty, nqOpts); } const uomRaw = trailingQtyResult.at(-1); if (uomRaw) { let uomID = null; let finalUomRaw = uomRaw; if (oIng.description) { const descWords = oIng.description.trim().split(/\s+/); if (descWords.length >= 1) { const twoWordUnit = descWords[descWords.length - 1] + " " + uomRaw; const twoWordID = identifyUnit(twoWordUnit, options); if (twoWordID) { uomID = twoWordID; finalUomRaw = twoWordUnit; oIng.description = descWords.slice(0, -1).join(" "); } } } if (!uomID) { uomID = identifyUnit(uomRaw, options); finalUomRaw = uomRaw; } if (uomID) { oIng.unitOfMeasureID = uomID; oIng.unitOfMeasure = opts.normalizeUOM ? uomID : finalUomRaw; } else if (oIng.description.match(trailingContextRegex)) oIng.description += ` ${uomRaw}`; } } else { oIng.description = lineToParse; if (oIng.description.endsWith(":") || (groupHeaderRegex === null || groupHeaderRegex === void 0 ? void 0 : groupHeaderRegex.test(oIng.description))) oIng.isGroupHeader = true; } } const q2reMatch = rangeSeparatorRegex.exec(oIng.description); if (q2reMatch) { const q2reMatchLen = q2reMatch[1].length; const q2Portion = stripLeadingQuantityPrefixes(oIng.description.substring(q2reMatchLen).trim(), leadingQuantityPrefixRegex); if (q2Portion) { const nqResultFirstChar = numericQuantity(q2Portion[0], nqOpts); if (!isNaN(nqResultFirstChar)) { let lenNum = 7; let nqResult = NaN; while (--lenNum > 0 && isNaN(nqResult)) { nqResult = numericQuantity(q2Portion.substring(0, lenNum), nqOpts); if (!isNaN(nqResult)) { oIng.quantity2 = nqResult; oIng.description = q2Portion.substring(lenNum).trim(); } } } } } const firstWordREMatches = firstWordRegEx.exec(oIng.description); if (firstWordREMatches) { var _firstWordREMatches$; const firstWord = firstWordREMatches[1].replace(/\s+/g, " "); const remainingDesc = ((_firstWordREMatches$ = firstWordREMatches[2]) !== null && _firstWordREMatches$ !== void 0 ? _firstWordREMatches$ : "").trim(); if (remainingDesc) { let uomID = identifyUnit(firstWord, options); let matchedUnit = firstWord; let finalDesc = remainingDesc; const nextWords = remainingDesc.match(nextWordRegExp); if (nextWords) { const twoWordCombo = firstWord + " " + nextWords[1]; const twoWordID = identifyUnit(twoWordCombo, options); const multiWordFinalDesc = remainingDesc.substring(nextWords[0].length).trim(); if (twoWordID && (oIng.quantity !== null || multiWordFinalDesc)) { uomID = twoWordID; matchedUnit = twoWordCombo; finalDesc = multiWordFinalDesc; } } if (uomID) { oIng.unitOfMeasureID = uomID; oIng.unitOfMeasure = opts.normalizeUOM ? uomID : matchedUnit; oIng.description = finalDesc; } } } if (!oIng.unitOfMeasureID && opts.partialUnitMatching && oIng.description) { const descLower = oIng.description.toLowerCase(); for (const uomStr of uomStrings) { const idx = descLower.indexOf(uomStr.toLowerCase()); if (idx === -1) continue; const matchedText = oIng.description.substring(idx, idx + uomStr.length); const uomID = identifyUnit(matchedText, options); if (!uomID) continue; const newDesc = [oIng.description.substring(0, idx).trim(), oIng.description.substring(idx + uomStr.length).trim()].filter(Boolean).join(" "); if (!newDesc) continue; oIng.unitOfMeasureID = uomID; oIng.unitOfMeasure = opts.normalizeUOM ? uomID : matchedText; oIng.description = newDesc; break; } } if (!opts.allowLeadingOf && stripPrefixRegex && oIng.description.match(stripPrefixRegex)) oIng.description = oIng.description.replace(stripPrefixRegex, ""); return oIng; }); }; //#endregion export { buildLeadingQuantityPrefixRegex, buildPrefixPatternRegex, buildRangeSeparatorRegex, buildRangeSeparatorSource, buildStripPrefixRegex, buildTrailingContextRegex, buildTrailingQuantityRegex, convertUnit, defaultDescriptionStripPrefixes, defaultGroupHeaderPatterns, defaultLeadingQuantityPrefixes, defaultOptions, defaultRangeSeparators, defaultTrailingQuantityContext, escapeRegex, firstWordRegEx, fors, forsRegEx, fromRegEx, froms, identifyUnit, ofRegEx, ofs, parseIngredient, rangeSeparatorRegEx, rangeSeparatorWords, trailingQuantityRegEx, unitsOfMeasure }; //# sourceMappingURL=parse-ingredient.mjs.map