@jclind/ingredient-parser 1.3.0 → 1.3.2
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/README.md
CHANGED
|
@@ -1,211 +1,250 @@
|
|
|
1
|
-
# @jclind/ingredient-parser
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@jclind/ingredient-parser)
|
|
4
|
-
[](https://github.com/jclind/ingredient-parser/blob/main/LICENSE)
|
|
5
|
-
|
|
6
|
-
A TypeScript package for parsing ingredient strings and retrieving structured ingredient data from the Spoonacular API.
|
|
7
|
-
|
|
8
|
-
Built on top of [recipe-ingredient-parser-v3](https://www.npmjs.com/package/recipe-ingredient-parser-v3). If you only need ingredient parsing without ingredient metadata, nutrition, or API lookups, you may prefer using that package directly.
|
|
9
|
-
|
|
10
|
-
## Installation
|
|
11
|
-
|
|
12
|
-
```sh
|
|
13
|
-
npm install @jclind/ingredient-parser
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Quick Start
|
|
17
|
-
|
|
18
|
-
```ts
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
| `
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
{
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
1
|
+
# @jclind/ingredient-parser
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@jclind/ingredient-parser)
|
|
4
|
+
[](https://github.com/jclind/ingredient-parser/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
A TypeScript package for parsing ingredient strings and retrieving structured ingredient data from the Spoonacular API.
|
|
7
|
+
|
|
8
|
+
Built on top of [recipe-ingredient-parser-v3](https://www.npmjs.com/package/recipe-ingredient-parser-v3). If you only need ingredient parsing without ingredient metadata, nutrition, or API lookups, you may prefer using that package directly.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install @jclind/ingredient-parser
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { ingredientParser } from '@jclind/ingredient-parser'
|
|
20
|
+
|
|
21
|
+
const result = await ingredientParser('1 cup rice, washed', 'YOUR_API_KEY', {
|
|
22
|
+
returnNutritionData: true,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
console.log(result)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## API
|
|
29
|
+
|
|
30
|
+
### `ingredientParser(ingredientString, apiKey, options?)`
|
|
31
|
+
|
|
32
|
+
Parses an ingredient string and returns both the parsed ingredient data and ingredient metadata retrieved from Spoonacular.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { ingredientParser } from '@jclind/ingredient-parser'
|
|
36
|
+
|
|
37
|
+
ingredientParser(
|
|
38
|
+
ingredientString: string,
|
|
39
|
+
apiKey: string,
|
|
40
|
+
options?: {
|
|
41
|
+
returnNutritionData?: boolean
|
|
42
|
+
imageSize?: '100x100' | '250x250' | '500x500'
|
|
43
|
+
serverUrl?: string
|
|
44
|
+
}
|
|
45
|
+
): Promise<IngredientResponse>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| Parameter | Type | Required | Description |
|
|
49
|
+
| ------------------ | -------- | -------- | ------------------------------------------------------- |
|
|
50
|
+
| `ingredientString` | `string` | Yes | Ingredient string formatted like `2 cups onions, diced` |
|
|
51
|
+
| `apiKey` | `string` | Yes | Your Spoonacular API key |
|
|
52
|
+
| `options` | `object` | No | Additional parsing options |
|
|
53
|
+
|
|
54
|
+
#### Options
|
|
55
|
+
|
|
56
|
+
| Option | Type | Default | Description |
|
|
57
|
+
| --------------------- | --------------------------------------- | ----------- | ------------------------------------------------------------- |
|
|
58
|
+
| `returnNutritionData` | `boolean` | `false` | Includes Spoonacular nutrition data in `ingredientData` |
|
|
59
|
+
| `imageSize` | `'100x100' \| '250x250' \| '500x500'` | `'100x100'` | Size of the ingredient image returned in `imagePath` |
|
|
60
|
+
| `serverUrl` | `string` | — | Override the default proxy server URL used to call Spoonacular |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
### `parseIngredientString(ingredientString)`
|
|
65
|
+
|
|
66
|
+
Parses an ingredient string locally without any network calls. Use this when you only need quantity, unit, and ingredient name extraction.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { parseIngredientString } from '@jclind/ingredient-parser'
|
|
70
|
+
|
|
71
|
+
parseIngredientString(ingredientString: string): ParsedIngredient
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
parseIngredientString('1 cup rice, washed')
|
|
76
|
+
// {
|
|
77
|
+
// quantity: 1,
|
|
78
|
+
// unit: 'cup',
|
|
79
|
+
// unitPlural: 'cups',
|
|
80
|
+
// symbol: 'c',
|
|
81
|
+
// ingredient: 'rice',
|
|
82
|
+
// originalIngredientString: '1 cup rice, washed',
|
|
83
|
+
// minQty: 1,
|
|
84
|
+
// maxQty: 1,
|
|
85
|
+
// comment: 'washed'
|
|
86
|
+
// }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Response Structure
|
|
92
|
+
|
|
93
|
+
`ingredientParser` returns a discriminated union. On success, `ingredientData` is always present. On error, `error` is present and `ingredientData` is `null`.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// Success
|
|
97
|
+
{
|
|
98
|
+
parsedIngredient: ParsedIngredient
|
|
99
|
+
ingredientData: IngredientData
|
|
100
|
+
id?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Error
|
|
104
|
+
{
|
|
105
|
+
error: { message: string }
|
|
106
|
+
parsedIngredient: ParsedIngredient
|
|
107
|
+
ingredientData: null
|
|
108
|
+
id?: string
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
### `parsedIngredient`
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
{
|
|
118
|
+
quantity: 1,
|
|
119
|
+
unit: 'cup',
|
|
120
|
+
unitPlural: 'cups',
|
|
121
|
+
symbol: 'c',
|
|
122
|
+
ingredient: 'rice',
|
|
123
|
+
originalIngredientString: '1 cup rice, washed',
|
|
124
|
+
minQty: 1,
|
|
125
|
+
maxQty: 1,
|
|
126
|
+
comment: 'washed'
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### `ingredientData`
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
{
|
|
136
|
+
ingredientId: 20444,
|
|
137
|
+
originalName: 'rice',
|
|
138
|
+
name: 'rice',
|
|
139
|
+
amount: 1,
|
|
140
|
+
possibleUnits: ['g', 'oz', 'cup'],
|
|
141
|
+
consistency: 'solid',
|
|
142
|
+
shoppingListUnits: ['ounces', 'pounds'],
|
|
143
|
+
aisle: 'Pasta and Rice',
|
|
144
|
+
image: 'uncooked-white-rice.png',
|
|
145
|
+
imagePath: 'https://spoonacular.com/cdn/ingredients_100x100/uncooked-white-rice.png',
|
|
146
|
+
nutrition?: {
|
|
147
|
+
nutrients: [...],
|
|
148
|
+
properties: [...],
|
|
149
|
+
flavonoids: [...],
|
|
150
|
+
caloricBreakdown: {...},
|
|
151
|
+
weightPerServing: {...}
|
|
152
|
+
},
|
|
153
|
+
totalPriceUSACents: 75.71
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Error Handling
|
|
160
|
+
|
|
161
|
+
The function returns an error object (and `ingredientData: null`) when the ingredient cannot be identified or the API key is invalid. `parsedIngredient` is always populated.
|
|
162
|
+
|
|
163
|
+
### Unknown Ingredient
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const result = await ingredientParser('Invalid Text', 'YOUR_API_KEY')
|
|
167
|
+
|
|
168
|
+
/*
|
|
169
|
+
{
|
|
170
|
+
error: { message: 'Ingredient not formatted correctly or Ingredient Unknown. Please pass ingredient comments/instructions after a comma' },
|
|
171
|
+
ingredientData: null,
|
|
172
|
+
parsedIngredient: {
|
|
173
|
+
quantity: null,
|
|
174
|
+
unit: null,
|
|
175
|
+
unitPlural: null,
|
|
176
|
+
symbol: null,
|
|
177
|
+
ingredient: 'Invalid Text',
|
|
178
|
+
originalIngredientString: 'Invalid Text',
|
|
179
|
+
minQty: null,
|
|
180
|
+
maxQty: null,
|
|
181
|
+
comment: null
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
*/
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Invalid API Key
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const result = await ingredientParser('1 cup rice', 'INVALID_KEY')
|
|
191
|
+
|
|
192
|
+
/*
|
|
193
|
+
{
|
|
194
|
+
error: { message: 'API Key Not Valid' },
|
|
195
|
+
ingredientData: null,
|
|
196
|
+
parsedIngredient: {
|
|
197
|
+
quantity: 1,
|
|
198
|
+
unit: 'cup',
|
|
199
|
+
unitPlural: 'cups',
|
|
200
|
+
symbol: 'c',
|
|
201
|
+
ingredient: 'rice',
|
|
202
|
+
originalIngredientString: '1 cup rice',
|
|
203
|
+
minQty: 1,
|
|
204
|
+
maxQty: 1,
|
|
205
|
+
comment: null
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
*/
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## TypeScript
|
|
214
|
+
|
|
215
|
+
This package ships with full TypeScript definitions. No additional `@types` package required.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import {
|
|
219
|
+
ingredientParser,
|
|
220
|
+
parseIngredientString,
|
|
221
|
+
ParsedIngredient,
|
|
222
|
+
IngredientData,
|
|
223
|
+
IngredientResponse,
|
|
224
|
+
} from '@jclind/ingredient-parser'
|
|
225
|
+
|
|
226
|
+
const result: IngredientResponse = await ingredientParser(
|
|
227
|
+
'1 cup rice, washed',
|
|
228
|
+
'YOUR_API_KEY'
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if ('error' in result) {
|
|
232
|
+
console.error(result.error.message)
|
|
233
|
+
} else {
|
|
234
|
+
const parsed: ParsedIngredient = result.parsedIngredient
|
|
235
|
+
const data: IngredientData = result.ingredientData
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Notes
|
|
242
|
+
|
|
243
|
+
- A valid Spoonacular API key is required for ingredient lookups via `ingredientParser`.
|
|
244
|
+
- `parseIngredientString` works offline with no API key.
|
|
245
|
+
- Nutrition data is optional and disabled by default to reduce API usage.
|
|
246
|
+
- `parsedIngredient` is always populated, even when the API call fails.
|
|
247
|
+
|
|
248
|
+
## Issues & Contributing
|
|
249
|
+
|
|
250
|
+
Found a bug or have a feature request? [Open an issue](https://github.com/jclind/ingredient-parser/issues) on GitHub. PRs are welcome.
|
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.2",
|
|
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
|
}
|