@tmlmt/cooklang-parser 1.0.8 → 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 +412 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -4
- package/dist/index.d.ts +73 -4
- package/dist/index.js +411 -61
- 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
|
-
const [num, den] = clean_str.split("/").map(Number);
|
|
368
|
-
return num / den;
|
|
689
|
+
var parseFixedValue = (input_str) => {
|
|
690
|
+
if (!numberLikeRegex.test(input_str)) {
|
|
691
|
+
return { type: "text", value: input_str };
|
|
369
692
|
}
|
|
370
|
-
|
|
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 };
|
|
699
|
+
}
|
|
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
|
{
|
|
@@ -587,12 +927,14 @@ var Recipe = class _Recipe {
|
|
|
587
927
|
} else if (groups.mCookwareName || groups.sCookwareName) {
|
|
588
928
|
const name = groups.mCookwareName || groups.sCookwareName;
|
|
589
929
|
const modifier = groups.mCookwareModifier || groups.sCookwareModifier;
|
|
930
|
+
const quantityRaw = groups.mCookwareQuantity || groups.sCookwareQuantity;
|
|
590
931
|
const optional = modifier === "?";
|
|
591
932
|
const hidden = modifier === "-";
|
|
592
933
|
const reference = modifier === "&";
|
|
934
|
+
const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
|
|
593
935
|
const idxInList = findAndUpsertCookware(
|
|
594
936
|
this.cookware,
|
|
595
|
-
{ name, optional, hidden },
|
|
937
|
+
{ name, quantity, optional, hidden },
|
|
596
938
|
reference
|
|
597
939
|
);
|
|
598
940
|
items.push({ type: "cookware", value: idxInList });
|
|
@@ -603,7 +945,7 @@ var Recipe = class _Recipe {
|
|
|
603
945
|
throw new Error("Timer missing units");
|
|
604
946
|
}
|
|
605
947
|
const name = groups.timerName || void 0;
|
|
606
|
-
const duration =
|
|
948
|
+
const duration = parseQuantityInput(durationStr);
|
|
607
949
|
const timerObj = {
|
|
608
950
|
name,
|
|
609
951
|
duration,
|
|
@@ -611,7 +953,7 @@ var Recipe = class _Recipe {
|
|
|
611
953
|
};
|
|
612
954
|
const idxInList = findOrPush(
|
|
613
955
|
this.timers,
|
|
614
|
-
(
|
|
956
|
+
(t2) => t2.name === timerObj.name && t2.duration === timerObj.duration && t2.unit === timerObj.unit,
|
|
615
957
|
() => timerObj
|
|
616
958
|
);
|
|
617
959
|
items.push({ type: "timer", value: idxInList });
|
|
@@ -654,8 +996,11 @@ var Recipe = class _Recipe {
|
|
|
654
996
|
throw new Error("Error scaling recipe: no initial servings value set");
|
|
655
997
|
}
|
|
656
998
|
newRecipe.ingredients = newRecipe.ingredients.map((ingredient) => {
|
|
657
|
-
if (ingredient.quantity && !
|
|
658
|
-
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
|
+
);
|
|
659
1004
|
}
|
|
660
1005
|
return ingredient;
|
|
661
1006
|
}).filter((ingredient) => ingredient.quantity !== null);
|
|
@@ -680,6 +1025,11 @@ var Recipe = class _Recipe {
|
|
|
680
1025
|
}
|
|
681
1026
|
return newRecipe;
|
|
682
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
|
+
*/
|
|
683
1033
|
getServings() {
|
|
684
1034
|
if (this.servings) {
|
|
685
1035
|
return this.servings;
|
|
@@ -742,7 +1092,7 @@ var ShoppingList = class {
|
|
|
742
1092
|
continue;
|
|
743
1093
|
}
|
|
744
1094
|
const existingIngredient = this.ingredients.find(
|
|
745
|
-
(
|
|
1095
|
+
(i2) => i2.name === ingredient.name
|
|
746
1096
|
);
|
|
747
1097
|
let addSeparate = false;
|
|
748
1098
|
try {
|
|
@@ -853,6 +1203,7 @@ var ShoppingList = class {
|
|
|
853
1203
|
0 && (module.exports = {
|
|
854
1204
|
AisleConfig,
|
|
855
1205
|
Recipe,
|
|
1206
|
+
Section,
|
|
856
1207
|
ShoppingList
|
|
857
1208
|
});
|
|
858
1209
|
//# sourceMappingURL=index.cjs.map
|