@jclind/ingredient-parser 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,3 @@
1
- export declare const spoonacularHttp: import("axios").AxiosInstance;
2
- export declare const createIngredientServerHttp: (serverUrl?: string) => import("axios").AxiosInstance;
1
+ import _axios from 'axios';
2
+ export declare const spoonacularHttp: _axios.AxiosInstance;
3
+ export declare const createIngredientServerHttp: (serverUrl?: string) => _axios.AxiosInstance;
@@ -4,7 +4,85 @@ exports.parseIngredientString = void 0;
4
4
  const convertFractions_js_1 = require("./convertFractions.js");
5
5
  const parseStringConsecutiveTs_js_1 = require("./parseStringConsecutiveTs.js");
6
6
  const parseIngredientString = (ingrStr) => {
7
- var _a, _b;
7
+ var _a, _b, _c;
8
+ // Input validation
9
+ if (typeof ingrStr !== 'string' || ingrStr === null || ingrStr === undefined) {
10
+ throw new TypeError('parseIngredientString expects a string input');
11
+ }
12
+ // Store original input before any modifications
13
+ const originalInput = ingrStr;
14
+ // Extract range quantities (e.g., "1-2 cups", "2 to 3 cups")
15
+ let minQty = null;
16
+ let maxQty = null;
17
+ const hyphenRangeMatch = ingrStr.match(/(\d+(?:\.\d+)?)\s*[-–]\s*(\d+(?:\.\d+)?)/);
18
+ const toRangeMatch = ingrStr.match(/(\d+(?:\.\d+)?)\s+to\s+(\d+(?:\.\d+)?)/i);
19
+ if (hyphenRangeMatch) {
20
+ minQty = parseFloat(hyphenRangeMatch[1]);
21
+ maxQty = parseFloat(hyphenRangeMatch[2]);
22
+ // Replace the range with minQty for parsing
23
+ ingrStr = ingrStr.replace(hyphenRangeMatch[0], String(minQty));
24
+ }
25
+ else if (toRangeMatch) {
26
+ minQty = parseFloat(toRangeMatch[1]);
27
+ maxQty = parseFloat(toRangeMatch[2]);
28
+ // Replace the range with minQty for parsing
29
+ ingrStr = ingrStr.replace(toRangeMatch[0], String(minQty));
30
+ }
31
+ // Store extracted ranges for later use
32
+ const extractedMinQty = minQty;
33
+ const extractedMaxQty = maxQty;
34
+ // Pre-processing: handle informal quantity patterns ("a pinch of", "handful", "dash")
35
+ // These patterns aren't well-handled by the upstream parser
36
+ const informalQtyPatterns = [
37
+ {
38
+ // "a pinch of salt"
39
+ regex: /^a\s+(pinch|dash|handful)\s+of\s+(.+)$/i,
40
+ extract: (match) => ({
41
+ quantity: 1,
42
+ unit: match[1].toLowerCase(),
43
+ ingredient: match[2].trim(),
44
+ })
45
+ },
46
+ {
47
+ // "pinch of salt", "dash of hot sauce"
48
+ regex: /^(pinch|dash|handful)\s+of\s+(.+)$/i,
49
+ extract: (match) => ({
50
+ quantity: 1,
51
+ unit: match[1].toLowerCase(),
52
+ ingredient: match[2].trim(),
53
+ })
54
+ },
55
+ {
56
+ // "handful spinach" (no "of")
57
+ regex: /^(pinch|dash|handful)\s+(.+)$/i,
58
+ extract: (match) => ({
59
+ quantity: 1,
60
+ unit: match[1].toLowerCase(),
61
+ ingredient: match[2].trim(),
62
+ })
63
+ },
64
+ ];
65
+ for (const pattern of informalQtyPatterns) {
66
+ const match = ingrStr.match(pattern.regex);
67
+ if (match) {
68
+ const { quantity, unit, ingredient } = pattern.extract(match);
69
+ // Apply descriptor stripping to ingredient
70
+ const wordsToRemove = ['small', 'medium', 'large', 'fresh', 'canned', 'freshly', 'finely', 'roughly', 'coarsely', 'grated', 'chopped'];
71
+ const regex = new RegExp('\\b(' + wordsToRemove.join('|') + ')\\b', 'gi');
72
+ const cleanedIngredient = ingredient.replace(regex, '').trim();
73
+ return {
74
+ quantity,
75
+ unit,
76
+ unitPlural: unit + 's',
77
+ symbol: null,
78
+ ingredient: cleanedIngredient,
79
+ originalIngredientString: originalInput,
80
+ minQty: quantity,
81
+ maxQty: quantity,
82
+ comment: '',
83
+ };
84
+ }
85
+ }
8
86
  // Define regular expressions for text inside parentheses and text before the first comma
9
87
  const parenRegex = /(\(.*?\))/;
10
88
  const commaRegex = /^(.*?)(?=,)/;
@@ -49,14 +127,83 @@ const parseIngredientString = (ingrStr) => {
49
127
  ml: 'milliliter',
50
128
  lb: 'pound',
51
129
  g: 'gram',
130
+ // Additional unit recognitions
131
+ sprigs: 'sprig',
132
+ sprig: 'sprig',
133
+ 'bay leaves': 'bay leaf',
134
+ 'bay leaf': 'bay leaf',
135
+ sheets: 'sheet',
136
+ sheet: 'sheet',
137
+ tblsp: 'tablespoon',
138
+ dessertspoon: 'dessertspoon',
139
+ dessertspoons: 'dessertspoon',
140
+ 'fl oz': 'fluid ounce',
141
+ 'fluid oz': 'fluid ounce',
52
142
  };
53
143
  const unitPattern = new RegExp('\\b(' + Object.keys(unitNormalizations).join('|') + ')\\b', 'gi');
54
144
  const prepIngrText = ingrText.replace(unitPattern, match => { var _a; return (_a = unitNormalizations[match.toLowerCase()]) !== null && _a !== void 0 ? _a : match; });
55
- const parsedIngrRes = (0, parseStringConsecutiveTs_js_1.parseStringConsecutiveTs)(prepIngrText);
145
+ let parsedIngrRes;
146
+ try {
147
+ parsedIngrRes = (0, parseStringConsecutiveTs_js_1.parseStringConsecutiveTs)(prepIngrText);
148
+ }
149
+ catch (_d) {
150
+ // Return degraded result for malformed input (e.g., division by zero)
151
+ return {
152
+ quantity: 0,
153
+ unit: null,
154
+ unitPlural: null,
155
+ symbol: null,
156
+ ingredient: ingrStr.replace(/[^a-zA-Z\s]/g, '').trim(),
157
+ minQty: null,
158
+ maxQty: null,
159
+ originalIngredientString: originalInput,
160
+ comment,
161
+ };
162
+ }
163
+ // Post-processing: check if ingredient name starts with an unrecognized unit
164
+ // The upstream parser doesn't recognize some units, so we extract them manually
165
+ const unrecognizedUnits = [
166
+ 'sprig', 'sprigs',
167
+ 'strip', 'strips',
168
+ 'sheet', 'sheets',
169
+ 'dessertspoon', 'dessertspoons',
170
+ 'handful', 'handfuls',
171
+ 'dash', 'dashes',
172
+ 'bay leaf', 'bay leaves',
173
+ ];
174
+ const unrecognizedUnitPattern = new RegExp('^(' + unrecognizedUnits.join('|') + ')\\b', 'i');
175
+ if (parsedIngrRes.ingredient && parsedIngrRes.unit === null) {
176
+ const unitMatch = parsedIngrRes.ingredient.match(unrecognizedUnitPattern);
177
+ if (unitMatch) {
178
+ let unit = unitMatch[1].toLowerCase();
179
+ // Normalize plural forms to singular
180
+ const unitSingular = {
181
+ sprigs: 'sprig',
182
+ strips: 'strip',
183
+ sheets: 'sheet',
184
+ dessertspoons: 'dessertspoon',
185
+ handfuls: 'handful',
186
+ dashes: 'dash',
187
+ 'bay leaves': 'bay leaf',
188
+ };
189
+ parsedIngrRes.unit = (_c = unitSingular[unit]) !== null && _c !== void 0 ? _c : unit;
190
+ parsedIngrRes.ingredient = parsedIngrRes.ingredient
191
+ .replace(unrecognizedUnitPattern, '')
192
+ .trim();
193
+ }
194
+ }
195
+ // Post-processing: handle "fl oz" / "fluid ounce" which upstream parser normalizes to "ounce"
196
+ if (parsedIngrRes.unit === 'ounce' &&
197
+ (originalInput.toLowerCase().includes('fl oz') ||
198
+ originalInput.toLowerCase().includes('fluid ounce'))) {
199
+ parsedIngrRes.unit = 'fluid ounce';
200
+ // Also fix the symbol
201
+ parsedIngrRes.symbol = 'fl oz';
202
+ }
56
203
  if (!parsedIngrRes.ingredient) {
57
- return Object.assign(Object.assign({}, parsedIngrRes), { originalIngredientString: ingrStr, comment });
204
+ return Object.assign(Object.assign({}, parsedIngrRes), { originalIngredientString: originalInput, comment });
58
205
  }
59
- const wordsToRemove = ['small', 'medium', 'large', 'fresh', 'canned'];
206
+ const wordsToRemove = ['small', 'medium', 'large', 'fresh', 'canned', 'freshly', 'finely', 'roughly', 'coarsely', 'grated', 'chopped'];
60
207
  const regex = new RegExp('\\b(' + wordsToRemove.join('|') + ')\\b', 'gi');
61
208
  const descriptorSet = new Set(wordsToRemove.map(w => w.toLowerCase()));
62
209
  const unit = parsedIngrRes.unit && descriptorSet.has(parsedIngrRes.unit.toLowerCase())
@@ -69,6 +216,6 @@ const parseIngredientString = (ingrStr) => {
69
216
  .replace(/^(fluid|fl|oz) /, '') // Remove "fluid ", "fl ", or "oz " at the beginning of the string
70
217
  .replace(regex, '')
71
218
  .trim();
72
- return Object.assign(Object.assign({}, parsedIngrRes), { unit, ingredient: formattedIngrName, originalIngredientString: ingrStr, comment });
219
+ return Object.assign(Object.assign({}, parsedIngrRes), { unit, ingredient: formattedIngrName, originalIngredientString: originalInput, comment, minQty: extractedMinQty !== null && extractedMinQty !== void 0 ? extractedMinQty : parsedIngrRes.minQty, maxQty: extractedMaxQty !== null && extractedMaxQty !== void 0 ? extractedMaxQty : parsedIngrRes.maxQty });
73
220
  };
74
221
  exports.parseIngredientString = parseIngredientString;
@@ -1,4 +1,3 @@
1
1
  import { ParsedIngredient } from '../../types.js';
2
- type ParsedIngredientOmitType = Omit<ParsedIngredient, 'originalIngredientString' | 'comment'>;
2
+ export type ParsedIngredientOmitType = Omit<ParsedIngredient, 'originalIngredientString' | 'comment'>;
3
3
  export declare const parseStringConsecutiveTs: (ingrStr: string) => ParsedIngredientOmitType;
4
- export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jclind/ingredient-parser",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Parses given sentence including ingredient information and attempts to return quantity, measurement and ingredient data",
5
5
  "keywords": [
6
6
  "recipe",
@@ -37,15 +37,15 @@
37
37
  "homepage": "https://github.com/jclind/ingredient-parser#readme",
38
38
  "dependencies": {
39
39
  "@jclind/ingredient-unit-converter": "^1.1.0",
40
- "axios": "^1.0.0",
40
+ "axios": "^1.16.0",
41
41
  "recipe-ingredient-parser-v3": "^1.5.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/jest": "^30.0.0",
45
- "@types/node": "^22.0.0",
45
+ "@types/node": "^25.6.0",
46
46
  "jest": "^30.3.0",
47
- "rimraf": "^5.0.0",
47
+ "rimraf": "^6.1.3",
48
48
  "ts-jest": "^29.4.9",
49
- "typescript": "^5.0.0"
49
+ "typescript": "^6.0.3"
50
50
  }
51
51
  }