parse-ingredient
Version:
Recipe ingredient parser with support for mixed numbers and vulgar fractions
406 lines • 14.2 kB
text/typescript
//#region src/types.d.ts
/**
* Metadata about the parsed ingredient line.
*/
interface IngredientMeta {
/**
* The source text of the ingredient line before parsing.
*/
sourceText: string;
/**
* The source index (line number) of the ingredient in the input.
* Zero-based index relative to the order of non-empty lines.
*/
sourceIndex: number;
}
/**
* Ingredient properties.
*/
interface Ingredient {
/**
* The primary quantity (the lower quantity in a range, if applicable)
*/
quantity: number | null;
/**
* The secondary quantity (the upper quantity in a range, or `null` if not applicable)
*/
quantity2: number | null;
/**
* The unit of measure identifier
*/
unitOfMeasureID: string | null;
/**
* The unit of measure
*/
unitOfMeasure: string | null;
/**
* The description
*/
description: string;
/**
* Whether the "ingredient" is actually a group header, e.g. "For icing:"
*/
isGroupHeader: boolean;
/**
* Metadata about the parsed ingredient line.
* Only included when the `includeMeta` option is `true`.
*/
meta?: IngredientMeta;
}
/**
* The type of measurement.
*/
type UnitType = 'volume' | 'mass' | 'length' | 'count' | 'other';
/**
* The measurement system.
*/
type UnitSystem = 'us' | 'imperial' | 'metric';
/**
* Conversion factor that differs between measurement systems.
*/
interface MultiSystemConversionFactor {
us?: number;
imperial?: number;
metric?: number;
}
/**
* Unit of measure properties.
*/
interface UnitOfMeasure {
/**
* Abbreviation or short name for the unit.
*/
short: string;
/**
* Full name of the unit used when quantity is greater than one.
*/
plural: string;
/**
* List of all known alternate spellings, abbreviations, etc.
*/
alternates: string[];
/**
* The type of measurement (volume, mass, length, count, or other).
*/
type?: UnitType;
/**
* Conversion factor to base unit (ml for volume, g for mass, mm for length).
* A number means the same factor applies to all systems.
* An object with `us`, `imperial`, and/or `metric` keys means the factor differs by system.
*/
conversionFactor?: number | MultiSystemConversionFactor;
}
type UnitOfMeasureDefinitions = Record<string, UnitOfMeasure>;
/**
* Options available to {@link parseIngredient}.
*/
interface ParseIngredientOptions {
/**
* Converts the unit of measure (`unitOfMeasure` property) of each
* ingredient to its long, singular form. For example, "ml" becomes
* "milliliter" and "cups" becomes "cup".
*
* @default false
*/
normalizeUOM?: boolean;
/**
* An object that matches the format of {@link unitsOfMeasure}. Keys that
* match any in {@link unitsOfMeasure} will be used instead of the default,
* and any others will be added to the list of known units of measure
* when parsing ingredients.
*
* @default {}
*/
additionalUOMs?: UnitOfMeasureDefinitions;
/**
* An array of strings to ignore as units of measure when parsing ingredients.
*
* @example
*
* ```ts
* parseIngredient('2 small eggs', {
* ignoreUOMs: ['small', 'medium', 'large']
* })
* // [
* // {
* // quantity: 2,
* // quantity2: null,
* // unitOfMeasure: null,
* // unitOfMeasureID: null,
* // description: 'small eggs',
* // isGroupHeader: false,
* // }
* // ]
* ```
*
* @default []
*/
ignoreUOMs?: string[];
/**
* If `true`, ingredient descriptions that start with "of " will not be
* modified. (By default, a leading "of " will be removed from all descriptions.)
*
* @default false
*/
allowLeadingOf?: boolean;
/**
* The character used as a decimal separator in numeric quantities.
* Use `","` for European-style decimal commas (e.g., "1,5" for 1.5).
*
* @default "."
*/
decimalSeparator?: '.' | ',';
/**
* Patterns to identify group headers (e.g., "For the icing:").
* Strings are treated as prefix patterns (matched at the start of the line followed by whitespace).
* RegExp patterns are used as-is for more complex matching.
*
* @default ['For']
* @example ['For', 'Für', /^Pour\s/iu]
*/
groupHeaderPatterns?: (string | RegExp)[];
/**
* Words or patterns to identify ranges between quantities (e.g., "1 to 2", "1 or 2").
* Strings are matched as whole words followed by whitespace.
* RegExp patterns are used as-is for more complex matching.
*
* @default ['to', 'or']
* @example ['to', 'or', 'bis', 'oder', 'à']
*/
rangeSeparators?: (string | RegExp)[];
/**
* Words or patterns to strip from the beginning of ingredient descriptions.
* Commonly used to remove "of" from phrases like "1 cup of sugar".
* Strings are matched as whole words followed by whitespace.
* RegExp patterns are used as-is for more complex matching (e.g., French elisions).
*
* @default ['of']
* @example ['of', 'de', /d[eu]?\s/iu, new RegExp("de\\s+l[ae']?\\s*", "iu")]
*/
descriptionStripPrefixes?: (string | RegExp)[];
/**
* Words that indicate a trailing quantity extraction context.
* Used to identify patterns like "Juice of 3 lemons" or "Peels from 5 oranges".
*
* @default ['from', 'of']
* @example ['from', 'of', 'von', 'de']
*/
trailingQuantityContext?: string[];
/**
* Words or patterns to strip from the beginning of a quantity expression.
* Useful for approximation prefixes like "about 2 cups", "ca. 200 g", or
* range modifiers like "bis zu 3 EL".
*
* List longer/more-specific patterns before shorter ones (e.g., `['ca.', 'ca']`
* instead of `['ca', 'ca.']`) since regex alternation matches left-to-right.
*
* @default []
* @example ['about', 'approx.', 'ca.', 'etwa', 'bis zu']
*/
leadingQuantityPrefixes?: (string | RegExp)[];
/**
* If `true`, include a `meta` property on each ingredient containing
* the original text, original index, and other metadata.
*
* @default false
*/
includeMeta?: boolean;
/**
* When `true`, if normal whitespace-based parsing fails to identify a unit
* of measure, the parser scans the description for known UOM strings
* registered via `additionalUOMs`. Useful for CJK languages where words
* are not separated by spaces.
*
* @default false
*/
partialUnitMatching?: boolean;
}
//#endregion
//#region src/constants.d.ts
/**
* Escapes special regex characters in a string.
*/
declare const escapeRegex: (str: string) => string;
/**
* 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.
*/
declare const buildPrefixPatternRegex: (patterns: (string | RegExp)[]) => RegExp | null;
/**
* Builds a regex source string for range separators (dashes and word separators).
* Always includes dash characters (-, –, —), plus any custom word separators.
*/
declare const buildRangeSeparatorSource: (words: (string | RegExp)[]) => string;
/**
* Builds a regex that matches range separators at the start of a string.
*/
declare const buildRangeSeparatorRegex: (words: (string | RegExp)[]) => RegExp;
/**
* 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).
*/
declare const buildStripPrefixRegex: (patterns: (string | RegExp)[]) => RegExp | null;
/**
* Builds a regex that matches approximation/modifier prefixes at the start of
* quantity expressions (e.g., "about", "ca.", "bis zu"), followed by optional whitespace.
*/
declare const buildLeadingQuantityPrefixRegex: (patterns: (string | RegExp)[]) => RegExp | null;
/**
* 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".
*/
declare const buildTrailingContextRegex: (words: string[]) => RegExp;
/**
* Default group header prefixes (e.g., "For the icing:").
*/
declare const defaultGroupHeaderPatterns: readonly ['For'];
/**
* Default range separator words (e.g., "1 to 2", "1 or 2").
*/
declare const defaultRangeSeparators: readonly ['or', 'to'];
/**
* Default words to strip from the beginning of descriptions.
*/
declare const defaultDescriptionStripPrefixes: readonly ['of'];
/**
* Default words that indicate trailing quantity context.
*/
declare const defaultTrailingQuantityContext: readonly ['from', 'of'];
/**
* Default words/patterns that are stripped from the beginning of quantity expressions.
*/
declare const defaultLeadingQuantityPrefixes: readonly [];
/**
* Default options for {@link parseIngredient}.
*/
declare const defaultOptions: Required<ParseIngredientOptions>;
/**
* List of "for" equivalents.
* @deprecated Use `defaultGroupHeaderPatterns` instead.
*/
declare const fors: typeof defaultGroupHeaderPatterns;
/**
* Regex to capture "for" equivalents.
* @deprecated Build dynamically using `buildPrefixPatternRegex(options.groupHeaderPatterns)`.
*/
declare const forsRegEx: RegExp;
/**
* List of range separators.
* @deprecated Use `defaultRangeSeparators` instead.
*/
declare const rangeSeparatorWords: typeof defaultRangeSeparators;
/**
* Regex to capture range separators.
* @deprecated Build dynamically using `buildRangeSeparatorRegex(options.rangeSeparators)`.
*/
declare const rangeSeparatorRegEx: RegExp;
/**
* Regex to capture the first word of a description to check if it's a unit of measure.
*/
declare const firstWordRegEx: RegExp;
/**
* Builds a regex to capture trailing quantity and unit of measure,
* using the provided range separator words.
*/
declare const buildTrailingQuantityRegex: (rangeSeparators: (string | RegExp)[]) => RegExp;
/**
* Regex to capture trailing quantity and unit of measure.
* @deprecated Build dynamically using `buildTrailingQuantityRegex(options.rangeSeparators)`.
*/
declare const trailingQuantityRegEx: RegExp;
/**
* List of "of" equivalents.
* @deprecated Use `defaultDescriptionStripPrefixes` instead.
*/
declare const ofs: typeof defaultDescriptionStripPrefixes;
/**
* Regex to capture "of" equivalents at the beginning of a string.
* @deprecated Build dynamically using `buildStripPrefixRegex(options.descriptionStripPrefixes)`.
*/
declare const ofRegEx: RegExp;
/**
* List of "from" equivalents.
* @deprecated Use `defaultTrailingQuantityContext` instead.
*/
declare const froms: typeof defaultTrailingQuantityContext;
/**
* Regex to capture "from" equivalents at the end of a string.
* @deprecated Build dynamically using `buildTrailingContextRegex(options.trailingQuantityContext)`.
*/
declare const fromRegEx: RegExp;
/**
* Default unit of measure specifications.
*/
declare const unitsOfMeasure: UnitOfMeasureDefinitions;
//#endregion
//#region src/convertUnit.d.ts
type IdentifyUnitOptions = Pick<ParseIngredientOptions, 'additionalUOMs' | 'ignoreUOMs'>;
/**
* 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
* ```
*/
declare const identifyUnit: (unit: string, options?: IdentifyUnitOptions) => string | null;
/**
* Options for {@link convertUnit}.
*/
interface ConvertUnitOptions {
/**
* The measurement system to use when units have different US/Imperial conversion factors.
*
* @default 'us'
*/
fromSystem?: UnitSystem;
/**
* The measurement system to use when units have different US/Imperial conversion factors.
*
* @default 'us'
*/
toSystem?: UnitSystem;
/**
* Additional unit definitions to use for conversion.
* These are merged with the default {@link unitsOfMeasure}.
*/
additionalUOMs?: UnitOfMeasureDefinitions;
}
/**
* 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)
* ```
*/
declare const convertUnit: (value: number, fromUnit: string, toUnit: string, options?: ConvertUnitOptions) => number | null;
//#endregion
//#region src/parseIngredient.d.ts
/**
* Parses a string or array of strings into an array of recipe ingredient objects
*/
declare const parseIngredient: (ingredientText: string | string[], options?: ParseIngredientOptions) => Ingredient[];
//#endregion
export { ConvertUnitOptions, IdentifyUnitOptions, Ingredient, IngredientMeta, MultiSystemConversionFactor, ParseIngredientOptions, UnitOfMeasure, UnitOfMeasureDefinitions, UnitSystem, UnitType, 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.production.d.mts.map