@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.
package/dist/src/api/http.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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": "^
|
|
45
|
+
"@types/node": "^25.6.0",
|
|
46
46
|
"jest": "^30.3.0",
|
|
47
|
-
"rimraf": "^
|
|
47
|
+
"rimraf": "^6.1.3",
|
|
48
48
|
"ts-jest": "^29.4.9",
|
|
49
|
-
"typescript": "^
|
|
49
|
+
"typescript": "^6.0.3"
|
|
50
50
|
}
|
|
51
51
|
}
|