@tmlmt/cooklang-parser 1.0.7 → 1.1.0
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 +2 -2
- package/dist/index.cjs +422 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -3
- package/dist/index.d.ts +78 -3
- package/dist/index.js +421 -62
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -91,13 +91,13 @@ console.log(shoppingList.categories);
|
|
|
91
91
|
|
|
92
92
|
I plan to further develop features depending on the needs I will encounter in using this library in a practical application. The current todo includes:
|
|
93
93
|
|
|
94
|
-
-
|
|
94
|
+
- Ingredients aliases. See issue tmlmt/cooklang-parser#5
|
|
95
95
|
|
|
96
96
|
## Test coverage
|
|
97
97
|
|
|
98
98
|
This project includes a test setup aimed at eventually ensuring reliable parsing/scaling of as many recipe use cases as possible.
|
|
99
99
|
|
|
100
|
-
You can run the tests yourself by cloning the repository and running `pnpm test`. To see the coverage report, run `pnpm
|
|
100
|
+
You can run the tests yourself by cloning the repository and running `pnpm test`. To see the coverage report, run `pnpm test:coverage`.
|
|
101
101
|
|
|
102
102
|
If you find any issue with your own examples of recipes, feel free to open an Issue and if you want to help fix it, to submit a Pull Request.
|
|
103
103
|
|
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ var index_exports = {};
|
|
|
24
24
|
__export(index_exports, {
|
|
25
25
|
AisleConfig: () => AisleConfig,
|
|
26
26
|
Recipe: () => Recipe,
|
|
27
|
+
Section: () => Section,
|
|
27
28
|
ShoppingList: () => ShoppingList
|
|
28
29
|
});
|
|
29
30
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -91,23 +92,233 @@ var AisleConfig = class {
|
|
|
91
92
|
|
|
92
93
|
// src/classes/section.ts
|
|
93
94
|
var Section = class {
|
|
95
|
+
/**
|
|
96
|
+
* Creates an instance of Section.
|
|
97
|
+
* @param name - The name of the section. Defaults to an empty string.
|
|
98
|
+
*/
|
|
94
99
|
constructor(name = "") {
|
|
100
|
+
/** The name of the section. Can be an empty string for the default (first) section. */
|
|
95
101
|
__publicField(this, "name");
|
|
102
|
+
/** An array of steps and notes that make up the content of the section. */
|
|
96
103
|
__publicField(this, "content", []);
|
|
97
104
|
this.name = name;
|
|
98
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Checks if the section is blank (has no name and no content).
|
|
108
|
+
* Used during recipe parsing
|
|
109
|
+
* @returns `true` if the section is blank, otherwise `false`.
|
|
110
|
+
*/
|
|
99
111
|
isBlank() {
|
|
100
112
|
return this.name === "" && this.content.length === 0;
|
|
101
113
|
}
|
|
102
114
|
};
|
|
103
115
|
|
|
116
|
+
// node_modules/.pnpm/human-regex@2.1.5_patch_hash=6d6bd9e233f99785a7c2187fd464edc114b76d47001dbb4eb6b5d72168de7460/node_modules/human-regex/dist/human-regex.esm.js
|
|
117
|
+
var t = /* @__PURE__ */ new Map();
|
|
118
|
+
var r = { GLOBAL: "g", NON_SENSITIVE: "i", MULTILINE: "m", DOT_ALL: "s", UNICODE: "u", STICKY: "y" };
|
|
119
|
+
var e = Object.freeze({ digit: "0-9", lowercaseLetter: "a-z", uppercaseLetter: "A-Z", letter: "a-zA-Z", alphanumeric: "a-zA-Z0-9", anyCharacter: "." });
|
|
120
|
+
var n = Object.freeze({ zeroOrMore: "*", oneOrMore: "+", optional: "?" });
|
|
121
|
+
var a = class {
|
|
122
|
+
constructor() {
|
|
123
|
+
this.parts = [], this.flags = /* @__PURE__ */ new Set();
|
|
124
|
+
}
|
|
125
|
+
digit() {
|
|
126
|
+
return this.add("\\d");
|
|
127
|
+
}
|
|
128
|
+
special() {
|
|
129
|
+
return this.add("(?=.*[!@#$%^&*])");
|
|
130
|
+
}
|
|
131
|
+
word() {
|
|
132
|
+
return this.add("\\w");
|
|
133
|
+
}
|
|
134
|
+
whitespace() {
|
|
135
|
+
return this.add("\\s");
|
|
136
|
+
}
|
|
137
|
+
nonWhitespace() {
|
|
138
|
+
return this.add("\\S");
|
|
139
|
+
}
|
|
140
|
+
literal(r2) {
|
|
141
|
+
return this.add((function(r3) {
|
|
142
|
+
t.has(r3) || t.set(r3, r3.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
143
|
+
return t.get(r3);
|
|
144
|
+
})(r2));
|
|
145
|
+
}
|
|
146
|
+
or() {
|
|
147
|
+
return this.add("|");
|
|
148
|
+
}
|
|
149
|
+
range(t2) {
|
|
150
|
+
const r2 = e[t2];
|
|
151
|
+
if (!r2) throw new Error(`Unknown range: ${t2}`);
|
|
152
|
+
return this.add(`[${r2}]`);
|
|
153
|
+
}
|
|
154
|
+
notRange(t2) {
|
|
155
|
+
const r2 = e[t2];
|
|
156
|
+
if (!r2) throw new Error(`Unknown range: ${t2}`);
|
|
157
|
+
return this.add(`[^${r2}]`);
|
|
158
|
+
}
|
|
159
|
+
anyOf(t2) {
|
|
160
|
+
return this.add(`[${t2}]`);
|
|
161
|
+
}
|
|
162
|
+
notAnyOf(t2) {
|
|
163
|
+
return this.add(`[^${t2}]`);
|
|
164
|
+
}
|
|
165
|
+
lazy() {
|
|
166
|
+
const t2 = this.parts.pop();
|
|
167
|
+
if (!t2) throw new Error("No quantifier to make lazy");
|
|
168
|
+
return this.add(`${t2}?`);
|
|
169
|
+
}
|
|
170
|
+
letter() {
|
|
171
|
+
return this.add("[a-zA-Z]");
|
|
172
|
+
}
|
|
173
|
+
anyCharacter() {
|
|
174
|
+
return this.add(".");
|
|
175
|
+
}
|
|
176
|
+
newline() {
|
|
177
|
+
return this.add("(?:\\r\\n|\\r|\\n)");
|
|
178
|
+
}
|
|
179
|
+
negativeLookahead(t2) {
|
|
180
|
+
return this.add(`(?!${t2})`);
|
|
181
|
+
}
|
|
182
|
+
positiveLookahead(t2) {
|
|
183
|
+
return this.add(`(?=${t2})`);
|
|
184
|
+
}
|
|
185
|
+
positiveLookbehind(t2) {
|
|
186
|
+
return this.add(`(?<=${t2})`);
|
|
187
|
+
}
|
|
188
|
+
negativeLookbehind(t2) {
|
|
189
|
+
return this.add(`(?<!${t2})`);
|
|
190
|
+
}
|
|
191
|
+
hasSpecialCharacter() {
|
|
192
|
+
return this.add("(?=.*[!@#$%^&*])");
|
|
193
|
+
}
|
|
194
|
+
hasDigit() {
|
|
195
|
+
return this.add("(?=.*\\d)");
|
|
196
|
+
}
|
|
197
|
+
hasLetter() {
|
|
198
|
+
return this.add("(?=.*[a-zA-Z])");
|
|
199
|
+
}
|
|
200
|
+
optional() {
|
|
201
|
+
return this.add(n.optional);
|
|
202
|
+
}
|
|
203
|
+
exactly(t2) {
|
|
204
|
+
return this.add(`{${t2}}`);
|
|
205
|
+
}
|
|
206
|
+
atLeast(t2) {
|
|
207
|
+
return this.add(`{${t2},}`);
|
|
208
|
+
}
|
|
209
|
+
atMost(t2) {
|
|
210
|
+
return this.add(`{0,${t2}}`);
|
|
211
|
+
}
|
|
212
|
+
between(t2, r2) {
|
|
213
|
+
return this.add(`{${t2},${r2}}`);
|
|
214
|
+
}
|
|
215
|
+
oneOrMore() {
|
|
216
|
+
return this.add(n.oneOrMore);
|
|
217
|
+
}
|
|
218
|
+
zeroOrMore() {
|
|
219
|
+
return this.add(n.zeroOrMore);
|
|
220
|
+
}
|
|
221
|
+
startNamedGroup(t2) {
|
|
222
|
+
return this.add(`(?<${t2}>`);
|
|
223
|
+
}
|
|
224
|
+
startGroup() {
|
|
225
|
+
return this.add("(?:");
|
|
226
|
+
}
|
|
227
|
+
startCaptureGroup() {
|
|
228
|
+
return this.add("(");
|
|
229
|
+
}
|
|
230
|
+
wordBoundary() {
|
|
231
|
+
return this.add("\\b");
|
|
232
|
+
}
|
|
233
|
+
nonWordBoundary() {
|
|
234
|
+
return this.add("\\B");
|
|
235
|
+
}
|
|
236
|
+
endGroup() {
|
|
237
|
+
return this.add(")");
|
|
238
|
+
}
|
|
239
|
+
startAnchor() {
|
|
240
|
+
return this.add("^");
|
|
241
|
+
}
|
|
242
|
+
endAnchor() {
|
|
243
|
+
return this.add("$");
|
|
244
|
+
}
|
|
245
|
+
global() {
|
|
246
|
+
return this.flags.add(r.GLOBAL), this;
|
|
247
|
+
}
|
|
248
|
+
nonSensitive() {
|
|
249
|
+
return this.flags.add(r.NON_SENSITIVE), this;
|
|
250
|
+
}
|
|
251
|
+
multiline() {
|
|
252
|
+
return this.flags.add(r.MULTILINE), this;
|
|
253
|
+
}
|
|
254
|
+
dotAll() {
|
|
255
|
+
return this.flags.add(r.DOT_ALL), this;
|
|
256
|
+
}
|
|
257
|
+
sticky() {
|
|
258
|
+
return this.flags.add(r.STICKY), this;
|
|
259
|
+
}
|
|
260
|
+
unicodeChar(t2) {
|
|
261
|
+
this.flags.add(r.UNICODE);
|
|
262
|
+
const e2 = /* @__PURE__ */ new Set(["u", "l", "t", "m", "o"]);
|
|
263
|
+
if (void 0 !== t2 && !e2.has(t2)) throw new Error(`Invalid Unicode letter variant: ${t2}`);
|
|
264
|
+
return this.add(`\\p{L${null != t2 ? t2 : ""}}`);
|
|
265
|
+
}
|
|
266
|
+
unicodeDigit() {
|
|
267
|
+
return this.flags.add(r.UNICODE), this.add("\\p{N}");
|
|
268
|
+
}
|
|
269
|
+
unicodePunctuation() {
|
|
270
|
+
return this.flags.add(r.UNICODE), this.add("\\p{P}");
|
|
271
|
+
}
|
|
272
|
+
unicodeSymbol() {
|
|
273
|
+
return this.flags.add(r.UNICODE), this.add("\\p{S}");
|
|
274
|
+
}
|
|
275
|
+
repeat(t2) {
|
|
276
|
+
if (0 === this.parts.length) throw new Error("No pattern to repeat");
|
|
277
|
+
const r2 = this.parts.pop();
|
|
278
|
+
return this.parts.push(`(${r2}){${t2}}`), this;
|
|
279
|
+
}
|
|
280
|
+
ipv4Octet() {
|
|
281
|
+
return this.add("(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)");
|
|
282
|
+
}
|
|
283
|
+
protocol() {
|
|
284
|
+
return this.add("https?://");
|
|
285
|
+
}
|
|
286
|
+
www() {
|
|
287
|
+
return this.add("(www\\.)?");
|
|
288
|
+
}
|
|
289
|
+
tld() {
|
|
290
|
+
return this.add("(com|org|net)");
|
|
291
|
+
}
|
|
292
|
+
path() {
|
|
293
|
+
return this.add("(/\\w+)*");
|
|
294
|
+
}
|
|
295
|
+
add(t2) {
|
|
296
|
+
return this.parts.push(t2), this;
|
|
297
|
+
}
|
|
298
|
+
toString() {
|
|
299
|
+
return this.parts.join("");
|
|
300
|
+
}
|
|
301
|
+
toRegExp() {
|
|
302
|
+
return new RegExp(this.toString(), [...this.flags].join(""));
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
var d = () => new a();
|
|
306
|
+
var i = (() => {
|
|
307
|
+
const t2 = (t3) => {
|
|
308
|
+
const r2 = t3().toRegExp();
|
|
309
|
+
return () => new RegExp(r2.source, r2.flags);
|
|
310
|
+
};
|
|
311
|
+
return { email: t2((() => d().startAnchor().word().oneOrMore().literal("@").word().oneOrMore().startGroup().literal(".").word().oneOrMore().endGroup().zeroOrMore().literal(".").letter().atLeast(2).endAnchor())), url: t2((() => d().startAnchor().protocol().www().word().oneOrMore().literal(".").tld().path().endAnchor())), phoneInternational: t2((() => d().startAnchor().literal("+").digit().between(1, 3).literal("-").digit().between(3, 14).endAnchor())) };
|
|
312
|
+
})();
|
|
313
|
+
|
|
104
314
|
// src/regex.ts
|
|
105
|
-
var metadataRegex =
|
|
106
|
-
var
|
|
107
|
-
var
|
|
108
|
-
var
|
|
109
|
-
var
|
|
110
|
-
var
|
|
315
|
+
var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
|
|
316
|
+
var nonWordChar = "\\s@#~\\[\\]{(.,;:!?";
|
|
317
|
+
var multiwordIngredient = d().literal("@").startNamedGroup("mIngredientModifier").anyOf("@\\-&?").endGroup().optional().startNamedGroup("mIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").startGroup().literal("{").startNamedGroup("mIngredientQuantity").notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("mIngredientUnits").notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("mIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
|
|
318
|
+
var singleWordIngredient = d().literal("@").startNamedGroup("sIngredientModifier").anyOf("@\\-&?").endGroup().optional().startNamedGroup("sIngredientName").notAnyOf(nonWordChar).oneOrMore().endGroup().startGroup().literal("{").startNamedGroup("sIngredientQuantity").notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("sIngredientUnits").notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("sIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
|
|
319
|
+
var multiwordCookware = d().literal("#").startNamedGroup("mCookwareModifier").anyOf("\\-&?").endGroup().optional().startNamedGroup("mCookwareName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\})").literal("{").startNamedGroup("mCookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").toRegExp();
|
|
320
|
+
var singleWordCookware = d().literal("#").startNamedGroup("sCookwareModifier").anyOf("\\-&?").endGroup().optional().startNamedGroup("sCookwareName").notAnyOf(nonWordChar).oneOrMore().endGroup().startGroup().literal("{").startNamedGroup("sCookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").endGroup().optional().toRegExp();
|
|
321
|
+
var timer = d().literal("~").startNamedGroup("timerName").anyCharacter().zeroOrMore().lazy().endGroup().literal("{").startNamedGroup("timerQuantity").anyCharacter().oneOrMore().lazy().endGroup().startGroup().literal("%").startNamedGroup("timerUnits").anyCharacter().oneOrMore().lazy().endGroup().endGroup().optional().literal("}").toRegExp();
|
|
111
322
|
var tokensRegex = new RegExp(
|
|
112
323
|
[
|
|
113
324
|
multiwordIngredient,
|
|
@@ -115,11 +326,14 @@ var tokensRegex = new RegExp(
|
|
|
115
326
|
multiwordCookware,
|
|
116
327
|
singleWordCookware,
|
|
117
328
|
timer
|
|
118
|
-
].map((
|
|
329
|
+
].map((r2) => r2.source).join("|"),
|
|
119
330
|
"gu"
|
|
120
331
|
);
|
|
121
|
-
var commentRegex =
|
|
122
|
-
var blockCommentRegex =
|
|
332
|
+
var commentRegex = d().literal("--").anyCharacter().zeroOrMore().global().toRegExp();
|
|
333
|
+
var blockCommentRegex = d().whitespace().zeroOrMore().literal("[-").anyCharacter().zeroOrMore().lazy().literal("-]").whitespace().zeroOrMore().global().toRegExp();
|
|
334
|
+
var shoppingListRegex = d().literal("[").startNamedGroup("name").anyCharacter().oneOrMore().endGroup().literal("]").newline().startNamedGroup("items").anyCharacter().zeroOrMore().lazy().endGroup().startGroup().newline().newline().or().endAnchor().endGroup().global().toRegExp();
|
|
335
|
+
var rangeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().literal("-").startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
336
|
+
var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
|
|
123
337
|
|
|
124
338
|
// src/units.ts
|
|
125
339
|
var units = [
|
|
@@ -237,46 +451,148 @@ for (const unit of units) {
|
|
|
237
451
|
function normalizeUnit(unit) {
|
|
238
452
|
return unitMap.get(unit.toLowerCase().trim());
|
|
239
453
|
}
|
|
454
|
+
var CannotAddTextValueError = class extends Error {
|
|
455
|
+
constructor() {
|
|
456
|
+
super("Cannot add a quantity with a text value.");
|
|
457
|
+
this.name = "CannotAddTextValueError";
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
var IncompatibleUnitsError = class extends Error {
|
|
461
|
+
constructor(unit1, unit2) {
|
|
462
|
+
super(
|
|
463
|
+
`Cannot add quantities with incompatible or unknown units: ${unit1} and ${unit2}`
|
|
464
|
+
);
|
|
465
|
+
this.name = "IncompatibleUnitsError";
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
function gcd(a2, b) {
|
|
469
|
+
return b === 0 ? a2 : gcd(b, a2 % b);
|
|
470
|
+
}
|
|
471
|
+
function simplifyFraction(num, den) {
|
|
472
|
+
if (den === 0) {
|
|
473
|
+
throw new Error("Denominator cannot be zero.");
|
|
474
|
+
}
|
|
475
|
+
const commonDivisor = gcd(Math.abs(num), Math.abs(den));
|
|
476
|
+
let simplifiedNum = num / commonDivisor;
|
|
477
|
+
let simplifiedDen = den / commonDivisor;
|
|
478
|
+
if (simplifiedDen < 0) {
|
|
479
|
+
simplifiedNum = -simplifiedNum;
|
|
480
|
+
simplifiedDen = -simplifiedDen;
|
|
481
|
+
}
|
|
482
|
+
if (simplifiedDen === 1) {
|
|
483
|
+
return { type: "decimal", value: simplifiedNum };
|
|
484
|
+
} else {
|
|
485
|
+
return { type: "fraction", num: simplifiedNum, den: simplifiedDen };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function multiplyNumericValue(v, factor) {
|
|
489
|
+
if (v.type === "decimal") {
|
|
490
|
+
return { type: "decimal", value: v.value * factor };
|
|
491
|
+
}
|
|
492
|
+
return simplifyFraction(v.num * factor, v.den);
|
|
493
|
+
}
|
|
494
|
+
function addNumericValues(val1, val2) {
|
|
495
|
+
let num1;
|
|
496
|
+
let den1;
|
|
497
|
+
let num2;
|
|
498
|
+
let den2;
|
|
499
|
+
if (val1.type === "decimal") {
|
|
500
|
+
num1 = val1.value;
|
|
501
|
+
den1 = 1;
|
|
502
|
+
} else {
|
|
503
|
+
num1 = val1.num;
|
|
504
|
+
den1 = val1.den;
|
|
505
|
+
}
|
|
506
|
+
if (val2.type === "decimal") {
|
|
507
|
+
num2 = val2.value;
|
|
508
|
+
den2 = 1;
|
|
509
|
+
} else {
|
|
510
|
+
num2 = val2.num;
|
|
511
|
+
den2 = val2.den;
|
|
512
|
+
}
|
|
513
|
+
if (val1.type === "fraction" && val2.type === "fraction") {
|
|
514
|
+
const commonDen = den1 * den2;
|
|
515
|
+
const sumNum = num1 * den2 + num2 * den1;
|
|
516
|
+
return simplifyFraction(sumNum, commonDen);
|
|
517
|
+
} else {
|
|
518
|
+
return { type: "decimal", value: num1 / den1 + num2 / den2 };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
var toRoundedDecimal = (v) => {
|
|
522
|
+
const value = v.type === "decimal" ? v.value : v.num / v.den;
|
|
523
|
+
return { type: "decimal", value: Math.floor(value * 100) / 100 };
|
|
524
|
+
};
|
|
525
|
+
function multiplyQuantityValue(value, factor) {
|
|
526
|
+
if (value.type === "fixed") {
|
|
527
|
+
return {
|
|
528
|
+
type: "fixed",
|
|
529
|
+
value: toRoundedDecimal(
|
|
530
|
+
multiplyNumericValue(
|
|
531
|
+
value.value,
|
|
532
|
+
factor
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
type: "range",
|
|
539
|
+
min: toRoundedDecimal(
|
|
540
|
+
multiplyNumericValue(value.min, factor)
|
|
541
|
+
),
|
|
542
|
+
max: toRoundedDecimal(
|
|
543
|
+
multiplyNumericValue(value.max, factor)
|
|
544
|
+
)
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
var convertQuantityValue = (value, def, targetDef) => {
|
|
548
|
+
if (def.name === targetDef.name) return value;
|
|
549
|
+
const factor = def.toBase / targetDef.toBase;
|
|
550
|
+
return multiplyQuantityValue(value, factor);
|
|
551
|
+
};
|
|
240
552
|
function addQuantities(q1, q2) {
|
|
553
|
+
const v1 = q1.value;
|
|
554
|
+
const v2 = q2.value;
|
|
555
|
+
if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
|
|
556
|
+
throw new CannotAddTextValueError();
|
|
557
|
+
}
|
|
241
558
|
const unit1Def = normalizeUnit(q1.unit);
|
|
242
559
|
const unit2Def = normalizeUnit(q2.unit);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
560
|
+
const addQuantityValuesAndSetUnit = (val1, val2, unit) => {
|
|
561
|
+
if (val1.type === "fixed" && val2.type === "fixed") {
|
|
562
|
+
const res = addNumericValues(
|
|
563
|
+
val1.value,
|
|
564
|
+
val2.value
|
|
565
|
+
);
|
|
566
|
+
return { value: { type: "fixed", value: res }, unit };
|
|
567
|
+
}
|
|
568
|
+
const r1 = val1.type === "range" ? val1 : { type: "range", min: val1.value, max: val1.value };
|
|
569
|
+
const r2 = val2.type === "range" ? val2 : { type: "range", min: val2.value, max: val2.value };
|
|
570
|
+
const newMin = addNumericValues(
|
|
571
|
+
r1.min,
|
|
572
|
+
r2.min
|
|
246
573
|
);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
`Cannot add quantity to string-quantified value: ${q2.value}`
|
|
574
|
+
const newMax = addNumericValues(
|
|
575
|
+
r1.max,
|
|
576
|
+
r2.max
|
|
251
577
|
);
|
|
252
|
-
|
|
578
|
+
return { value: { type: "range", min: newMin, max: newMax }, unit };
|
|
579
|
+
};
|
|
253
580
|
if (q1.unit === "" && unit2Def) {
|
|
254
|
-
return
|
|
255
|
-
value: Math.round((q1.value + q2.value) * 100) / 100,
|
|
256
|
-
unit: q2.unit
|
|
257
|
-
};
|
|
581
|
+
return addQuantityValuesAndSetUnit(v1, v2, q2.unit);
|
|
258
582
|
}
|
|
259
583
|
if (q2.unit === "" && unit1Def) {
|
|
260
|
-
return
|
|
261
|
-
value: Math.round((q1.value + q2.value) * 100) / 100,
|
|
262
|
-
unit: q1.unit
|
|
263
|
-
};
|
|
584
|
+
return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
|
|
264
585
|
}
|
|
265
586
|
if (q1.unit.toLowerCase() === q2.unit.toLowerCase()) {
|
|
266
|
-
return
|
|
267
|
-
value: Math.round((q1.value + q2.value) * 100) / 100,
|
|
268
|
-
unit: q1.unit
|
|
269
|
-
};
|
|
587
|
+
return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
|
|
270
588
|
}
|
|
271
589
|
if (unit1Def && unit2Def) {
|
|
272
590
|
if (unit1Def.type !== unit2Def.type) {
|
|
273
|
-
throw new
|
|
274
|
-
|
|
591
|
+
throw new IncompatibleUnitsError(
|
|
592
|
+
`${unit1Def.type} (${q1.unit})`,
|
|
593
|
+
`${unit2Def.type} (${q2.unit})`
|
|
275
594
|
);
|
|
276
595
|
}
|
|
277
|
-
const baseValue1 = q1.value * unit1Def.toBase;
|
|
278
|
-
const baseValue2 = q2.value * unit2Def.toBase;
|
|
279
|
-
const totalBaseValue = baseValue1 + baseValue2;
|
|
280
596
|
let targetUnitDef;
|
|
281
597
|
if (unit1Def.system !== unit2Def.system) {
|
|
282
598
|
const metricUnitDef = unit1Def.system === "metric" ? unit1Def : unit2Def;
|
|
@@ -286,15 +602,15 @@ function addQuantities(q1, q2) {
|
|
|
286
602
|
} else {
|
|
287
603
|
targetUnitDef = unit1Def.toBase >= unit2Def.toBase ? unit1Def : unit2Def;
|
|
288
604
|
}
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
605
|
+
const convertedV1 = convertQuantityValue(v1, unit1Def, targetUnitDef);
|
|
606
|
+
const convertedV2 = convertQuantityValue(v2, unit2Def, targetUnitDef);
|
|
607
|
+
return addQuantityValuesAndSetUnit(
|
|
608
|
+
convertedV1,
|
|
609
|
+
convertedV2,
|
|
610
|
+
targetUnitDef.name
|
|
611
|
+
);
|
|
294
612
|
}
|
|
295
|
-
throw new
|
|
296
|
-
`Cannot add quantities with incompatible or unknown units: ${q1.unit} and ${q2.unit}`
|
|
297
|
-
);
|
|
613
|
+
throw new IncompatibleUnitsError(q1.unit, q2.unit);
|
|
298
614
|
}
|
|
299
615
|
|
|
300
616
|
// src/parser_helpers.ts
|
|
@@ -324,7 +640,7 @@ function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
|
|
|
324
640
|
const { name, quantity, unit } = newIngredient;
|
|
325
641
|
if (isReference) {
|
|
326
642
|
const index = ingredients.findIndex(
|
|
327
|
-
(
|
|
643
|
+
(i2) => i2.name.toLowerCase() === name.toLowerCase()
|
|
328
644
|
);
|
|
329
645
|
if (index === -1) {
|
|
330
646
|
throw new Error(
|
|
@@ -334,13 +650,22 @@ function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
|
|
|
334
650
|
const existingIngredient = ingredients[index];
|
|
335
651
|
if (quantity !== void 0) {
|
|
336
652
|
const currentQuantity = {
|
|
337
|
-
value: existingIngredient.quantity ??
|
|
653
|
+
value: existingIngredient.quantity ?? {
|
|
654
|
+
type: "fixed",
|
|
655
|
+
value: { type: "decimal", value: 0 }
|
|
656
|
+
},
|
|
338
657
|
unit: existingIngredient.unit ?? ""
|
|
339
658
|
};
|
|
340
659
|
const newQuantity = { value: quantity, unit: unit ?? "" };
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
660
|
+
try {
|
|
661
|
+
const total = addQuantities(currentQuantity, newQuantity);
|
|
662
|
+
existingIngredient.quantity = total.value;
|
|
663
|
+
existingIngredient.unit = total.unit || void 0;
|
|
664
|
+
} catch (e2) {
|
|
665
|
+
if (e2 instanceof IncompatibleUnitsError || e2 instanceof CannotAddTextValueError) {
|
|
666
|
+
return ingredients.push(newIngredient) - 1;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
344
669
|
}
|
|
345
670
|
return index;
|
|
346
671
|
}
|
|
@@ -350,7 +675,7 @@ function findAndUpsertCookware(cookware, newCookware, isReference) {
|
|
|
350
675
|
const { name } = newCookware;
|
|
351
676
|
if (isReference) {
|
|
352
677
|
const index = cookware.findIndex(
|
|
353
|
-
(
|
|
678
|
+
(i2) => i2.name.toLowerCase() === name.toLowerCase()
|
|
354
679
|
);
|
|
355
680
|
if (index === -1) {
|
|
356
681
|
throw new Error(
|
|
@@ -361,13 +686,28 @@ function findAndUpsertCookware(cookware, newCookware, isReference) {
|
|
|
361
686
|
}
|
|
362
687
|
return cookware.push(newCookware) - 1;
|
|
363
688
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
689
|
+
var parseFixedValue = (input_str) => {
|
|
690
|
+
if (!numberLikeRegex.test(input_str)) {
|
|
691
|
+
return { type: "text", value: input_str };
|
|
692
|
+
}
|
|
693
|
+
const s = input_str.trim().replace(",", ".");
|
|
694
|
+
if (s.includes("/")) {
|
|
695
|
+
const parts = s.split("/");
|
|
696
|
+
const num = Number(parts[0]);
|
|
697
|
+
const den = Number(parts[1]);
|
|
698
|
+
return { type: "fraction", num, den };
|
|
369
699
|
}
|
|
370
|
-
return Number(
|
|
700
|
+
return { type: "decimal", value: Number(s) };
|
|
701
|
+
};
|
|
702
|
+
function parseQuantityInput(input_str) {
|
|
703
|
+
const clean_str = String(input_str).trim();
|
|
704
|
+
if (rangeRegex.test(clean_str)) {
|
|
705
|
+
const range_parts = clean_str.split("-");
|
|
706
|
+
const min = parseFixedValue(range_parts[0].trim());
|
|
707
|
+
const max = parseFixedValue(range_parts[1].trim());
|
|
708
|
+
return { type: "range", min, max };
|
|
709
|
+
}
|
|
710
|
+
return { type: "fixed", value: parseFixedValue(clean_str) };
|
|
371
711
|
}
|
|
372
712
|
function parseSimpleMetaVar(content, varName) {
|
|
373
713
|
const varMatch = content.match(
|
|
@@ -560,7 +900,7 @@ var Recipe = class _Recipe {
|
|
|
560
900
|
const hidden = modifier === "-";
|
|
561
901
|
const reference = modifier === "&";
|
|
562
902
|
const isRecipe = modifier === "@";
|
|
563
|
-
const quantity = quantityRaw ?
|
|
903
|
+
const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
|
|
564
904
|
const idxInList = findAndUpsertIngredient(
|
|
565
905
|
this.ingredients,
|
|
566
906
|
{
|
|
@@ -574,16 +914,27 @@ var Recipe = class _Recipe {
|
|
|
574
914
|
},
|
|
575
915
|
reference
|
|
576
916
|
);
|
|
577
|
-
|
|
917
|
+
const newItem = {
|
|
918
|
+
type: "ingredient",
|
|
919
|
+
value: idxInList
|
|
920
|
+
};
|
|
921
|
+
if (reference) {
|
|
922
|
+
newItem.partialQuantity = quantity;
|
|
923
|
+
newItem.partialUnit = units2;
|
|
924
|
+
newItem.partialPreparation = preparation;
|
|
925
|
+
}
|
|
926
|
+
items.push(newItem);
|
|
578
927
|
} else if (groups.mCookwareName || groups.sCookwareName) {
|
|
579
928
|
const name = groups.mCookwareName || groups.sCookwareName;
|
|
580
929
|
const modifier = groups.mCookwareModifier || groups.sCookwareModifier;
|
|
930
|
+
const quantityRaw = groups.mCookwareQuantity || groups.sCookwareQuantity;
|
|
581
931
|
const optional = modifier === "?";
|
|
582
932
|
const hidden = modifier === "-";
|
|
583
933
|
const reference = modifier === "&";
|
|
934
|
+
const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
|
|
584
935
|
const idxInList = findAndUpsertCookware(
|
|
585
936
|
this.cookware,
|
|
586
|
-
{ name, optional, hidden },
|
|
937
|
+
{ name, quantity, optional, hidden },
|
|
587
938
|
reference
|
|
588
939
|
);
|
|
589
940
|
items.push({ type: "cookware", value: idxInList });
|
|
@@ -594,7 +945,7 @@ var Recipe = class _Recipe {
|
|
|
594
945
|
throw new Error("Timer missing units");
|
|
595
946
|
}
|
|
596
947
|
const name = groups.timerName || void 0;
|
|
597
|
-
const duration =
|
|
948
|
+
const duration = parseQuantityInput(durationStr);
|
|
598
949
|
const timerObj = {
|
|
599
950
|
name,
|
|
600
951
|
duration,
|
|
@@ -602,7 +953,7 @@ var Recipe = class _Recipe {
|
|
|
602
953
|
};
|
|
603
954
|
const idxInList = findOrPush(
|
|
604
955
|
this.timers,
|
|
605
|
-
(
|
|
956
|
+
(t2) => t2.name === timerObj.name && t2.duration === timerObj.duration && t2.unit === timerObj.unit,
|
|
606
957
|
() => timerObj
|
|
607
958
|
);
|
|
608
959
|
items.push({ type: "timer", value: idxInList });
|
|
@@ -645,8 +996,11 @@ var Recipe = class _Recipe {
|
|
|
645
996
|
throw new Error("Error scaling recipe: no initial servings value set");
|
|
646
997
|
}
|
|
647
998
|
newRecipe.ingredients = newRecipe.ingredients.map((ingredient) => {
|
|
648
|
-
if (ingredient.quantity && !
|
|
649
|
-
ingredient.quantity
|
|
999
|
+
if (ingredient.quantity && !(ingredient.quantity.type === "fixed" && ingredient.quantity.value.type === "text")) {
|
|
1000
|
+
ingredient.quantity = multiplyQuantityValue(
|
|
1001
|
+
ingredient.quantity,
|
|
1002
|
+
factor
|
|
1003
|
+
);
|
|
650
1004
|
}
|
|
651
1005
|
return ingredient;
|
|
652
1006
|
}).filter((ingredient) => ingredient.quantity !== null);
|
|
@@ -671,6 +1025,11 @@ var Recipe = class _Recipe {
|
|
|
671
1025
|
}
|
|
672
1026
|
return newRecipe;
|
|
673
1027
|
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Gets the number of servings for the recipe.
|
|
1030
|
+
* @private
|
|
1031
|
+
* @returns The number of servings, or undefined if not set.
|
|
1032
|
+
*/
|
|
674
1033
|
getServings() {
|
|
675
1034
|
if (this.servings) {
|
|
676
1035
|
return this.servings;
|
|
@@ -733,7 +1092,7 @@ var ShoppingList = class {
|
|
|
733
1092
|
continue;
|
|
734
1093
|
}
|
|
735
1094
|
const existingIngredient = this.ingredients.find(
|
|
736
|
-
(
|
|
1095
|
+
(i2) => i2.name === ingredient.name
|
|
737
1096
|
);
|
|
738
1097
|
let addSeparate = false;
|
|
739
1098
|
try {
|
|
@@ -844,6 +1203,7 @@ var ShoppingList = class {
|
|
|
844
1203
|
0 && (module.exports = {
|
|
845
1204
|
AisleConfig,
|
|
846
1205
|
Recipe,
|
|
1206
|
+
Section,
|
|
847
1207
|
ShoppingList
|
|
848
1208
|
});
|
|
849
1209
|
//# sourceMappingURL=index.cjs.map
|