@tmlmt/cooklang-parser 1.0.3 → 1.0.5

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
@@ -34,10 +34,12 @@ const recipeString = `
34
34
  title: Pancakes
35
35
  tags: breakfast, easy
36
36
  ---
37
-
38
37
  Crack the @eggs{3} into a bowl, and add @coarse salt{}.
38
+
39
39
  Melt the @butter{50%g} in a #pan on medium heat.
40
+
40
41
  Cook for ~{15%minutes}.
42
+
41
43
  Serve hot.
42
44
  `;
43
45
 
package/dist/index.cjs ADDED
@@ -0,0 +1,849 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
21
+
22
+ // src/index.ts
23
+ var index_exports = {};
24
+ __export(index_exports, {
25
+ AisleConfig: () => AisleConfig,
26
+ Recipe: () => Recipe,
27
+ ShoppingList: () => ShoppingList
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/classes/aisle_config.ts
32
+ var AisleConfig = class {
33
+ /**
34
+ * Creates a new AisleConfig instance.
35
+ * @param config - The aisle configuration to parse.
36
+ */
37
+ constructor(config) {
38
+ /**
39
+ * The categories of aisles.
40
+ * @see {@link AisleCategory}
41
+ */
42
+ __publicField(this, "categories", []);
43
+ if (config) {
44
+ this.parse(config);
45
+ }
46
+ }
47
+ /**
48
+ * Parses an aisle configuration from a string.
49
+ * @param config - The aisle configuration to parse.
50
+ */
51
+ parse(config) {
52
+ let currentCategory = null;
53
+ const categoryNames = /* @__PURE__ */ new Set();
54
+ const ingredientNames = /* @__PURE__ */ new Set();
55
+ for (const line of config.split("\n")) {
56
+ const trimmedLine = line.trim();
57
+ if (trimmedLine.length === 0) {
58
+ continue;
59
+ }
60
+ if (trimmedLine.startsWith("[") && trimmedLine.endsWith("]")) {
61
+ const categoryName = trimmedLine.substring(1, trimmedLine.length - 1).trim();
62
+ if (categoryNames.has(categoryName)) {
63
+ throw new Error(`Duplicate category found: ${categoryName}`);
64
+ }
65
+ categoryNames.add(categoryName);
66
+ currentCategory = { name: categoryName, ingredients: [] };
67
+ this.categories.push(currentCategory);
68
+ } else {
69
+ if (currentCategory === null) {
70
+ throw new Error(
71
+ `Ingredient found without a category: ${trimmedLine}`
72
+ );
73
+ }
74
+ const aliases = trimmedLine.split("|").map((s) => s.trim());
75
+ for (const alias of aliases) {
76
+ if (ingredientNames.has(alias)) {
77
+ throw new Error(`Duplicate ingredient/alias found: ${alias}`);
78
+ }
79
+ ingredientNames.add(alias);
80
+ }
81
+ const ingredient = {
82
+ name: aliases[0],
83
+ // We know this exists because trimmedLine is not empty
84
+ aliases
85
+ };
86
+ currentCategory.ingredients.push(ingredient);
87
+ }
88
+ }
89
+ }
90
+ };
91
+
92
+ // src/classes/section.ts
93
+ var Section = class {
94
+ constructor(name = "") {
95
+ __publicField(this, "name");
96
+ __publicField(this, "content", []);
97
+ this.name = name;
98
+ }
99
+ isBlank() {
100
+ return this.name === "" && this.content.length === 0;
101
+ }
102
+ };
103
+
104
+ // src/regex.ts
105
+ var metadataRegex = /---\n(.*?)\n---/s;
106
+ var multiwordIngredient = /@(?<mIngredientModifier>[@\-&+*!?])?(?<mIngredientName>(?:[^\s@#~\[\]{(.,;:!?]+(?:\s+[^\s@#~\[\]{(.,;:!?]+)+))(?=\s*(?:\{|\}|\(\s*[^)]*\s*\)))(?:\{(?<mIngredientQuantity>\p{No}|(?:\p{Nd}+(?:[.,\/][\p{Nd}]+)?))?(?:%(?<mIngredientUnits>[^}]+?))?\})?(?:\((?<mIngredientPreparation>[^)]*?)\))?/gu;
107
+ var singleWordIngredient = /@(?<sIngredientModifier>[@\-&+*!?])?(?<sIngredientName>[^\s@#~\[\]{(.,;:!?]+)(?:\{(?<sIngredientQuantity>\p{No}|(?:\p{Nd}+(?:[.,\/][\p{Nd}]+)?))(?:%(?<sIngredientUnits>[^}]+?))?\})?(?:\((?<sIngredientPreparation>[^)]*?)\))?/gu;
108
+ var multiwordCookware = /#(?<mCookwareModifier>[\-&+*!?])?(?<mCookwareName>[^#~[]+?)\{(?<mCookwareQuantity>.*?)\}/;
109
+ var singleWordCookware = /#(?<sCookwareModifier>[\-&+*!?])?(?<sCookwareName>[^\s\t\s\p{P}]+)/u;
110
+ var timer = /~(?<timerName>.*?)(?:\{(?<timerQuantity>.*?)(?:%(?<timerUnits>.+?))?\})/;
111
+ var tokensRegex = new RegExp(
112
+ [
113
+ multiwordIngredient,
114
+ singleWordIngredient,
115
+ multiwordCookware,
116
+ singleWordCookware,
117
+ timer
118
+ ].map((r) => r.source).join("|"),
119
+ "gu"
120
+ );
121
+ var commentRegex = /--.*/g;
122
+ var blockCommentRegex = /\s*\[\-.*?\-\]\s*/g;
123
+
124
+ // src/units.ts
125
+ var units = [
126
+ // Mass (Metric)
127
+ {
128
+ name: "g",
129
+ type: "mass",
130
+ system: "metric",
131
+ aliases: ["gram", "grams"],
132
+ toBase: 1
133
+ },
134
+ {
135
+ name: "kg",
136
+ type: "mass",
137
+ system: "metric",
138
+ aliases: ["kilogram", "kilograms"],
139
+ toBase: 1e3
140
+ },
141
+ // Mass (Imperial)
142
+ {
143
+ name: "oz",
144
+ type: "mass",
145
+ system: "imperial",
146
+ aliases: ["ounce", "ounces"],
147
+ toBase: 28.3495
148
+ },
149
+ {
150
+ name: "lb",
151
+ type: "mass",
152
+ system: "imperial",
153
+ aliases: ["pound", "pounds"],
154
+ toBase: 453.592
155
+ },
156
+ // Volume (Metric)
157
+ {
158
+ name: "ml",
159
+ type: "volume",
160
+ system: "metric",
161
+ aliases: ["milliliter", "milliliters", "millilitre", "millilitres"],
162
+ toBase: 1
163
+ },
164
+ {
165
+ name: "l",
166
+ type: "volume",
167
+ system: "metric",
168
+ aliases: ["liter", "liters", "litre", "litres"],
169
+ toBase: 1e3
170
+ },
171
+ {
172
+ name: "tsp",
173
+ type: "volume",
174
+ system: "metric",
175
+ aliases: ["teaspoon", "teaspoons"],
176
+ toBase: 5
177
+ },
178
+ {
179
+ name: "tbsp",
180
+ type: "volume",
181
+ system: "metric",
182
+ aliases: ["tablespoon", "tablespoons"],
183
+ toBase: 15
184
+ },
185
+ // Volume (Imperial)
186
+ {
187
+ name: "fl-oz",
188
+ type: "volume",
189
+ system: "imperial",
190
+ aliases: ["fluid ounce", "fluid ounces"],
191
+ toBase: 29.5735
192
+ },
193
+ {
194
+ name: "cup",
195
+ type: "volume",
196
+ system: "imperial",
197
+ aliases: ["cups"],
198
+ toBase: 236.588
199
+ },
200
+ {
201
+ name: "pint",
202
+ type: "volume",
203
+ system: "imperial",
204
+ aliases: ["pints"],
205
+ toBase: 473.176
206
+ },
207
+ {
208
+ name: "quart",
209
+ type: "volume",
210
+ system: "imperial",
211
+ aliases: ["quarts"],
212
+ toBase: 946.353
213
+ },
214
+ {
215
+ name: "gallon",
216
+ type: "volume",
217
+ system: "imperial",
218
+ aliases: ["gallons"],
219
+ toBase: 3785.41
220
+ },
221
+ // Count units (no conversion, but recognized as a type)
222
+ {
223
+ name: "piece",
224
+ type: "count",
225
+ system: "metric",
226
+ aliases: ["pieces"],
227
+ toBase: 1
228
+ }
229
+ ];
230
+ var unitMap = /* @__PURE__ */ new Map();
231
+ for (const unit of units) {
232
+ unitMap.set(unit.name.toLowerCase(), unit);
233
+ for (const alias of unit.aliases) {
234
+ unitMap.set(alias.toLowerCase(), unit);
235
+ }
236
+ }
237
+ function normalizeUnit(unit) {
238
+ return unitMap.get(unit.toLowerCase().trim());
239
+ }
240
+ function addQuantities(q1, q2) {
241
+ const unit1Def = normalizeUnit(q1.unit);
242
+ const unit2Def = normalizeUnit(q2.unit);
243
+ if (isNaN(Number(q1.value))) {
244
+ throw new Error(
245
+ `Cannot add quantity to string-quantified value: ${q1.value}`
246
+ );
247
+ }
248
+ if (isNaN(Number(q2.value))) {
249
+ throw new Error(
250
+ `Cannot add quantity to string-quantified value: ${q2.value}`
251
+ );
252
+ }
253
+ if (q1.unit === "" && unit2Def) {
254
+ return {
255
+ value: Math.round((q1.value + q2.value) * 100) / 100,
256
+ unit: q2.unit
257
+ };
258
+ }
259
+ if (q2.unit === "" && unit1Def) {
260
+ return {
261
+ value: Math.round((q1.value + q2.value) * 100) / 100,
262
+ unit: q1.unit
263
+ };
264
+ }
265
+ if (q1.unit.toLowerCase() === q2.unit.toLowerCase()) {
266
+ return {
267
+ value: Math.round((q1.value + q2.value) * 100) / 100,
268
+ unit: q1.unit
269
+ };
270
+ }
271
+ if (unit1Def && unit2Def) {
272
+ if (unit1Def.type !== unit2Def.type) {
273
+ throw new Error(
274
+ `Cannot add quantities of different types: ${unit1Def.type} (${q1.unit}) and ${unit2Def.type} (${q2.unit})`
275
+ );
276
+ }
277
+ const baseValue1 = q1.value * unit1Def.toBase;
278
+ const baseValue2 = q2.value * unit2Def.toBase;
279
+ const totalBaseValue = baseValue1 + baseValue2;
280
+ let targetUnitDef;
281
+ if (unit1Def.system !== unit2Def.system) {
282
+ const metricUnitDef = unit1Def.system === "metric" ? unit1Def : unit2Def;
283
+ targetUnitDef = units.filter((u) => u.type === metricUnitDef.type && u.system === "metric").reduce(
284
+ (prev, current) => prev.toBase > current.toBase ? prev : current
285
+ );
286
+ } else {
287
+ targetUnitDef = unit1Def.toBase >= unit2Def.toBase ? unit1Def : unit2Def;
288
+ }
289
+ const finalValue = totalBaseValue / targetUnitDef.toBase;
290
+ return {
291
+ value: Math.round(finalValue * 100) / 100,
292
+ unit: targetUnitDef.name
293
+ };
294
+ }
295
+ throw new Error(
296
+ `Cannot add quantities with incompatible or unknown units: ${q1.unit} and ${q2.unit}`
297
+ );
298
+ }
299
+
300
+ // src/parser_helpers.ts
301
+ function findOrPush(list, finder, creator) {
302
+ let index = list.findIndex(finder);
303
+ if (index === -1) {
304
+ index = list.push(creator()) - 1;
305
+ }
306
+ return index;
307
+ }
308
+ function flushPendingNote(section, note) {
309
+ if (note.length > 0) {
310
+ section.content.push({ note });
311
+ return "";
312
+ }
313
+ return note;
314
+ }
315
+ function flushPendingItems(section, items) {
316
+ if (items.length > 0) {
317
+ section.content.push({ items: [...items] });
318
+ items.length = 0;
319
+ return true;
320
+ }
321
+ return false;
322
+ }
323
+ function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
324
+ const { name, quantity, unit } = newIngredient;
325
+ if (isReference) {
326
+ const index = ingredients.findIndex(
327
+ (i) => i.name.toLowerCase() === name.toLowerCase()
328
+ );
329
+ if (index === -1) {
330
+ throw new Error(
331
+ `Referenced ingredient "${name}" not found. A referenced ingredient must be declared before being referenced with '&'.`
332
+ );
333
+ }
334
+ const existingIngredient = ingredients[index];
335
+ if (quantity !== void 0) {
336
+ const currentQuantity = {
337
+ value: existingIngredient.quantity ?? 0,
338
+ unit: existingIngredient.unit ?? ""
339
+ };
340
+ const newQuantity = { value: quantity, unit: unit ?? "" };
341
+ const total = addQuantities(currentQuantity, newQuantity);
342
+ existingIngredient.quantity = total.value;
343
+ existingIngredient.unit = total.unit || void 0;
344
+ }
345
+ return index;
346
+ }
347
+ return ingredients.push(newIngredient) - 1;
348
+ }
349
+ function findAndUpsertCookware(cookware, newCookware, isReference) {
350
+ const { name } = newCookware;
351
+ if (isReference) {
352
+ const index = cookware.findIndex(
353
+ (i) => i.name.toLowerCase() === name.toLowerCase()
354
+ );
355
+ if (index === -1) {
356
+ throw new Error(
357
+ `Referenced cookware "${name}" not found. A referenced cookware must be declared before being referenced with '&'.`
358
+ );
359
+ }
360
+ return index;
361
+ }
362
+ return cookware.push(newCookware) - 1;
363
+ }
364
+ function parseNumber(input_str) {
365
+ const clean_str = String(input_str).replace(",", ".");
366
+ if (!clean_str.startsWith("/") && clean_str.includes("/")) {
367
+ const [num, den] = clean_str.split("/").map(Number);
368
+ return num / den;
369
+ }
370
+ return Number(clean_str);
371
+ }
372
+ function parseSimpleMetaVar(content, varName) {
373
+ const varMatch = content.match(
374
+ new RegExp(`^${varName}:\\s*(.*(?:\\r?\\n\\s+.*)*)+`, "m")
375
+ );
376
+ return varMatch ? varMatch[1]?.trim().replace(/\s*\r?\n\s+/g, " ") : void 0;
377
+ }
378
+ function parseScalingMetaVar(content, varName) {
379
+ const varMatch = content.match(
380
+ new RegExp(`^${varName}:[\\t ]*(([^,\\n]*),? ?(?:.*)?)`, "m")
381
+ );
382
+ if (!varMatch) return void 0;
383
+ if (isNaN(Number(varMatch[2]?.trim()))) {
384
+ throw new Error("Scaling variables should be numbers");
385
+ }
386
+ return [Number(varMatch[2]?.trim()), varMatch[1]?.trim()];
387
+ }
388
+ function parseListMetaVar(content, varName) {
389
+ const listMatch = content.match(
390
+ new RegExp(
391
+ `^${varName}:\\s*(?:\\[([^\\]]*)\\]|((?:\\r?\\n\\s*-\\s*.+)+))`,
392
+ "m"
393
+ )
394
+ );
395
+ if (!listMatch) return void 0;
396
+ if (listMatch[1] !== void 0) {
397
+ return listMatch[1].split(",").map((tag) => tag.trim());
398
+ } else if (listMatch[2]) {
399
+ return listMatch[2].split("\n").filter((line) => line.trim() !== "").map((line) => line.replace(/^\s*-\s*/, "").trim());
400
+ }
401
+ }
402
+ function extractMetadata(content) {
403
+ const metadata = {};
404
+ let servings = void 0;
405
+ const metadataContent = content.match(metadataRegex)?.[1];
406
+ if (!metadataContent) {
407
+ return { metadata };
408
+ }
409
+ for (const metaVar of [
410
+ "title",
411
+ "source",
412
+ "source.name",
413
+ "source.url",
414
+ "author",
415
+ "source.author",
416
+ "prep time",
417
+ "time.prep",
418
+ "cook time",
419
+ "time.cook",
420
+ "time required",
421
+ "time",
422
+ "duration",
423
+ "locale",
424
+ "introduction",
425
+ "description",
426
+ "course",
427
+ "category",
428
+ "diet",
429
+ "cuisine",
430
+ "difficulty",
431
+ "image",
432
+ "picture"
433
+ ]) {
434
+ const stringMetaValue = parseSimpleMetaVar(metadataContent, metaVar);
435
+ if (stringMetaValue) metadata[metaVar] = stringMetaValue;
436
+ }
437
+ for (const metaVar of ["servings", "yield", "serves"]) {
438
+ const scalingMetaValue = parseScalingMetaVar(metadataContent, metaVar);
439
+ if (scalingMetaValue && scalingMetaValue[1]) {
440
+ metadata[metaVar] = scalingMetaValue[1];
441
+ servings = scalingMetaValue[0];
442
+ }
443
+ }
444
+ for (const metaVar of ["tags", "images", "pictures"]) {
445
+ const listMetaValue = parseListMetaVar(metadataContent, metaVar);
446
+ if (listMetaValue) metadata[metaVar] = listMetaValue;
447
+ }
448
+ return { metadata, servings };
449
+ }
450
+
451
+ // src/classes/recipe.ts
452
+ var Recipe = class _Recipe {
453
+ /**
454
+ * Creates a new Recipe instance.
455
+ * @param content - The recipe content to parse.
456
+ */
457
+ constructor(content) {
458
+ /**
459
+ * The recipe's metadata.
460
+ * @see {@link Metadata}
461
+ */
462
+ __publicField(this, "metadata", {});
463
+ /**
464
+ * The recipe's ingredients.
465
+ * @see {@link Ingredient}
466
+ */
467
+ __publicField(this, "ingredients", []);
468
+ /**
469
+ * The recipe's sections.
470
+ * @see {@link Section}
471
+ */
472
+ __publicField(this, "sections", []);
473
+ /**
474
+ * The recipe's cookware.
475
+ * @see {@link Cookware}
476
+ */
477
+ __publicField(this, "cookware", []);
478
+ /**
479
+ * The recipe's timers.
480
+ * @see {@link Timer}
481
+ */
482
+ __publicField(this, "timers", []);
483
+ /**
484
+ * The recipe's servings. Used for scaling
485
+ */
486
+ __publicField(this, "servings");
487
+ if (content) {
488
+ this.parse(content);
489
+ }
490
+ }
491
+ /**
492
+ * Parses a recipe from a string.
493
+ * @param content - The recipe content to parse.
494
+ */
495
+ parse(content) {
496
+ const cleanContent = content.replace(metadataRegex, "").replace(commentRegex, "").replace(blockCommentRegex, "").trim().split(/\r\n?|\n/);
497
+ const { metadata, servings } = extractMetadata(content);
498
+ this.metadata = metadata;
499
+ this.servings = servings;
500
+ let blankLineBefore = true;
501
+ let section = new Section();
502
+ const items = [];
503
+ let note = "";
504
+ let inNote = false;
505
+ for (const line of cleanContent) {
506
+ if (line.trim().length === 0) {
507
+ flushPendingItems(section, items);
508
+ note = flushPendingNote(section, note);
509
+ blankLineBefore = true;
510
+ inNote = false;
511
+ continue;
512
+ }
513
+ if (line.startsWith("=")) {
514
+ flushPendingItems(section, items);
515
+ note = flushPendingNote(section, note);
516
+ if (this.sections.length === 0 && section.isBlank()) {
517
+ section.name = line.substring(1).trim();
518
+ } else {
519
+ if (!section.isBlank()) {
520
+ this.sections.push(section);
521
+ }
522
+ section = new Section(line.substring(1).trim());
523
+ }
524
+ blankLineBefore = true;
525
+ inNote = false;
526
+ continue;
527
+ }
528
+ if (blankLineBefore && line.startsWith(">")) {
529
+ flushPendingItems(section, items);
530
+ note = flushPendingNote(section, note);
531
+ note += line.substring(1).trim();
532
+ inNote = true;
533
+ blankLineBefore = false;
534
+ continue;
535
+ }
536
+ if (inNote) {
537
+ if (line.startsWith(">")) {
538
+ note += " " + line.substring(1).trim();
539
+ } else {
540
+ note += " " + line.trim();
541
+ }
542
+ blankLineBefore = false;
543
+ continue;
544
+ }
545
+ note = flushPendingNote(section, note);
546
+ let cursor = 0;
547
+ for (const match of line.matchAll(tokensRegex)) {
548
+ const idx = match.index;
549
+ if (idx > cursor) {
550
+ items.push({ type: "text", value: line.slice(cursor, idx) });
551
+ }
552
+ const groups = match.groups;
553
+ if (groups.mIngredientName || groups.sIngredientName) {
554
+ const name = groups.mIngredientName || groups.sIngredientName;
555
+ const quantityRaw = groups.mIngredientQuantity || groups.sIngredientQuantity;
556
+ const units2 = groups.mIngredientUnits || groups.sIngredientUnits;
557
+ const preparation = groups.mIngredientPreparation || groups.sIngredientPreparation;
558
+ const modifier = groups.mIngredientModifier || groups.sIngredientModifier;
559
+ const optional = modifier === "?";
560
+ const hidden = modifier === "-";
561
+ const reference = modifier === "&";
562
+ const isRecipe = modifier === "@";
563
+ const quantity = quantityRaw ? parseNumber(quantityRaw) : void 0;
564
+ const idxInList = findAndUpsertIngredient(
565
+ this.ingredients,
566
+ {
567
+ name,
568
+ quantity,
569
+ unit: units2,
570
+ optional,
571
+ hidden,
572
+ preparation,
573
+ isRecipe
574
+ },
575
+ reference
576
+ );
577
+ items.push({ type: "ingredient", value: idxInList });
578
+ } else if (groups.mCookwareName || groups.sCookwareName) {
579
+ const name = groups.mCookwareName || groups.sCookwareName;
580
+ const modifier = groups.mCookwareModifier || groups.sCookwareModifier;
581
+ const optional = modifier === "?";
582
+ const hidden = modifier === "-";
583
+ const reference = modifier === "&";
584
+ const idxInList = findAndUpsertCookware(
585
+ this.cookware,
586
+ { name, optional, hidden },
587
+ reference
588
+ );
589
+ items.push({ type: "cookware", value: idxInList });
590
+ } else if (groups.timerQuantity !== void 0) {
591
+ const durationStr = groups.timerQuantity.trim();
592
+ const unit = (groups.timerUnits || "").trim();
593
+ if (!unit) {
594
+ throw new Error("Timer missing units");
595
+ }
596
+ const name = groups.timerName || void 0;
597
+ const duration = parseNumber(durationStr);
598
+ const timerObj = {
599
+ name,
600
+ duration,
601
+ unit
602
+ };
603
+ const idxInList = findOrPush(
604
+ this.timers,
605
+ (t) => t.name === timerObj.name && t.duration === timerObj.duration && t.unit === timerObj.unit,
606
+ () => timerObj
607
+ );
608
+ items.push({ type: "timer", value: idxInList });
609
+ }
610
+ cursor = idx + match[0].length;
611
+ }
612
+ if (cursor < line.length) {
613
+ items.push({ type: "text", value: line.slice(cursor) });
614
+ }
615
+ blankLineBefore = false;
616
+ }
617
+ flushPendingItems(section, items);
618
+ note = flushPendingNote(section, note);
619
+ if (!section.isBlank()) {
620
+ this.sections.push(section);
621
+ }
622
+ }
623
+ /**
624
+ * Scales the recipe to a new number of servings.
625
+ * @param newServings - The new number of servings.
626
+ * @returns A new Recipe instance with the scaled ingredients.
627
+ */
628
+ scaleTo(newServings) {
629
+ const originalServings = this.getServings();
630
+ if (originalServings === void 0 || originalServings === 0) {
631
+ throw new Error("Error scaling recipe: no initial servings value set");
632
+ }
633
+ const factor = newServings / originalServings;
634
+ return this.scaleBy(factor);
635
+ }
636
+ /**
637
+ * Scales the recipe by a factor.
638
+ * @param factor - The factor to scale the recipe by.
639
+ * @returns A new Recipe instance with the scaled ingredients.
640
+ */
641
+ scaleBy(factor) {
642
+ const newRecipe = this.clone();
643
+ const originalServings = newRecipe.getServings();
644
+ if (originalServings === void 0 || originalServings === 0) {
645
+ throw new Error("Error scaling recipe: no initial servings value set");
646
+ }
647
+ newRecipe.ingredients = newRecipe.ingredients.map((ingredient) => {
648
+ if (ingredient.quantity && !isNaN(Number(ingredient.quantity))) {
649
+ ingredient.quantity *= factor;
650
+ }
651
+ return ingredient;
652
+ }).filter((ingredient) => ingredient.quantity !== null);
653
+ newRecipe.servings = originalServings * factor;
654
+ if (newRecipe.metadata.servings && this.metadata.servings) {
655
+ const servingsValue = parseFloat(this.metadata.servings);
656
+ if (!isNaN(servingsValue)) {
657
+ newRecipe.metadata.servings = String(servingsValue * factor);
658
+ }
659
+ }
660
+ if (newRecipe.metadata.yield && this.metadata.yield) {
661
+ const yieldValue = parseFloat(this.metadata.yield);
662
+ if (!isNaN(yieldValue)) {
663
+ newRecipe.metadata.yield = String(yieldValue * factor);
664
+ }
665
+ }
666
+ if (newRecipe.metadata.serves && this.metadata.serves) {
667
+ const servesValue = parseFloat(this.metadata.serves);
668
+ if (!isNaN(servesValue)) {
669
+ newRecipe.metadata.serves = String(servesValue * factor);
670
+ }
671
+ }
672
+ return newRecipe;
673
+ }
674
+ getServings() {
675
+ if (this.servings) {
676
+ return this.servings;
677
+ }
678
+ return void 0;
679
+ }
680
+ /**
681
+ * Clones the recipe.
682
+ * @returns A new Recipe instance with the same properties.
683
+ */
684
+ clone() {
685
+ const newRecipe = new _Recipe();
686
+ newRecipe.metadata = JSON.parse(JSON.stringify(this.metadata));
687
+ newRecipe.ingredients = JSON.parse(JSON.stringify(this.ingredients));
688
+ newRecipe.sections = JSON.parse(JSON.stringify(this.sections));
689
+ newRecipe.cookware = JSON.parse(JSON.stringify(this.cookware));
690
+ newRecipe.timers = JSON.parse(JSON.stringify(this.timers));
691
+ newRecipe.servings = this.servings;
692
+ return newRecipe;
693
+ }
694
+ };
695
+
696
+ // src/classes/shopping_list.ts
697
+ var ShoppingList = class {
698
+ /**
699
+ * Creates a new ShoppingList instance.
700
+ * @param aisle_config_str - The aisle configuration to parse.
701
+ */
702
+ constructor(aisle_config_str) {
703
+ /**
704
+ * The ingredients in the shopping list.
705
+ * @see {@link Ingredient}
706
+ */
707
+ __publicField(this, "ingredients", []);
708
+ /**
709
+ * The recipes in the shopping list.
710
+ * @see {@link AddedRecipe}
711
+ */
712
+ __publicField(this, "recipes", []);
713
+ /**
714
+ * The aisle configuration for the shopping list.
715
+ * @see {@link AisleConfig}
716
+ */
717
+ __publicField(this, "aisle_config");
718
+ /**
719
+ * The categorized ingredients in the shopping list.
720
+ * @see {@link CategorizedIngredients}
721
+ */
722
+ __publicField(this, "categories");
723
+ if (aisle_config_str) {
724
+ this.set_aisle_config(aisle_config_str);
725
+ }
726
+ }
727
+ calculate_ingredients() {
728
+ this.ingredients = [];
729
+ for (const { recipe, factor } of this.recipes) {
730
+ const scaledRecipe = factor === 1 ? recipe : recipe.scaleBy(factor);
731
+ for (const ingredient of scaledRecipe.ingredients) {
732
+ if (ingredient.hidden) {
733
+ continue;
734
+ }
735
+ const existingIngredient = this.ingredients.find(
736
+ (i) => i.name === ingredient.name
737
+ );
738
+ let addSeparate = false;
739
+ try {
740
+ if (existingIngredient) {
741
+ if (existingIngredient.quantity && ingredient.quantity) {
742
+ const newQuantity = addQuantities(
743
+ {
744
+ value: existingIngredient.quantity,
745
+ unit: existingIngredient.unit ?? ""
746
+ },
747
+ {
748
+ value: ingredient.quantity,
749
+ unit: ingredient.unit ?? ""
750
+ }
751
+ );
752
+ existingIngredient.quantity = newQuantity.value;
753
+ if (newQuantity.unit) {
754
+ existingIngredient.unit = newQuantity.unit;
755
+ }
756
+ } else if (ingredient.quantity) {
757
+ existingIngredient.quantity = ingredient.quantity;
758
+ if (ingredient.unit) {
759
+ existingIngredient.unit = ingredient.unit;
760
+ }
761
+ }
762
+ }
763
+ } catch {
764
+ addSeparate = true;
765
+ }
766
+ if (!existingIngredient || addSeparate) {
767
+ const newIngredient = { name: ingredient.name };
768
+ if (ingredient.quantity) {
769
+ newIngredient.quantity = ingredient.quantity;
770
+ }
771
+ if (ingredient.unit) {
772
+ newIngredient.unit = ingredient.unit;
773
+ }
774
+ this.ingredients.push(newIngredient);
775
+ }
776
+ }
777
+ }
778
+ }
779
+ /**
780
+ * Adds a recipe to the shopping list.
781
+ * @param recipe - The recipe to add.
782
+ * @param factor - The factor to scale the recipe by.
783
+ */
784
+ add_recipe(recipe, factor = 1) {
785
+ this.recipes.push({ recipe, factor });
786
+ this.calculate_ingredients();
787
+ this.categorize();
788
+ }
789
+ /**
790
+ * Removes a recipe from the shopping list.
791
+ * @param index - The index of the recipe to remove.
792
+ */
793
+ remove_recipe(index) {
794
+ if (index < 0 || index >= this.recipes.length) {
795
+ throw new Error("Index out of bounds");
796
+ }
797
+ this.recipes.splice(index, 1);
798
+ this.calculate_ingredients();
799
+ this.categorize();
800
+ }
801
+ /**
802
+ * Sets the aisle configuration for the shopping list.
803
+ * @param config - The aisle configuration to parse.
804
+ */
805
+ set_aisle_config(config) {
806
+ this.aisle_config = new AisleConfig(config);
807
+ this.categorize();
808
+ }
809
+ /**
810
+ * Categorizes the ingredients in the shopping list
811
+ * Will use the aisle config if any, otherwise all ingredients will be placed in the "other" category
812
+ */
813
+ categorize() {
814
+ if (!this.aisle_config) {
815
+ this.categories = { other: this.ingredients };
816
+ return;
817
+ }
818
+ const categories = { other: [] };
819
+ for (const category of this.aisle_config.categories) {
820
+ categories[category.name] = [];
821
+ }
822
+ for (const ingredient of this.ingredients) {
823
+ let found = false;
824
+ for (const category of this.aisle_config.categories) {
825
+ for (const aisleIngredient of category.ingredients) {
826
+ if (aisleIngredient.aliases.includes(ingredient.name)) {
827
+ categories[category.name].push(ingredient);
828
+ found = true;
829
+ break;
830
+ }
831
+ }
832
+ if (found) {
833
+ break;
834
+ }
835
+ }
836
+ if (!found) {
837
+ categories.other.push(ingredient);
838
+ }
839
+ }
840
+ this.categories = categories;
841
+ }
842
+ };
843
+ // Annotate the CommonJS export names for ESM import in node:
844
+ 0 && (module.exports = {
845
+ AisleConfig,
846
+ Recipe,
847
+ ShoppingList
848
+ });
849
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/classes/aisle_config.ts","../src/classes/section.ts","../src/regex.ts","../src/units.ts","../src/parser_helpers.ts","../src/classes/recipe.ts","../src/classes/shopping_list.ts"],"sourcesContent":["import { AisleConfig } from \"./classes/aisle_config\";\nimport { Recipe } from \"./classes/recipe\";\nimport { ShoppingList } from \"./classes/shopping_list\";\nimport type {\n Metadata,\n MetadataExtract,\n Ingredient,\n Timer,\n TextItem,\n IngredientItem,\n CookwareItem,\n TimerItem,\n Item,\n Step,\n Note,\n Cookware,\n CategorizedIngredients,\n AddedRecipe,\n AisleIngredient,\n AisleCategory,\n} from \"./types\";\n\nexport {\n Recipe,\n ShoppingList,\n AisleConfig,\n Metadata,\n MetadataExtract,\n Ingredient,\n Timer,\n TextItem,\n IngredientItem,\n CookwareItem,\n TimerItem,\n Item,\n Step,\n Note,\n Cookware,\n CategorizedIngredients,\n AddedRecipe,\n AisleIngredient,\n AisleCategory,\n};\n","import type { AisleCategory, AisleIngredient } from \"../types\";\n\n/**\n * Represents the aisle configuration for a shopping list.\n * @category Classes\n */\nexport class AisleConfig {\n /**\n * The categories of aisles.\n * @see {@link AisleCategory}\n */\n categories: AisleCategory[] = [];\n\n /**\n * Creates a new AisleConfig instance.\n * @param config - The aisle configuration to parse.\n */\n constructor(config?: string) {\n if (config) {\n this.parse(config);\n }\n }\n\n /**\n * Parses an aisle configuration from a string.\n * @param config - The aisle configuration to parse.\n */\n parse(config: string) {\n let currentCategory: AisleCategory | null = null;\n const categoryNames = new Set<string>();\n const ingredientNames = new Set<string>();\n\n for (const line of config.split(\"\\n\")) {\n const trimmedLine = line.trim();\n\n if (trimmedLine.length === 0) {\n continue;\n }\n\n if (trimmedLine.startsWith(\"[\") && trimmedLine.endsWith(\"]\")) {\n const categoryName = trimmedLine\n .substring(1, trimmedLine.length - 1)\n .trim();\n\n if (categoryNames.has(categoryName)) {\n throw new Error(`Duplicate category found: ${categoryName}`);\n }\n categoryNames.add(categoryName);\n\n currentCategory = { name: categoryName, ingredients: [] };\n this.categories.push(currentCategory);\n } else {\n if (currentCategory === null) {\n throw new Error(\n `Ingredient found without a category: ${trimmedLine}`,\n );\n }\n\n const aliases = trimmedLine.split(\"|\").map((s) => s.trim());\n for (const alias of aliases) {\n if (ingredientNames.has(alias)) {\n throw new Error(`Duplicate ingredient/alias found: ${alias}`);\n }\n ingredientNames.add(alias);\n }\n\n const ingredient: AisleIngredient = {\n name: aliases[0]!, // We know this exists because trimmedLine is not empty\n aliases: aliases,\n };\n currentCategory.ingredients.push(ingredient);\n }\n }\n }\n}\n","import type { Step, Note } from \"../types\";\n\nexport class Section {\n name: string;\n content: (Step | Note)[] = [];\n\n constructor(name: string = \"\") {\n this.name = name;\n }\n\n isBlank(): boolean {\n return this.name === \"\" && this.content.length === 0;\n }\n}\n","export const metadataRegex = /---\\n(.*?)\\n---/s;\n\nconst multiwordIngredient =\n /@(?<mIngredientModifier>[@\\-&+*!?])?(?<mIngredientName>(?:[^\\s@#~\\[\\]{(.,;:!?]+(?:\\s+[^\\s@#~\\[\\]{(.,;:!?]+)+))(?=\\s*(?:\\{|\\}|\\(\\s*[^)]*\\s*\\)))(?:\\{(?<mIngredientQuantity>\\p{No}|(?:\\p{Nd}+(?:[.,\\/][\\p{Nd}]+)?))?(?:%(?<mIngredientUnits>[^}]+?))?\\})?(?:\\((?<mIngredientPreparation>[^)]*?)\\))?/gu;\nconst singleWordIngredient =\n /@(?<sIngredientModifier>[@\\-&+*!?])?(?<sIngredientName>[^\\s@#~\\[\\]{(.,;:!?]+)(?:\\{(?<sIngredientQuantity>\\p{No}|(?:\\p{Nd}+(?:[.,\\/][\\p{Nd}]+)?))(?:%(?<sIngredientUnits>[^}]+?))?\\})?(?:\\((?<sIngredientPreparation>[^)]*?)\\))?/gu;\n\nconst multiwordCookware =\n /#(?<mCookwareModifier>[\\-&+*!?])?(?<mCookwareName>[^#~[]+?)\\{(?<mCookwareQuantity>.*?)\\}/;\nconst singleWordCookware =\n /#(?<sCookwareModifier>[\\-&+*!?])?(?<sCookwareName>[^\\s\\t\\s\\p{P}]+)/u;\n\nconst timer =\n /~(?<timerName>.*?)(?:\\{(?<timerQuantity>.*?)(?:%(?<timerUnits>.+?))?\\})/;\n\nexport const tokensRegex = new RegExp(\n [\n multiwordIngredient,\n singleWordIngredient,\n multiwordCookware,\n singleWordCookware,\n timer,\n ]\n .map((r) => r.source)\n .join(\"|\"),\n \"gu\",\n);\n\nexport const commentRegex = /--.*/g;\nexport const blockCommentRegex = /\\s*\\[\\-.*?\\-\\]\\s*/g;\n\nexport const shoppingListRegex =\n /\\n\\s*\\[(?<name>.+)\\]\\n(?<items>[^]*?)(?:\\n\\n|$)/g;\n","export type UnitType = \"mass\" | \"volume\" | \"count\";\nexport type UnitSystem = \"metric\" | \"imperial\";\n\nexport interface UnitDefinition {\n name: string; // canonical name, e.g. 'g'\n type: UnitType;\n system: UnitSystem;\n aliases: string[]; // e.g. ['gram', 'grams']\n toBase: number; // conversion factor to the base unit of its type\n}\n\nexport interface Quantity {\n value: number | string;\n unit: string;\n}\n\n// Base units: mass -> gram (g), volume -> milliliter (ml)\nconst units: UnitDefinition[] = [\n // Mass (Metric)\n {\n name: \"g\",\n type: \"mass\",\n system: \"metric\",\n aliases: [\"gram\", \"grams\"],\n toBase: 1,\n },\n {\n name: \"kg\",\n type: \"mass\",\n system: \"metric\",\n aliases: [\"kilogram\", \"kilograms\"],\n toBase: 1000,\n },\n // Mass (Imperial)\n {\n name: \"oz\",\n type: \"mass\",\n system: \"imperial\",\n aliases: [\"ounce\", \"ounces\"],\n toBase: 28.3495,\n },\n {\n name: \"lb\",\n type: \"mass\",\n system: \"imperial\",\n aliases: [\"pound\", \"pounds\"],\n toBase: 453.592,\n },\n\n // Volume (Metric)\n {\n name: \"ml\",\n type: \"volume\",\n system: \"metric\",\n aliases: [\"milliliter\", \"milliliters\", \"millilitre\", \"millilitres\"],\n toBase: 1,\n },\n {\n name: \"l\",\n type: \"volume\",\n system: \"metric\",\n aliases: [\"liter\", \"liters\", \"litre\", \"litres\"],\n toBase: 1000,\n },\n {\n name: \"tsp\",\n type: \"volume\",\n system: \"metric\",\n aliases: [\"teaspoon\", \"teaspoons\"],\n toBase: 5,\n },\n {\n name: \"tbsp\",\n type: \"volume\",\n system: \"metric\",\n aliases: [\"tablespoon\", \"tablespoons\"],\n toBase: 15,\n },\n\n // Volume (Imperial)\n {\n name: \"fl-oz\",\n type: \"volume\",\n system: \"imperial\",\n aliases: [\"fluid ounce\", \"fluid ounces\"],\n toBase: 29.5735,\n },\n {\n name: \"cup\",\n type: \"volume\",\n system: \"imperial\",\n aliases: [\"cups\"],\n toBase: 236.588,\n },\n {\n name: \"pint\",\n type: \"volume\",\n system: \"imperial\",\n aliases: [\"pints\"],\n toBase: 473.176,\n },\n {\n name: \"quart\",\n type: \"volume\",\n system: \"imperial\",\n aliases: [\"quarts\"],\n toBase: 946.353,\n },\n {\n name: \"gallon\",\n type: \"volume\",\n system: \"imperial\",\n aliases: [\"gallons\"],\n toBase: 3785.41,\n },\n\n // Count units (no conversion, but recognized as a type)\n {\n name: \"piece\",\n type: \"count\",\n system: \"metric\",\n aliases: [\"pieces\"],\n toBase: 1,\n },\n];\n\nconst unitMap = new Map<string, UnitDefinition>();\nfor (const unit of units) {\n unitMap.set(unit.name.toLowerCase(), unit);\n for (const alias of unit.aliases) {\n unitMap.set(alias.toLowerCase(), unit);\n }\n}\n\nexport function normalizeUnit(unit: string): UnitDefinition | undefined {\n return unitMap.get(unit.toLowerCase().trim());\n}\n\n/**\n * Adds two quantities, returning the result in the most appropriate unit.\n */\nexport function addQuantities(q1: Quantity, q2: Quantity): Quantity {\n const unit1Def = normalizeUnit(q1.unit);\n const unit2Def = normalizeUnit(q2.unit);\n\n if (isNaN(Number(q1.value))) {\n throw new Error(\n `Cannot add quantity to string-quantified value: ${q1.value}`,\n );\n }\n if (isNaN(Number(q2.value))) {\n throw new Error(\n `Cannot add quantity to string-quantified value: ${q2.value}`,\n );\n }\n\n // If one unit is empty, assume it's of the same type as the other\n if (q1.unit === \"\" && unit2Def) {\n return {\n value:\n Math.round(((q1.value as number) + (q2.value as number)) * 100) / 100,\n unit: q2.unit,\n };\n }\n if (q2.unit === \"\" && unit1Def) {\n return {\n value:\n Math.round(((q1.value as number) + (q2.value as number)) * 100) / 100,\n unit: q1.unit,\n };\n }\n\n // If both units are the same (even if unknown, e.g. \"cloves\")\n if (q1.unit.toLowerCase() === q2.unit.toLowerCase()) {\n return {\n value:\n Math.round(((q1.value as number) + (q2.value as number)) * 100) / 100,\n unit: q1.unit,\n };\n }\n\n // If both are known and compatible\n if (unit1Def && unit2Def) {\n if (unit1Def.type !== unit2Def.type) {\n throw new Error(\n `Cannot add quantities of different types: ${unit1Def.type} (${q1.unit}) and ${unit2Def.type} (${q2.unit})`,\n );\n }\n\n // Convert both to base unit value\n const baseValue1 = (q1.value as number) * unit1Def.toBase;\n const baseValue2 = (q2.value as number) * unit2Def.toBase;\n const totalBaseValue = baseValue1 + baseValue2;\n\n let targetUnitDef: UnitDefinition;\n\n // Rule: If systems differ, convert to the largest metric unit.\n if (unit1Def.system !== unit2Def.system) {\n const metricUnitDef = unit1Def.system === \"metric\" ? unit1Def : unit2Def;\n targetUnitDef = units\n .filter((u) => u.type === metricUnitDef.type && u.system === \"metric\")\n .reduce((prev, current) =>\n prev.toBase > current.toBase ? prev : current,\n );\n } else {\n // Rule: Same system, use the biggest of the two input units.\n targetUnitDef = unit1Def.toBase >= unit2Def.toBase ? unit1Def : unit2Def;\n }\n\n const finalValue = totalBaseValue / targetUnitDef.toBase;\n\n return {\n value: Math.round(finalValue * 100) / 100,\n unit: targetUnitDef.name,\n };\n }\n\n // Otherwise, units are different and at least one is unknown.\n throw new Error(\n `Cannot add quantities with incompatible or unknown units: ${q1.unit} and ${q2.unit}`,\n );\n}\n","import type { MetadataExtract, Metadata } from \"./types\";\nimport { metadataRegex } from \"./regex\";\nimport { Section as SectionObject } from \"./classes/section\";\nimport type { Ingredient, Note, Step, Cookware } from \"./types\";\nimport { addQuantities } from \"./units\";\n\n/**\n * Finds an item in a list or adds it if not present, then returns its index.\n * @param list The list to search in.\n * @param finder A predicate to find the item.\n * @param creator A function to create the item if not found.\n * @returns The index of the item in the list.\n */\nexport function findOrPush<T>(\n list: T[],\n finder: (elem: T) => boolean,\n creator: () => T,\n): number {\n let index = list.findIndex(finder);\n if (index === -1) {\n index = list.push(creator()) - 1;\n }\n return index;\n}\n\n/**\n * Pushes a pending note to the section content if it's not empty.\n * @param section The current section object.\n * @param note The note content.\n * @returns An empty string if the note was pushed, otherwise the original note.\n */\nexport function flushPendingNote(\n section: SectionObject,\n note: Note[\"note\"],\n): Note[\"note\"] {\n if (note.length > 0) {\n section.content.push({ note });\n return \"\";\n }\n return note;\n}\n\n/**\n * Pushes pending step items and a pending note to the section content.\n * @param section The current section object.\n * @param items The list of step items. This array will be cleared.\n * @returns true if the items were pushed, otherwise false.\n */\nexport function flushPendingItems(\n section: SectionObject,\n items: Step[\"items\"],\n): boolean {\n if (items.length > 0) {\n section.content.push({ items: [...items] });\n items.length = 0;\n return true;\n }\n return false;\n}\n\n/**\n * Finds an ingredient in the list (case-insensitively) and updates it, or adds it if not present.\n * This function mutates the `ingredients` array.\n * @param ingredients The list of ingredients.\n * @param newIngredient The ingredient to find or add.\n * @param isReference Whether this is a reference ingredient (`&` modifier).\n * @returns The index of the ingredient in the list.\n */\nexport function findAndUpsertIngredient(\n ingredients: Ingredient[],\n newIngredient: Ingredient,\n isReference: boolean,\n): number {\n const { name, quantity, unit } = newIngredient;\n\n // New ingredient\n if (isReference) {\n const index = ingredients.findIndex(\n (i) => i.name.toLowerCase() === name.toLowerCase(),\n );\n\n if (index === -1) {\n throw new Error(\n `Referenced ingredient \"${name}\" not found. A referenced ingredient must be declared before being referenced with '&'.`,\n );\n }\n\n // Ingredient already exists, update it\n const existingIngredient = ingredients[index]!;\n if (quantity !== undefined) {\n const currentQuantity = {\n value: existingIngredient.quantity ?? 0,\n unit: existingIngredient.unit ?? \"\",\n };\n const newQuantity = { value: quantity, unit: unit ?? \"\" };\n\n const total = addQuantities(currentQuantity, newQuantity);\n existingIngredient.quantity = total.value;\n existingIngredient.unit = total.unit || undefined;\n }\n return index;\n }\n\n // Not a reference, so add as a new ingredient.\n return ingredients.push(newIngredient) - 1;\n}\n\nexport function findAndUpsertCookware(\n cookware: Cookware[],\n newCookware: Cookware,\n isReference: boolean,\n): number {\n const { name } = newCookware;\n\n if (isReference) {\n const index = cookware.findIndex(\n (i) => i.name.toLowerCase() === name.toLowerCase(),\n );\n\n if (index === -1) {\n throw new Error(\n `Referenced cookware \"${name}\" not found. A referenced cookware must be declared before being referenced with '&'.`,\n );\n }\n\n return index;\n }\n\n return cookware.push(newCookware) - 1;\n}\n\nexport function parseNumber(input_str: string): number {\n const clean_str = String(input_str).replace(\",\", \".\");\n if (!clean_str.startsWith(\"/\") && clean_str.includes(\"/\")) {\n const [num, den] = clean_str.split(\"/\").map(Number);\n return num! / den!;\n }\n return Number(clean_str);\n}\n\nexport function parseSimpleMetaVar(content: string, varName: string) {\n const varMatch = content.match(\n new RegExp(`^${varName}:\\\\s*(.*(?:\\\\r?\\\\n\\\\s+.*)*)+`, \"m\"),\n );\n return varMatch\n ? varMatch[1]?.trim().replace(/\\s*\\r?\\n\\s+/g, \" \")\n : undefined;\n}\n\nexport function parseScalingMetaVar(content: string, varName: string) {\n const varMatch = content.match(\n new RegExp(`^${varName}:[\\\\t ]*(([^,\\\\n]*),? ?(?:.*)?)`, \"m\"),\n );\n if (!varMatch) return undefined;\n if (isNaN(Number(varMatch[2]?.trim()))) {\n throw new Error(\"Scaling variables should be numbers\");\n }\n return [Number(varMatch[2]?.trim()), varMatch[1]?.trim()];\n}\n\nexport function parseListMetaVar(content: string, varName: string) {\n // Handle both inline and YAML-style tags\n const listMatch = content.match(\n new RegExp(\n `^${varName}:\\\\s*(?:\\\\[([^\\\\]]*)\\\\]|((?:\\\\r?\\\\n\\\\s*-\\\\s*.+)+))`,\n \"m\",\n ),\n );\n if (!listMatch) return undefined;\n\n if (listMatch[1] !== undefined) {\n // Inline list: tags: [one, two, three]\n return listMatch[1].split(\",\").map((tag) => tag.trim());\n } else if (listMatch[2]) {\n // YAML list:\n // tags:\n // - one\n // - two\n return listMatch[2]\n .split(\"\\n\")\n .filter((line) => line.trim() !== \"\")\n .map((line) => line.replace(/^\\s*-\\s*/, \"\").trim());\n }\n}\n\nexport function extractMetadata(content: string): MetadataExtract {\n const metadata: Metadata = {};\n let servings: number | undefined = undefined;\n\n // Is there front-matter at all?\n const metadataContent = content.match(metadataRegex)?.[1];\n if (!metadataContent) {\n return { metadata };\n }\n\n // String metadata variables\n for (const metaVar of [\n \"title\",\n \"source\",\n \"source.name\",\n \"source.url\",\n \"author\",\n \"source.author\",\n \"prep time\",\n \"time.prep\",\n \"cook time\",\n \"time.cook\",\n \"time required\",\n \"time\",\n \"duration\",\n \"locale\",\n \"introduction\",\n \"description\",\n \"course\",\n \"category\",\n \"diet\",\n \"cuisine\",\n \"difficulty\",\n \"image\",\n \"picture\",\n ] as (keyof Metadata)[]) {\n const stringMetaValue: any = parseSimpleMetaVar(metadataContent, metaVar);\n if (stringMetaValue) metadata[metaVar] = stringMetaValue;\n }\n\n // String metadata variables\n for (const metaVar of [\"servings\", \"yield\", \"serves\"] as (keyof Metadata)[]) {\n const scalingMetaValue: any = parseScalingMetaVar(metadataContent, metaVar);\n if (scalingMetaValue && scalingMetaValue[1]) {\n metadata[metaVar] = scalingMetaValue[1];\n servings = scalingMetaValue[0];\n }\n }\n\n // List metadata variables\n for (const metaVar of [\"tags\", \"images\", \"pictures\"] as (keyof Metadata)[]) {\n const listMetaValue: any = parseListMetaVar(metadataContent, metaVar);\n if (listMetaValue) metadata[metaVar] = listMetaValue;\n }\n\n return { metadata, servings };\n}\n","import type {\n Metadata,\n Ingredient,\n Timer,\n Step,\n Note,\n Cookware,\n MetadataExtract,\n} from \"../types\";\nimport { Section } from \"./section\";\nimport {\n tokensRegex,\n commentRegex,\n blockCommentRegex,\n metadataRegex,\n} from \"../regex\";\nimport {\n findOrPush,\n flushPendingItems,\n flushPendingNote,\n findAndUpsertIngredient,\n findAndUpsertCookware,\n parseNumber,\n extractMetadata,\n} from \"../parser_helpers\";\n\n/**\n * Represents a recipe.\n * @category Classes\n */\nexport class Recipe {\n /**\n * The recipe's metadata.\n * @see {@link Metadata}\n */\n metadata: Metadata = {};\n /**\n * The recipe's ingredients.\n * @see {@link Ingredient}\n */\n ingredients: Ingredient[] = [];\n /**\n * The recipe's sections.\n * @see {@link Section}\n */\n sections: Section[] = [];\n /**\n * The recipe's cookware.\n * @see {@link Cookware}\n */\n cookware: Cookware[] = [];\n /**\n * The recipe's timers.\n * @see {@link Timer}\n */\n timers: Timer[] = [];\n /**\n * The recipe's servings. Used for scaling\n */\n servings?: number;\n\n /**\n * Creates a new Recipe instance.\n * @param content - The recipe content to parse.\n */\n constructor(content?: string) {\n if (content) {\n this.parse(content);\n }\n }\n\n /**\n * Parses a recipe from a string.\n * @param content - The recipe content to parse.\n */\n parse(content: string) {\n const cleanContent = content\n .replace(metadataRegex, \"\")\n .replace(commentRegex, \"\")\n .replace(blockCommentRegex, \"\")\n .trim()\n .split(/\\r\\n?|\\n/);\n\n const { metadata, servings }: MetadataExtract = extractMetadata(content);\n this.metadata = metadata;\n this.servings = servings;\n\n let blankLineBefore = true;\n let section: Section = new Section();\n const items: Step[\"items\"] = [];\n let note: Note[\"note\"] = \"\";\n let inNote = false;\n\n for (const line of cleanContent) {\n if (line.trim().length === 0) {\n flushPendingItems(section, items);\n note = flushPendingNote(section, note);\n blankLineBefore = true;\n inNote = false;\n continue;\n }\n\n if (line.startsWith(\"=\")) {\n flushPendingItems(section, items);\n note = flushPendingNote(section, note);\n\n if (this.sections.length === 0 && section.isBlank()) {\n section.name = line.substring(1).trim();\n } else {\n if (!section.isBlank()) {\n this.sections.push(section);\n }\n section = new Section(line.substring(1).trim());\n }\n blankLineBefore = true;\n inNote = false;\n continue;\n }\n\n if (blankLineBefore && line.startsWith(\">\")) {\n flushPendingItems(section, items);\n note = flushPendingNote(section, note);\n note += line.substring(1).trim();\n inNote = true;\n blankLineBefore = false;\n continue;\n }\n\n if (inNote) {\n if (line.startsWith(\">\")) {\n note += \" \" + line.substring(1).trim();\n } else {\n note += \" \" + line.trim();\n }\n blankLineBefore = false;\n continue;\n }\n\n note = flushPendingNote(section, note);\n\n let cursor = 0;\n for (const match of line.matchAll(tokensRegex)) {\n const idx = match.index;\n if (idx > cursor) {\n items.push({ type: \"text\", value: line.slice(cursor, idx) });\n }\n\n const groups = match.groups!;\n\n if (groups.mIngredientName || groups.sIngredientName) {\n const name = (groups.mIngredientName || groups.sIngredientName)!;\n const quantityRaw =\n groups.mIngredientQuantity || groups.sIngredientQuantity;\n const units = groups.mIngredientUnits || groups.sIngredientUnits;\n const preparation =\n groups.mIngredientPreparation || groups.sIngredientPreparation;\n const modifier =\n groups.mIngredientModifier || groups.sIngredientModifier;\n const optional = modifier === \"?\";\n const hidden = modifier === \"-\";\n const reference = modifier === \"&\";\n const isRecipe = modifier === \"@\";\n const quantity = quantityRaw ? parseNumber(quantityRaw) : undefined;\n\n const idxInList = findAndUpsertIngredient(\n this.ingredients,\n {\n name,\n quantity,\n unit: units,\n optional,\n hidden,\n preparation,\n isRecipe,\n },\n reference,\n );\n\n items.push({ type: \"ingredient\", value: idxInList });\n } else if (groups.mCookwareName || groups.sCookwareName) {\n const name = (groups.mCookwareName || groups.sCookwareName)!;\n const modifier = groups.mCookwareModifier || groups.sCookwareModifier;\n const optional = modifier === \"?\";\n const hidden = modifier === \"-\";\n const reference = modifier === \"&\";\n\n const idxInList = findAndUpsertCookware(\n this.cookware,\n { name, optional, hidden },\n reference,\n );\n items.push({ type: \"cookware\", value: idxInList });\n } else if (groups.timerQuantity !== undefined) {\n const durationStr = groups.timerQuantity.trim();\n const unit = (groups.timerUnits || \"\").trim();\n if (!unit) {\n throw new Error(\"Timer missing units\");\n }\n const name = groups.timerName || undefined;\n const duration = parseNumber(durationStr);\n const timerObj: Timer = {\n name,\n duration,\n unit,\n };\n const idxInList = findOrPush(\n this.timers,\n (t) =>\n t.name === timerObj.name &&\n t.duration === timerObj.duration &&\n t.unit === timerObj.unit,\n () => timerObj,\n );\n items.push({ type: \"timer\", value: idxInList });\n }\n\n cursor = idx + match[0].length;\n }\n\n if (cursor < line.length) {\n items.push({ type: \"text\", value: line.slice(cursor) });\n }\n\n blankLineBefore = false;\n }\n\n // End of content reached: pushing all temporarily saved elements\n flushPendingItems(section, items);\n note = flushPendingNote(section, note);\n if (!section.isBlank()) {\n this.sections.push(section);\n }\n }\n\n /**\n * Scales the recipe to a new number of servings.\n * @param newServings - The new number of servings.\n * @returns A new Recipe instance with the scaled ingredients.\n */\n scaleTo(newServings: number): Recipe {\n const originalServings = this.getServings();\n\n if (originalServings === undefined || originalServings === 0) {\n throw new Error(\"Error scaling recipe: no initial servings value set\");\n }\n\n const factor = newServings / originalServings;\n return this.scaleBy(factor);\n }\n\n /**\n * Scales the recipe by a factor.\n * @param factor - The factor to scale the recipe by.\n * @returns A new Recipe instance with the scaled ingredients.\n */\n scaleBy(factor: number): Recipe {\n const newRecipe = this.clone();\n\n const originalServings = newRecipe.getServings();\n\n if (originalServings === undefined || originalServings === 0) {\n throw new Error(\"Error scaling recipe: no initial servings value set\");\n }\n\n newRecipe.ingredients = newRecipe.ingredients\n .map((ingredient) => {\n if (ingredient.quantity && !isNaN(Number(ingredient.quantity))) {\n (ingredient.quantity as number) *= factor;\n }\n return ingredient;\n })\n .filter((ingredient) => ingredient.quantity !== null);\n\n newRecipe.servings = originalServings * factor;\n\n if (newRecipe.metadata.servings && this.metadata.servings) {\n const servingsValue = parseFloat(this.metadata.servings);\n if (!isNaN(servingsValue)) {\n newRecipe.metadata.servings = String(servingsValue * factor);\n }\n }\n\n if (newRecipe.metadata.yield && this.metadata.yield) {\n const yieldValue = parseFloat(this.metadata.yield);\n if (!isNaN(yieldValue)) {\n newRecipe.metadata.yield = String(yieldValue * factor);\n }\n }\n\n if (newRecipe.metadata.serves && this.metadata.serves) {\n const servesValue = parseFloat(this.metadata.serves);\n if (!isNaN(servesValue)) {\n newRecipe.metadata.serves = String(servesValue * factor);\n }\n }\n\n return newRecipe;\n }\n\n private getServings(): number | undefined {\n if (this.servings) {\n return this.servings;\n }\n return undefined;\n }\n\n /**\n * Clones the recipe.\n * @returns A new Recipe instance with the same properties.\n */\n clone(): Recipe {\n const newRecipe = new Recipe();\n // deep copy\n newRecipe.metadata = JSON.parse(JSON.stringify(this.metadata));\n newRecipe.ingredients = JSON.parse(JSON.stringify(this.ingredients));\n newRecipe.sections = JSON.parse(JSON.stringify(this.sections));\n newRecipe.cookware = JSON.parse(JSON.stringify(this.cookware));\n newRecipe.timers = JSON.parse(JSON.stringify(this.timers));\n newRecipe.servings = this.servings;\n return newRecipe;\n }\n}\n","import { AisleConfig } from \"./aisle_config\";\nimport { Recipe } from \"./recipe\";\nimport type {\n Ingredient,\n CategorizedIngredients,\n AddedRecipe,\n AddedIngredient,\n} from \"../types\";\nimport { addQuantities } from \"../units\";\n\n/**\n * Represents a shopping list.\n * @category Classes\n */\nexport class ShoppingList {\n /**\n * The ingredients in the shopping list.\n * @see {@link Ingredient}\n */\n ingredients: Ingredient[] = [];\n /**\n * The recipes in the shopping list.\n * @see {@link AddedRecipe}\n */\n recipes: AddedRecipe[] = [];\n /**\n * The aisle configuration for the shopping list.\n * @see {@link AisleConfig}\n */\n aisle_config?: AisleConfig;\n /**\n * The categorized ingredients in the shopping list.\n * @see {@link CategorizedIngredients}\n */\n categories?: CategorizedIngredients;\n\n /**\n * Creates a new ShoppingList instance.\n * @param aisle_config_str - The aisle configuration to parse.\n */\n constructor(aisle_config_str?: string) {\n if (aisle_config_str) {\n this.set_aisle_config(aisle_config_str);\n }\n }\n\n private calculate_ingredients() {\n this.ingredients = [];\n for (const { recipe, factor } of this.recipes) {\n const scaledRecipe = factor === 1 ? recipe : recipe.scaleBy(factor);\n for (const ingredient of scaledRecipe.ingredients) {\n if (ingredient.hidden) {\n continue;\n }\n\n const existingIngredient = this.ingredients.find(\n (i) => i.name === ingredient.name,\n );\n\n let addSeparate = false;\n try {\n if (existingIngredient) {\n if (existingIngredient.quantity && ingredient.quantity) {\n const newQuantity = addQuantities(\n {\n value: existingIngredient.quantity,\n unit: existingIngredient.unit ?? \"\",\n },\n {\n value: ingredient.quantity,\n unit: ingredient.unit ?? \"\",\n },\n );\n existingIngredient.quantity = newQuantity.value;\n if (newQuantity.unit) {\n existingIngredient.unit = newQuantity.unit;\n }\n } else if (ingredient.quantity) {\n existingIngredient.quantity = ingredient.quantity;\n if (ingredient.unit) {\n existingIngredient.unit = ingredient.unit;\n }\n }\n }\n } catch {\n // Cannot add quantities, adding as separate ingredients\n addSeparate = true;\n }\n\n if (!existingIngredient || addSeparate) {\n const newIngredient: AddedIngredient = { name: ingredient.name };\n if (ingredient.quantity) {\n newIngredient.quantity = ingredient.quantity;\n }\n if (ingredient.unit) {\n newIngredient.unit = ingredient.unit;\n }\n this.ingredients.push(newIngredient);\n }\n }\n }\n }\n\n /**\n * Adds a recipe to the shopping list.\n * @param recipe - The recipe to add.\n * @param factor - The factor to scale the recipe by.\n */\n add_recipe(recipe: Recipe, factor: number = 1) {\n this.recipes.push({ recipe, factor });\n this.calculate_ingredients();\n this.categorize();\n }\n\n /**\n * Removes a recipe from the shopping list.\n * @param index - The index of the recipe to remove.\n */\n remove_recipe(index: number) {\n if (index < 0 || index >= this.recipes.length) {\n throw new Error(\"Index out of bounds\");\n }\n this.recipes.splice(index, 1);\n this.calculate_ingredients();\n this.categorize();\n }\n\n /**\n * Sets the aisle configuration for the shopping list.\n * @param config - The aisle configuration to parse.\n */\n set_aisle_config(config: string) {\n this.aisle_config = new AisleConfig(config);\n this.categorize();\n }\n\n /**\n * Categorizes the ingredients in the shopping list\n * Will use the aisle config if any, otherwise all ingredients will be placed in the \"other\" category\n */\n categorize() {\n if (!this.aisle_config) {\n this.categories = { other: this.ingredients };\n return;\n }\n\n const categories: CategorizedIngredients = { other: [] };\n for (const category of this.aisle_config.categories) {\n categories[category.name] = [];\n }\n\n for (const ingredient of this.ingredients) {\n let found = false;\n for (const category of this.aisle_config.categories) {\n for (const aisleIngredient of category.ingredients) {\n if (aisleIngredient.aliases.includes(ingredient.name)) {\n categories[category.name]!.push(ingredient);\n found = true;\n break;\n }\n }\n if (found) {\n break;\n }\n }\n if (!found) {\n categories.other!.push(ingredient);\n }\n }\n\n this.categories = categories;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACMO,IAAM,cAAN,MAAkB;AAAA;AAAA;AAAA;AAAA;AAAA,EAWvB,YAAY,QAAiB;AAN7B;AAAA;AAAA;AAAA;AAAA,sCAA8B,CAAC;AAO7B,QAAI,QAAQ;AACV,WAAK,MAAM,MAAM;AAAA,IACnB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,QAAgB;AACpB,QAAI,kBAAwC;AAC5C,UAAM,gBAAgB,oBAAI,IAAY;AACtC,UAAM,kBAAkB,oBAAI,IAAY;AAExC,eAAW,QAAQ,OAAO,MAAM,IAAI,GAAG;AACrC,YAAM,cAAc,KAAK,KAAK;AAE9B,UAAI,YAAY,WAAW,GAAG;AAC5B;AAAA,MACF;AAEA,UAAI,YAAY,WAAW,GAAG,KAAK,YAAY,SAAS,GAAG,GAAG;AAC5D,cAAM,eAAe,YAClB,UAAU,GAAG,YAAY,SAAS,CAAC,EACnC,KAAK;AAER,YAAI,cAAc,IAAI,YAAY,GAAG;AACnC,gBAAM,IAAI,MAAM,6BAA6B,YAAY,EAAE;AAAA,QAC7D;AACA,sBAAc,IAAI,YAAY;AAE9B,0BAAkB,EAAE,MAAM,cAAc,aAAa,CAAC,EAAE;AACxD,aAAK,WAAW,KAAK,eAAe;AAAA,MACtC,OAAO;AACL,YAAI,oBAAoB,MAAM;AAC5B,gBAAM,IAAI;AAAA,YACR,wCAAwC,WAAW;AAAA,UACrD;AAAA,QACF;AAEA,cAAM,UAAU,YAAY,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AAC1D,mBAAW,SAAS,SAAS;AAC3B,cAAI,gBAAgB,IAAI,KAAK,GAAG;AAC9B,kBAAM,IAAI,MAAM,qCAAqC,KAAK,EAAE;AAAA,UAC9D;AACA,0BAAgB,IAAI,KAAK;AAAA,QAC3B;AAEA,cAAM,aAA8B;AAAA,UAClC,MAAM,QAAQ,CAAC;AAAA;AAAA,UACf;AAAA,QACF;AACA,wBAAgB,YAAY,KAAK,UAAU;AAAA,MAC7C;AAAA,IACF;AAAA,EACF;AACF;;;ACxEO,IAAM,UAAN,MAAc;AAAA,EAInB,YAAY,OAAe,IAAI;AAH/B;AACA,mCAA2B,CAAC;AAG1B,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,UAAmB;AACjB,WAAO,KAAK,SAAS,MAAM,KAAK,QAAQ,WAAW;AAAA,EACrD;AACF;;;ACbO,IAAM,gBAAgB;AAE7B,IAAM,sBACJ;AACF,IAAM,uBACJ;AAEF,IAAM,oBACJ;AACF,IAAM,qBACJ;AAEF,IAAM,QACJ;AAEK,IAAM,cAAc,IAAI;AAAA,EAC7B;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EACG,IAAI,CAAC,MAAM,EAAE,MAAM,EACnB,KAAK,GAAG;AAAA,EACX;AACF;AAEO,IAAM,eAAe;AACrB,IAAM,oBAAoB;;;ACZjC,IAAM,QAA0B;AAAA;AAAA,EAE9B;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS,CAAC,QAAQ,OAAO;AAAA,IACzB,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS,CAAC,YAAY,WAAW;AAAA,IACjC,QAAQ;AAAA,EACV;AAAA;AAAA,EAEA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS,CAAC,SAAS,QAAQ;AAAA,IAC3B,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS,CAAC,SAAS,QAAQ;AAAA,IAC3B,QAAQ;AAAA,EACV;AAAA;AAAA,EAGA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS,CAAC,cAAc,eAAe,cAAc,aAAa;AAAA,IAClE,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS,CAAC,SAAS,UAAU,SAAS,QAAQ;AAAA,IAC9C,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS,CAAC,YAAY,WAAW;AAAA,IACjC,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS,CAAC,cAAc,aAAa;AAAA,IACrC,QAAQ;AAAA,EACV;AAAA;AAAA,EAGA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS,CAAC,eAAe,cAAc;AAAA,IACvC,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS,CAAC,MAAM;AAAA,IAChB,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS,CAAC,OAAO;AAAA,IACjB,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS,CAAC,QAAQ;AAAA,IAClB,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS,CAAC,SAAS;AAAA,IACnB,QAAQ;AAAA,EACV;AAAA;AAAA,EAGA;AAAA,IACE,MAAM;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS,CAAC,QAAQ;AAAA,IAClB,QAAQ;AAAA,EACV;AACF;AAEA,IAAM,UAAU,oBAAI,IAA4B;AAChD,WAAW,QAAQ,OAAO;AACxB,UAAQ,IAAI,KAAK,KAAK,YAAY,GAAG,IAAI;AACzC,aAAW,SAAS,KAAK,SAAS;AAChC,YAAQ,IAAI,MAAM,YAAY,GAAG,IAAI;AAAA,EACvC;AACF;AAEO,SAAS,cAAc,MAA0C;AACtE,SAAO,QAAQ,IAAI,KAAK,YAAY,EAAE,KAAK,CAAC;AAC9C;AAKO,SAAS,cAAc,IAAc,IAAwB;AAClE,QAAM,WAAW,cAAc,GAAG,IAAI;AACtC,QAAM,WAAW,cAAc,GAAG,IAAI;AAEtC,MAAI,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG;AAC3B,UAAM,IAAI;AAAA,MACR,mDAAmD,GAAG,KAAK;AAAA,IAC7D;AAAA,EACF;AACA,MAAI,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG;AAC3B,UAAM,IAAI;AAAA,MACR,mDAAmD,GAAG,KAAK;AAAA,IAC7D;AAAA,EACF;AAGA,MAAI,GAAG,SAAS,MAAM,UAAU;AAC9B,WAAO;AAAA,MACL,OACE,KAAK,OAAQ,GAAG,QAAoB,GAAG,SAAoB,GAAG,IAAI;AAAA,MACpE,MAAM,GAAG;AAAA,IACX;AAAA,EACF;AACA,MAAI,GAAG,SAAS,MAAM,UAAU;AAC9B,WAAO;AAAA,MACL,OACE,KAAK,OAAQ,GAAG,QAAoB,GAAG,SAAoB,GAAG,IAAI;AAAA,MACpE,MAAM,GAAG;AAAA,IACX;AAAA,EACF;AAGA,MAAI,GAAG,KAAK,YAAY,MAAM,GAAG,KAAK,YAAY,GAAG;AACnD,WAAO;AAAA,MACL,OACE,KAAK,OAAQ,GAAG,QAAoB,GAAG,SAAoB,GAAG,IAAI;AAAA,MACpE,MAAM,GAAG;AAAA,IACX;AAAA,EACF;AAGA,MAAI,YAAY,UAAU;AACxB,QAAI,SAAS,SAAS,SAAS,MAAM;AACnC,YAAM,IAAI;AAAA,QACR,6CAA6C,SAAS,IAAI,KAAK,GAAG,IAAI,SAAS,SAAS,IAAI,KAAK,GAAG,IAAI;AAAA,MAC1G;AAAA,IACF;AAGA,UAAM,aAAc,GAAG,QAAmB,SAAS;AACnD,UAAM,aAAc,GAAG,QAAmB,SAAS;AACnD,UAAM,iBAAiB,aAAa;AAEpC,QAAI;AAGJ,QAAI,SAAS,WAAW,SAAS,QAAQ;AACvC,YAAM,gBAAgB,SAAS,WAAW,WAAW,WAAW;AAChE,sBAAgB,MACb,OAAO,CAAC,MAAM,EAAE,SAAS,cAAc,QAAQ,EAAE,WAAW,QAAQ,EACpE;AAAA,QAAO,CAAC,MAAM,YACb,KAAK,SAAS,QAAQ,SAAS,OAAO;AAAA,MACxC;AAAA,IACJ,OAAO;AAEL,sBAAgB,SAAS,UAAU,SAAS,SAAS,WAAW;AAAA,IAClE;AAEA,UAAM,aAAa,iBAAiB,cAAc;AAElD,WAAO;AAAA,MACL,OAAO,KAAK,MAAM,aAAa,GAAG,IAAI;AAAA,MACtC,MAAM,cAAc;AAAA,IACtB;AAAA,EACF;AAGA,QAAM,IAAI;AAAA,IACR,6DAA6D,GAAG,IAAI,QAAQ,GAAG,IAAI;AAAA,EACrF;AACF;;;AChNO,SAAS,WACd,MACA,QACA,SACQ;AACR,MAAI,QAAQ,KAAK,UAAU,MAAM;AACjC,MAAI,UAAU,IAAI;AAChB,YAAQ,KAAK,KAAK,QAAQ,CAAC,IAAI;AAAA,EACjC;AACA,SAAO;AACT;AAQO,SAAS,iBACd,SACA,MACc;AACd,MAAI,KAAK,SAAS,GAAG;AACnB,YAAQ,QAAQ,KAAK,EAAE,KAAK,CAAC;AAC7B,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAQO,SAAS,kBACd,SACA,OACS;AACT,MAAI,MAAM,SAAS,GAAG;AACpB,YAAQ,QAAQ,KAAK,EAAE,OAAO,CAAC,GAAG,KAAK,EAAE,CAAC;AAC1C,UAAM,SAAS;AACf,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAUO,SAAS,wBACd,aACA,eACA,aACQ;AACR,QAAM,EAAE,MAAM,UAAU,KAAK,IAAI;AAGjC,MAAI,aAAa;AACf,UAAM,QAAQ,YAAY;AAAA,MACxB,CAAC,MAAM,EAAE,KAAK,YAAY,MAAM,KAAK,YAAY;AAAA,IACnD;AAEA,QAAI,UAAU,IAAI;AAChB,YAAM,IAAI;AAAA,QACR,0BAA0B,IAAI;AAAA,MAChC;AAAA,IACF;AAGA,UAAM,qBAAqB,YAAY,KAAK;AAC5C,QAAI,aAAa,QAAW;AAC1B,YAAM,kBAAkB;AAAA,QACtB,OAAO,mBAAmB,YAAY;AAAA,QACtC,MAAM,mBAAmB,QAAQ;AAAA,MACnC;AACA,YAAM,cAAc,EAAE,OAAO,UAAU,MAAM,QAAQ,GAAG;AAExD,YAAM,QAAQ,cAAc,iBAAiB,WAAW;AACxD,yBAAmB,WAAW,MAAM;AACpC,yBAAmB,OAAO,MAAM,QAAQ;AAAA,IAC1C;AACA,WAAO;AAAA,EACT;AAGA,SAAO,YAAY,KAAK,aAAa,IAAI;AAC3C;AAEO,SAAS,sBACd,UACA,aACA,aACQ;AACR,QAAM,EAAE,KAAK,IAAI;AAEjB,MAAI,aAAa;AACf,UAAM,QAAQ,SAAS;AAAA,MACrB,CAAC,MAAM,EAAE,KAAK,YAAY,MAAM,KAAK,YAAY;AAAA,IACnD;AAEA,QAAI,UAAU,IAAI;AAChB,YAAM,IAAI;AAAA,QACR,wBAAwB,IAAI;AAAA,MAC9B;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAEA,SAAO,SAAS,KAAK,WAAW,IAAI;AACtC;AAEO,SAAS,YAAY,WAA2B;AACrD,QAAM,YAAY,OAAO,SAAS,EAAE,QAAQ,KAAK,GAAG;AACpD,MAAI,CAAC,UAAU,WAAW,GAAG,KAAK,UAAU,SAAS,GAAG,GAAG;AACzD,UAAM,CAAC,KAAK,GAAG,IAAI,UAAU,MAAM,GAAG,EAAE,IAAI,MAAM;AAClD,WAAO,MAAO;AAAA,EAChB;AACA,SAAO,OAAO,SAAS;AACzB;AAEO,SAAS,mBAAmB,SAAiB,SAAiB;AACnE,QAAM,WAAW,QAAQ;AAAA,IACvB,IAAI,OAAO,IAAI,OAAO,gCAAgC,GAAG;AAAA,EAC3D;AACA,SAAO,WACH,SAAS,CAAC,GAAG,KAAK,EAAE,QAAQ,gBAAgB,GAAG,IAC/C;AACN;AAEO,SAAS,oBAAoB,SAAiB,SAAiB;AACpE,QAAM,WAAW,QAAQ;AAAA,IACvB,IAAI,OAAO,IAAI,OAAO,mCAAmC,GAAG;AAAA,EAC9D;AACA,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI,MAAM,OAAO,SAAS,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG;AACtC,UAAM,IAAI,MAAM,qCAAqC;AAAA,EACvD;AACA,SAAO,CAAC,OAAO,SAAS,CAAC,GAAG,KAAK,CAAC,GAAG,SAAS,CAAC,GAAG,KAAK,CAAC;AAC1D;AAEO,SAAS,iBAAiB,SAAiB,SAAiB;AAEjE,QAAM,YAAY,QAAQ;AAAA,IACxB,IAAI;AAAA,MACF,IAAI,OAAO;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACA,MAAI,CAAC,UAAW,QAAO;AAEvB,MAAI,UAAU,CAAC,MAAM,QAAW;AAE9B,WAAO,UAAU,CAAC,EAAE,MAAM,GAAG,EAAE,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC;AAAA,EACxD,WAAW,UAAU,CAAC,GAAG;AAKvB,WAAO,UAAU,CAAC,EACf,MAAM,IAAI,EACV,OAAO,CAAC,SAAS,KAAK,KAAK,MAAM,EAAE,EACnC,IAAI,CAAC,SAAS,KAAK,QAAQ,YAAY,EAAE,EAAE,KAAK,CAAC;AAAA,EACtD;AACF;AAEO,SAAS,gBAAgB,SAAkC;AAChE,QAAM,WAAqB,CAAC;AAC5B,MAAI,WAA+B;AAGnC,QAAM,kBAAkB,QAAQ,MAAM,aAAa,IAAI,CAAC;AACxD,MAAI,CAAC,iBAAiB;AACpB,WAAO,EAAE,SAAS;AAAA,EACpB;AAGA,aAAW,WAAW;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAAyB;AACvB,UAAM,kBAAuB,mBAAmB,iBAAiB,OAAO;AACxE,QAAI,gBAAiB,UAAS,OAAO,IAAI;AAAA,EAC3C;AAGA,aAAW,WAAW,CAAC,YAAY,SAAS,QAAQ,GAAyB;AAC3E,UAAM,mBAAwB,oBAAoB,iBAAiB,OAAO;AAC1E,QAAI,oBAAoB,iBAAiB,CAAC,GAAG;AAC3C,eAAS,OAAO,IAAI,iBAAiB,CAAC;AACtC,iBAAW,iBAAiB,CAAC;AAAA,IAC/B;AAAA,EACF;AAGA,aAAW,WAAW,CAAC,QAAQ,UAAU,UAAU,GAAyB;AAC1E,UAAM,gBAAqB,iBAAiB,iBAAiB,OAAO;AACpE,QAAI,cAAe,UAAS,OAAO,IAAI;AAAA,EACzC;AAEA,SAAO,EAAE,UAAU,SAAS;AAC9B;;;ACnNO,IAAM,SAAN,MAAM,QAAO;AAAA;AAAA;AAAA;AAAA;AAAA,EAmClB,YAAY,SAAkB;AA9B9B;AAAA;AAAA;AAAA;AAAA,oCAAqB,CAAC;AAKtB;AAAA;AAAA;AAAA;AAAA,uCAA4B,CAAC;AAK7B;AAAA;AAAA;AAAA;AAAA,oCAAsB,CAAC;AAKvB;AAAA;AAAA;AAAA;AAAA,oCAAuB,CAAC;AAKxB;AAAA;AAAA;AAAA;AAAA,kCAAkB,CAAC;AAInB;AAAA;AAAA;AAAA;AAOE,QAAI,SAAS;AACX,WAAK,MAAM,OAAO;AAAA,IACpB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,SAAiB;AACrB,UAAM,eAAe,QAClB,QAAQ,eAAe,EAAE,EACzB,QAAQ,cAAc,EAAE,EACxB,QAAQ,mBAAmB,EAAE,EAC7B,KAAK,EACL,MAAM,UAAU;AAEnB,UAAM,EAAE,UAAU,SAAS,IAAqB,gBAAgB,OAAO;AACvE,SAAK,WAAW;AAChB,SAAK,WAAW;AAEhB,QAAI,kBAAkB;AACtB,QAAI,UAAmB,IAAI,QAAQ;AACnC,UAAM,QAAuB,CAAC;AAC9B,QAAI,OAAqB;AACzB,QAAI,SAAS;AAEb,eAAW,QAAQ,cAAc;AAC/B,UAAI,KAAK,KAAK,EAAE,WAAW,GAAG;AAC5B,0BAAkB,SAAS,KAAK;AAChC,eAAO,iBAAiB,SAAS,IAAI;AACrC,0BAAkB;AAClB,iBAAS;AACT;AAAA,MACF;AAEA,UAAI,KAAK,WAAW,GAAG,GAAG;AACxB,0BAAkB,SAAS,KAAK;AAChC,eAAO,iBAAiB,SAAS,IAAI;AAErC,YAAI,KAAK,SAAS,WAAW,KAAK,QAAQ,QAAQ,GAAG;AACnD,kBAAQ,OAAO,KAAK,UAAU,CAAC,EAAE,KAAK;AAAA,QACxC,OAAO;AACL,cAAI,CAAC,QAAQ,QAAQ,GAAG;AACtB,iBAAK,SAAS,KAAK,OAAO;AAAA,UAC5B;AACA,oBAAU,IAAI,QAAQ,KAAK,UAAU,CAAC,EAAE,KAAK,CAAC;AAAA,QAChD;AACA,0BAAkB;AAClB,iBAAS;AACT;AAAA,MACF;AAEA,UAAI,mBAAmB,KAAK,WAAW,GAAG,GAAG;AAC3C,0BAAkB,SAAS,KAAK;AAChC,eAAO,iBAAiB,SAAS,IAAI;AACrC,gBAAQ,KAAK,UAAU,CAAC,EAAE,KAAK;AAC/B,iBAAS;AACT,0BAAkB;AAClB;AAAA,MACF;AAEA,UAAI,QAAQ;AACV,YAAI,KAAK,WAAW,GAAG,GAAG;AACxB,kBAAQ,MAAM,KAAK,UAAU,CAAC,EAAE,KAAK;AAAA,QACvC,OAAO;AACL,kBAAQ,MAAM,KAAK,KAAK;AAAA,QAC1B;AACA,0BAAkB;AAClB;AAAA,MACF;AAEA,aAAO,iBAAiB,SAAS,IAAI;AAErC,UAAI,SAAS;AACb,iBAAW,SAAS,KAAK,SAAS,WAAW,GAAG;AAC9C,cAAM,MAAM,MAAM;AAClB,YAAI,MAAM,QAAQ;AAChB,gBAAM,KAAK,EAAE,MAAM,QAAQ,OAAO,KAAK,MAAM,QAAQ,GAAG,EAAE,CAAC;AAAA,QAC7D;AAEA,cAAM,SAAS,MAAM;AAErB,YAAI,OAAO,mBAAmB,OAAO,iBAAiB;AACpD,gBAAM,OAAQ,OAAO,mBAAmB,OAAO;AAC/C,gBAAM,cACJ,OAAO,uBAAuB,OAAO;AACvC,gBAAMA,SAAQ,OAAO,oBAAoB,OAAO;AAChD,gBAAM,cACJ,OAAO,0BAA0B,OAAO;AAC1C,gBAAM,WACJ,OAAO,uBAAuB,OAAO;AACvC,gBAAM,WAAW,aAAa;AAC9B,gBAAM,SAAS,aAAa;AAC5B,gBAAM,YAAY,aAAa;AAC/B,gBAAM,WAAW,aAAa;AAC9B,gBAAM,WAAW,cAAc,YAAY,WAAW,IAAI;AAE1D,gBAAM,YAAY;AAAA,YAChB,KAAK;AAAA,YACL;AAAA,cACE;AAAA,cACA;AAAA,cACA,MAAMA;AAAA,cACN;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,YACF;AAAA,YACA;AAAA,UACF;AAEA,gBAAM,KAAK,EAAE,MAAM,cAAc,OAAO,UAAU,CAAC;AAAA,QACrD,WAAW,OAAO,iBAAiB,OAAO,eAAe;AACvD,gBAAM,OAAQ,OAAO,iBAAiB,OAAO;AAC7C,gBAAM,WAAW,OAAO,qBAAqB,OAAO;AACpD,gBAAM,WAAW,aAAa;AAC9B,gBAAM,SAAS,aAAa;AAC5B,gBAAM,YAAY,aAAa;AAE/B,gBAAM,YAAY;AAAA,YAChB,KAAK;AAAA,YACL,EAAE,MAAM,UAAU,OAAO;AAAA,YACzB;AAAA,UACF;AACA,gBAAM,KAAK,EAAE,MAAM,YAAY,OAAO,UAAU,CAAC;AAAA,QACnD,WAAW,OAAO,kBAAkB,QAAW;AAC7C,gBAAM,cAAc,OAAO,cAAc,KAAK;AAC9C,gBAAM,QAAQ,OAAO,cAAc,IAAI,KAAK;AAC5C,cAAI,CAAC,MAAM;AACT,kBAAM,IAAI,MAAM,qBAAqB;AAAA,UACvC;AACA,gBAAM,OAAO,OAAO,aAAa;AACjC,gBAAM,WAAW,YAAY,WAAW;AACxC,gBAAM,WAAkB;AAAA,YACtB;AAAA,YACA;AAAA,YACA;AAAA,UACF;AACA,gBAAM,YAAY;AAAA,YAChB,KAAK;AAAA,YACL,CAAC,MACC,EAAE,SAAS,SAAS,QACpB,EAAE,aAAa,SAAS,YACxB,EAAE,SAAS,SAAS;AAAA,YACtB,MAAM;AAAA,UACR;AACA,gBAAM,KAAK,EAAE,MAAM,SAAS,OAAO,UAAU,CAAC;AAAA,QAChD;AAEA,iBAAS,MAAM,MAAM,CAAC,EAAE;AAAA,MAC1B;AAEA,UAAI,SAAS,KAAK,QAAQ;AACxB,cAAM,KAAK,EAAE,MAAM,QAAQ,OAAO,KAAK,MAAM,MAAM,EAAE,CAAC;AAAA,MACxD;AAEA,wBAAkB;AAAA,IACpB;AAGA,sBAAkB,SAAS,KAAK;AAChC,WAAO,iBAAiB,SAAS,IAAI;AACrC,QAAI,CAAC,QAAQ,QAAQ,GAAG;AACtB,WAAK,SAAS,KAAK,OAAO;AAAA,IAC5B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAQ,aAA6B;AACnC,UAAM,mBAAmB,KAAK,YAAY;AAE1C,QAAI,qBAAqB,UAAa,qBAAqB,GAAG;AAC5D,YAAM,IAAI,MAAM,qDAAqD;AAAA,IACvE;AAEA,UAAM,SAAS,cAAc;AAC7B,WAAO,KAAK,QAAQ,MAAM;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAQ,QAAwB;AAC9B,UAAM,YAAY,KAAK,MAAM;AAE7B,UAAM,mBAAmB,UAAU,YAAY;AAE/C,QAAI,qBAAqB,UAAa,qBAAqB,GAAG;AAC5D,YAAM,IAAI,MAAM,qDAAqD;AAAA,IACvE;AAEA,cAAU,cAAc,UAAU,YAC/B,IAAI,CAAC,eAAe;AACnB,UAAI,WAAW,YAAY,CAAC,MAAM,OAAO,WAAW,QAAQ,CAAC,GAAG;AAC9D,QAAC,WAAW,YAAuB;AAAA,MACrC;AACA,aAAO;AAAA,IACT,CAAC,EACA,OAAO,CAAC,eAAe,WAAW,aAAa,IAAI;AAEtD,cAAU,WAAW,mBAAmB;AAExC,QAAI,UAAU,SAAS,YAAY,KAAK,SAAS,UAAU;AACzD,YAAM,gBAAgB,WAAW,KAAK,SAAS,QAAQ;AACvD,UAAI,CAAC,MAAM,aAAa,GAAG;AACzB,kBAAU,SAAS,WAAW,OAAO,gBAAgB,MAAM;AAAA,MAC7D;AAAA,IACF;AAEA,QAAI,UAAU,SAAS,SAAS,KAAK,SAAS,OAAO;AACnD,YAAM,aAAa,WAAW,KAAK,SAAS,KAAK;AACjD,UAAI,CAAC,MAAM,UAAU,GAAG;AACtB,kBAAU,SAAS,QAAQ,OAAO,aAAa,MAAM;AAAA,MACvD;AAAA,IACF;AAEA,QAAI,UAAU,SAAS,UAAU,KAAK,SAAS,QAAQ;AACrD,YAAM,cAAc,WAAW,KAAK,SAAS,MAAM;AACnD,UAAI,CAAC,MAAM,WAAW,GAAG;AACvB,kBAAU,SAAS,SAAS,OAAO,cAAc,MAAM;AAAA,MACzD;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEQ,cAAkC;AACxC,QAAI,KAAK,UAAU;AACjB,aAAO,KAAK;AAAA,IACd;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAgB;AACd,UAAM,YAAY,IAAI,QAAO;AAE7B,cAAU,WAAW,KAAK,MAAM,KAAK,UAAU,KAAK,QAAQ,CAAC;AAC7D,cAAU,cAAc,KAAK,MAAM,KAAK,UAAU,KAAK,WAAW,CAAC;AACnE,cAAU,WAAW,KAAK,MAAM,KAAK,UAAU,KAAK,QAAQ,CAAC;AAC7D,cAAU,WAAW,KAAK,MAAM,KAAK,UAAU,KAAK,QAAQ,CAAC;AAC7D,cAAU,SAAS,KAAK,MAAM,KAAK,UAAU,KAAK,MAAM,CAAC;AACzD,cAAU,WAAW,KAAK;AAC1B,WAAO;AAAA,EACT;AACF;;;ACnTO,IAAM,eAAN,MAAmB;AAAA;AAAA;AAAA;AAAA;AAAA,EA0BxB,YAAY,kBAA2B;AArBvC;AAAA;AAAA;AAAA;AAAA,uCAA4B,CAAC;AAK7B;AAAA;AAAA;AAAA;AAAA,mCAAyB,CAAC;AAK1B;AAAA;AAAA;AAAA;AAAA;AAKA;AAAA;AAAA;AAAA;AAAA;AAOE,QAAI,kBAAkB;AACpB,WAAK,iBAAiB,gBAAgB;AAAA,IACxC;AAAA,EACF;AAAA,EAEQ,wBAAwB;AAC9B,SAAK,cAAc,CAAC;AACpB,eAAW,EAAE,QAAQ,OAAO,KAAK,KAAK,SAAS;AAC7C,YAAM,eAAe,WAAW,IAAI,SAAS,OAAO,QAAQ,MAAM;AAClE,iBAAW,cAAc,aAAa,aAAa;AACjD,YAAI,WAAW,QAAQ;AACrB;AAAA,QACF;AAEA,cAAM,qBAAqB,KAAK,YAAY;AAAA,UAC1C,CAAC,MAAM,EAAE,SAAS,WAAW;AAAA,QAC/B;AAEA,YAAI,cAAc;AAClB,YAAI;AACF,cAAI,oBAAoB;AACtB,gBAAI,mBAAmB,YAAY,WAAW,UAAU;AACtD,oBAAM,cAAc;AAAA,gBAClB;AAAA,kBACE,OAAO,mBAAmB;AAAA,kBAC1B,MAAM,mBAAmB,QAAQ;AAAA,gBACnC;AAAA,gBACA;AAAA,kBACE,OAAO,WAAW;AAAA,kBAClB,MAAM,WAAW,QAAQ;AAAA,gBAC3B;AAAA,cACF;AACA,iCAAmB,WAAW,YAAY;AAC1C,kBAAI,YAAY,MAAM;AACpB,mCAAmB,OAAO,YAAY;AAAA,cACxC;AAAA,YACF,WAAW,WAAW,UAAU;AAC9B,iCAAmB,WAAW,WAAW;AACzC,kBAAI,WAAW,MAAM;AACnB,mCAAmB,OAAO,WAAW;AAAA,cACvC;AAAA,YACF;AAAA,UACF;AAAA,QACF,QAAQ;AAEN,wBAAc;AAAA,QAChB;AAEA,YAAI,CAAC,sBAAsB,aAAa;AACtC,gBAAM,gBAAiC,EAAE,MAAM,WAAW,KAAK;AAC/D,cAAI,WAAW,UAAU;AACvB,0BAAc,WAAW,WAAW;AAAA,UACtC;AACA,cAAI,WAAW,MAAM;AACnB,0BAAc,OAAO,WAAW;AAAA,UAClC;AACA,eAAK,YAAY,KAAK,aAAa;AAAA,QACrC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAW,QAAgB,SAAiB,GAAG;AAC7C,SAAK,QAAQ,KAAK,EAAE,QAAQ,OAAO,CAAC;AACpC,SAAK,sBAAsB;AAC3B,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,cAAc,OAAe;AAC3B,QAAI,QAAQ,KAAK,SAAS,KAAK,QAAQ,QAAQ;AAC7C,YAAM,IAAI,MAAM,qBAAqB;AAAA,IACvC;AACA,SAAK,QAAQ,OAAO,OAAO,CAAC;AAC5B,SAAK,sBAAsB;AAC3B,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,iBAAiB,QAAgB;AAC/B,SAAK,eAAe,IAAI,YAAY,MAAM;AAC1C,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,aAAa;AACX,QAAI,CAAC,KAAK,cAAc;AACtB,WAAK,aAAa,EAAE,OAAO,KAAK,YAAY;AAC5C;AAAA,IACF;AAEA,UAAM,aAAqC,EAAE,OAAO,CAAC,EAAE;AACvD,eAAW,YAAY,KAAK,aAAa,YAAY;AACnD,iBAAW,SAAS,IAAI,IAAI,CAAC;AAAA,IAC/B;AAEA,eAAW,cAAc,KAAK,aAAa;AACzC,UAAI,QAAQ;AACZ,iBAAW,YAAY,KAAK,aAAa,YAAY;AACnD,mBAAW,mBAAmB,SAAS,aAAa;AAClD,cAAI,gBAAgB,QAAQ,SAAS,WAAW,IAAI,GAAG;AACrD,uBAAW,SAAS,IAAI,EAAG,KAAK,UAAU;AAC1C,oBAAQ;AACR;AAAA,UACF;AAAA,QACF;AACA,YAAI,OAAO;AACT;AAAA,QACF;AAAA,MACF;AACA,UAAI,CAAC,OAAO;AACV,mBAAW,MAAO,KAAK,UAAU;AAAA,MACnC;AAAA,IACF;AAEA,SAAK,aAAa;AAAA,EACpB;AACF;","names":["units"]}
@@ -0,0 +1,369 @@
1
+ declare class Section {
2
+ name: string;
3
+ content: (Step | Note)[];
4
+ constructor(name?: string);
5
+ isBlank(): boolean;
6
+ }
7
+
8
+ /**
9
+ * Represents a recipe.
10
+ * @category Classes
11
+ */
12
+ declare class Recipe {
13
+ /**
14
+ * The recipe's metadata.
15
+ * @see {@link Metadata}
16
+ */
17
+ metadata: Metadata;
18
+ /**
19
+ * The recipe's ingredients.
20
+ * @see {@link Ingredient}
21
+ */
22
+ ingredients: Ingredient[];
23
+ /**
24
+ * The recipe's sections.
25
+ * @see {@link Section}
26
+ */
27
+ sections: Section[];
28
+ /**
29
+ * The recipe's cookware.
30
+ * @see {@link Cookware}
31
+ */
32
+ cookware: Cookware[];
33
+ /**
34
+ * The recipe's timers.
35
+ * @see {@link Timer}
36
+ */
37
+ timers: Timer[];
38
+ /**
39
+ * The recipe's servings. Used for scaling
40
+ */
41
+ servings?: number;
42
+ /**
43
+ * Creates a new Recipe instance.
44
+ * @param content - The recipe content to parse.
45
+ */
46
+ constructor(content?: string);
47
+ /**
48
+ * Parses a recipe from a string.
49
+ * @param content - The recipe content to parse.
50
+ */
51
+ parse(content: string): void;
52
+ /**
53
+ * Scales the recipe to a new number of servings.
54
+ * @param newServings - The new number of servings.
55
+ * @returns A new Recipe instance with the scaled ingredients.
56
+ */
57
+ scaleTo(newServings: number): Recipe;
58
+ /**
59
+ * Scales the recipe by a factor.
60
+ * @param factor - The factor to scale the recipe by.
61
+ * @returns A new Recipe instance with the scaled ingredients.
62
+ */
63
+ scaleBy(factor: number): Recipe;
64
+ private getServings;
65
+ /**
66
+ * Clones the recipe.
67
+ * @returns A new Recipe instance with the same properties.
68
+ */
69
+ clone(): Recipe;
70
+ }
71
+
72
+ /**
73
+ * Represents the metadata of a recipe.
74
+ * @category Types
75
+ */
76
+ interface Metadata {
77
+ /** The title of the recipe. */
78
+ title?: string;
79
+ /** The tags of the recipe. */
80
+ tags?: string[];
81
+ /** The source of the recipe. */
82
+ source?: string;
83
+ /** The author of the recipe. */
84
+ author?: string;
85
+ /** The number of servings the recipe makes.
86
+ * Complex info can be given, as long as the first part before a comma has a numerical value, which will be used for scaling
87
+ * Interchangeable with `yield` or `serves`. If multiple ones are defined, the latest one will be used for scaling */
88
+ servings?: string;
89
+ /** The yield of the recipe.
90
+ * Complex info can be given, as long as the first part before a comma has a numerical value, which will be used for scaling
91
+ * Interchangeable with `servings` or `serves`. If multiple ones are defined, the latest one will be used for scaling
92
+ */
93
+ yield?: string;
94
+ /** The number of people the recipe serves.
95
+ * Complex info can be given, as long as the first part before a comma has a numerical value, which will be used for scaling
96
+ * Interchangeable with `servings` or `yield`. If multiple ones are defined, the latest one will be used for scaling
97
+ */
98
+ serves?: string;
99
+ /** The course of the recipe. */
100
+ course?: string;
101
+ /** The category of the recipe. */
102
+ category?: string;
103
+ /**
104
+ * The preparation time of the recipe.
105
+ * Will not be further parsed into any DateTime format nor normalize
106
+ */
107
+ "prep time"?: string;
108
+ /**
109
+ * Alias of `prep time`
110
+ */
111
+ "time.prep"?: string;
112
+ /**
113
+ * The cooking time of the recipe.
114
+ * Will not be further parsed into any DateTime format nor normalize
115
+ */
116
+ "cook time"?: string;
117
+ /**
118
+ * Alias of `cook time`
119
+ */
120
+ "time.cook"?: string;
121
+ /**
122
+ * The total time of the recipe.
123
+ * Will not be further parsed into any DateTime format nor normalize
124
+ */
125
+ "time required"?: string;
126
+ time?: string;
127
+ duration?: string;
128
+ /** The difficulty of the recipe. */
129
+ difficulty?: string;
130
+ /** The cuisine of the recipe. */
131
+ cuisine?: string;
132
+ /** The diet of the recipe. */
133
+ diet?: string;
134
+ /** The description of the recipe. */
135
+ description?: string;
136
+ /** The images of the recipe. */
137
+ images?: string[];
138
+ }
139
+ /**
140
+ * Represents the extracted metadata from a recipe.
141
+ * @category Types
142
+ */
143
+ interface MetadataExtract {
144
+ /** The metadata of the recipe. */
145
+ metadata: Metadata;
146
+ /** The number of servings the recipe makes. Used for scaling */
147
+ servings?: number;
148
+ }
149
+ /**
150
+ * Represents an ingredient in a recipe.
151
+ * @category Types
152
+ */
153
+ interface Ingredient {
154
+ /** The name of the ingredient. */
155
+ name: string;
156
+ /** The quantity of the ingredient. */
157
+ quantity?: number | string;
158
+ /** The unit of the ingredient. */
159
+ unit?: string;
160
+ /** The preparation of the ingredient. */
161
+ preparation?: string;
162
+ /** Whether the ingredient is optional. */
163
+ optional?: boolean;
164
+ /** Whether the ingredient is hidden. */
165
+ hidden?: boolean;
166
+ /** Whether the ingredient is a recipe. */
167
+ isRecipe?: boolean;
168
+ }
169
+ /**
170
+ * Represents a timer in a recipe.
171
+ * @category Types
172
+ */
173
+ interface Timer {
174
+ /** The name of the timer. */
175
+ name?: string;
176
+ /** The duration of the timer. */
177
+ duration: number;
178
+ /** The unit of the timer. */
179
+ unit: string;
180
+ }
181
+ /**
182
+ * Represents a text item in a recipe step.
183
+ * @category Types
184
+ */
185
+ interface TextItem {
186
+ /** The type of the item. */
187
+ type: "text";
188
+ /** The value of the item. */
189
+ value: string;
190
+ }
191
+ /**
192
+ * Represents an ingredient item in a recipe step.
193
+ * @category Types
194
+ */
195
+ interface IngredientItem {
196
+ /** The type of the item. */
197
+ type: "ingredient";
198
+ /** The value of the item. */
199
+ value: number;
200
+ }
201
+ /**
202
+ * Represents a cookware item in a recipe step.
203
+ * @category Types
204
+ */
205
+ interface CookwareItem {
206
+ /** The type of the item. */
207
+ type: "cookware";
208
+ /** The value of the item. */
209
+ value: number;
210
+ }
211
+ /**
212
+ * Represents a timer item in a recipe step.
213
+ * @category Types
214
+ */
215
+ interface TimerItem {
216
+ /** The type of the item. */
217
+ type: "timer";
218
+ /** The value of the item. */
219
+ value: number;
220
+ }
221
+ /**
222
+ * Represents an item in a recipe step.
223
+ * @category Types
224
+ */
225
+ type Item = TextItem | IngredientItem | CookwareItem | TimerItem;
226
+ /**
227
+ * Represents a step in a recipe.
228
+ * @category Types
229
+ */
230
+ interface Step {
231
+ /** The items in the step. */
232
+ items: Item[];
233
+ }
234
+ /**
235
+ * Represents a note in a recipe.
236
+ * @category Types
237
+ */
238
+ interface Note {
239
+ /** The content of the note. */
240
+ note: string;
241
+ }
242
+ /**
243
+ * Represents a piece of cookware in a recipe.
244
+ * @category Types
245
+ */
246
+ interface Cookware {
247
+ /** The name of the cookware. */
248
+ name: string;
249
+ /** Whether the cookware is optional. */
250
+ optional?: boolean;
251
+ /** Whether the cookware is hidden. */
252
+ hidden?: boolean;
253
+ }
254
+ /**
255
+ * Represents categorized ingredients.
256
+ * @category Types
257
+ */
258
+ interface CategorizedIngredients {
259
+ /** The category of the ingredients. */
260
+ [category: string]: Ingredient[];
261
+ }
262
+ /**
263
+ * Represents a recipe that has been added to a shopping list.
264
+ * @category Types
265
+ */
266
+ interface AddedRecipe {
267
+ /** The recipe that was added. */
268
+ recipe: Recipe;
269
+ /** The factor the recipe was scaled by. */
270
+ factor: number;
271
+ }
272
+ /**
273
+ * Represents an ingredient in an aisle.
274
+ * @category Types
275
+ */
276
+ interface AisleIngredient {
277
+ /** The name of the ingredient. */
278
+ name: string;
279
+ /** The aliases of the ingredient. */
280
+ aliases: string[];
281
+ }
282
+ /**
283
+ * Represents a category of aisles.
284
+ * @category Types
285
+ */
286
+ interface AisleCategory {
287
+ /** The name of the category. */
288
+ name: string;
289
+ /** The ingredients in the category. */
290
+ ingredients: AisleIngredient[];
291
+ }
292
+
293
+ /**
294
+ * Represents the aisle configuration for a shopping list.
295
+ * @category Classes
296
+ */
297
+ declare class AisleConfig {
298
+ /**
299
+ * The categories of aisles.
300
+ * @see {@link AisleCategory}
301
+ */
302
+ categories: AisleCategory[];
303
+ /**
304
+ * Creates a new AisleConfig instance.
305
+ * @param config - The aisle configuration to parse.
306
+ */
307
+ constructor(config?: string);
308
+ /**
309
+ * Parses an aisle configuration from a string.
310
+ * @param config - The aisle configuration to parse.
311
+ */
312
+ parse(config: string): void;
313
+ }
314
+
315
+ /**
316
+ * Represents a shopping list.
317
+ * @category Classes
318
+ */
319
+ declare class ShoppingList {
320
+ /**
321
+ * The ingredients in the shopping list.
322
+ * @see {@link Ingredient}
323
+ */
324
+ ingredients: Ingredient[];
325
+ /**
326
+ * The recipes in the shopping list.
327
+ * @see {@link AddedRecipe}
328
+ */
329
+ recipes: AddedRecipe[];
330
+ /**
331
+ * The aisle configuration for the shopping list.
332
+ * @see {@link AisleConfig}
333
+ */
334
+ aisle_config?: AisleConfig;
335
+ /**
336
+ * The categorized ingredients in the shopping list.
337
+ * @see {@link CategorizedIngredients}
338
+ */
339
+ categories?: CategorizedIngredients;
340
+ /**
341
+ * Creates a new ShoppingList instance.
342
+ * @param aisle_config_str - The aisle configuration to parse.
343
+ */
344
+ constructor(aisle_config_str?: string);
345
+ private calculate_ingredients;
346
+ /**
347
+ * Adds a recipe to the shopping list.
348
+ * @param recipe - The recipe to add.
349
+ * @param factor - The factor to scale the recipe by.
350
+ */
351
+ add_recipe(recipe: Recipe, factor?: number): void;
352
+ /**
353
+ * Removes a recipe from the shopping list.
354
+ * @param index - The index of the recipe to remove.
355
+ */
356
+ remove_recipe(index: number): void;
357
+ /**
358
+ * Sets the aisle configuration for the shopping list.
359
+ * @param config - The aisle configuration to parse.
360
+ */
361
+ set_aisle_config(config: string): void;
362
+ /**
363
+ * Categorizes the ingredients in the shopping list
364
+ * Will use the aisle config if any, otherwise all ingredients will be placed in the "other" category
365
+ */
366
+ categorize(): void;
367
+ }
368
+
369
+ export { type AddedRecipe, type AisleCategory, AisleConfig, type AisleIngredient, type CategorizedIngredients, type Cookware, type CookwareItem, type Ingredient, type IngredientItem, type Item, type Metadata, type MetadataExtract, type Note, Recipe, ShoppingList, type Step, type TextItem, type Timer, type TimerItem };
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@tmlmt/cooklang-parser",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Cooklang parsers and utilities",
5
5
  "author": "Thomas Lamant <tom@tmlmt.com>",
6
6
  "type": "module",
7
7
  "main": "dist/index.cjs",
8
- "module": "dist/index.mjs",
8
+ "module": "dist/index.js",
9
9
  "types": "dist/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
- "import": "./dist/index.mjs",
12
+ "import": "./dist/index.js",
13
13
  "require": "./dist/index.cjs"
14
14
  },
15
15
  "./package.json": "./package.json"