@tmlmt/cooklang-parser 2.1.7 → 3.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -33,8 +33,12 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
33
33
  var index_exports = {};
34
34
  __export(index_exports, {
35
35
  CategoryConfig: () => CategoryConfig,
36
+ NoProductCatalogForCartError: () => NoProductCatalogForCartError,
37
+ NoShoppingListForCartError: () => NoShoppingListForCartError,
38
+ ProductCatalog: () => ProductCatalog,
36
39
  Recipe: () => Recipe,
37
40
  Section: () => Section,
41
+ ShoppingCart: () => ShoppingCart,
38
42
  ShoppingList: () => ShoppingList
39
43
  });
40
44
  module.exports = __toCommonJS(index_exports);
@@ -100,33 +104,10 @@ var CategoryConfig = class {
100
104
  }
101
105
  };
102
106
 
103
- // src/classes/section.ts
104
- var Section = class {
105
- /**
106
- * Creates an instance of Section.
107
- * @param name - The name of the section. Defaults to an empty string.
108
- */
109
- constructor(name = "") {
110
- /**
111
- * The name of the section. Can be an empty string for the default (first) section.
112
- * @defaultValue `""`
113
- */
114
- __publicField(this, "name");
115
- /** An array of steps and notes that make up the content of the section. */
116
- __publicField(this, "content", []);
117
- this.name = name;
118
- }
119
- /**
120
- * Checks if the section is blank (has no name and no content).
121
- * Used during recipe parsing
122
- * @returns `true` if the section is blank, otherwise `false`.
123
- */
124
- isBlank() {
125
- return this.name === "" && this.content.length === 0;
126
- }
127
- };
107
+ // src/classes/product_catalog.ts
108
+ var import_smol_toml = __toESM(require("smol-toml"), 1);
128
109
 
129
- // node_modules/.pnpm/human-regex@2.1.5_patch_hash=6d6bd9e233f99785a7c2187fd464edc114b76d47001dbb4eb6b5d72168de7460/node_modules/human-regex/dist/human-regex.esm.js
110
+ // node_modules/.pnpm/human-regex@2.2.0/node_modules/human-regex/dist/human-regex.esm.js
130
111
  var t = /* @__PURE__ */ new Map();
131
112
  var r = { GLOBAL: "g", NON_SENSITIVE: "i", MULTILINE: "m", DOT_ALL: "s", UNICODE: "u", STICKY: "y" };
132
113
  var e = Object.freeze({ digit: "0-9", lowercaseLetter: "a-z", uppercaseLetter: "A-Z", letter: "a-zA-Z", alphanumeric: "a-zA-Z0-9", anyCharacter: "." });
@@ -187,7 +168,7 @@ var a = class {
187
168
  return this.add(".");
188
169
  }
189
170
  newline() {
190
- return this.add("(?:\\r\\n|\\r|\\n)");
171
+ return this.add("(\\r\\n|\\r|\\n)");
191
172
  }
192
173
  negativeLookahead(t2) {
193
174
  return this.add(`(?!${t2})`);
@@ -328,19 +309,20 @@ var i = (() => {
328
309
  var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
329
310
  var scalingMetaValueRegex = (varName) => d().startAnchor().literal(varName).literal(":").anyOf("\\t ").zeroOrMore().startCaptureGroup().startCaptureGroup().notAnyOf(",\\n").oneOrMore().endGroup().startGroup().literal(",").whitespace().zeroOrMore().startCaptureGroup().anyCharacter().oneOrMore().endGroup().endGroup().optional().endGroup().endAnchor().multiline().toRegExp();
330
311
  var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
331
- var multiwordIngredient = d().literal("@").startNamedGroup("mIngredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("mIngredientRecipeAnchor").literal("./").endGroup().optional().startNamedGroup("mIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().notAnyOf("\\." + nonWordChar).endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").startGroup().literal("{").startNamedGroup("mIngredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("mIngredientQuantity").notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("mIngredientUnit").notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("mIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
332
- var singleWordIngredient = d().literal("@").startNamedGroup("sIngredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("sIngredientRecipeAnchor").literal("./").endGroup().optional().startNamedGroup("sIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().startGroup().literal("{").startNamedGroup("sIngredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("sIngredientQuantity").notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("sIngredientUnit").notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("sIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
312
+ var nonWordCharStrict = "\\s@#~\\[\\]{(,;:!?|";
313
+ var ingredientWithAlternativeRegex = d().literal("@").startNamedGroup("ingredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("ingredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("mIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("sIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("ingredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("ingredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("ingredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startNamedGroup("ingredientNote").notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().startNamedGroup("ingredientAlternative").startGroup().literal("|").startGroup().anyOf("@\\-&?").zeroOrMore().endGroup().optional().startGroup().literal("./").endGroup().optional().startGroup().startGroup().startGroup().notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startGroup().notAnyOf(nonWordChar).oneOrMore().endGroup().endGroup().startGroup().literal("{").startGroup().literal("=").exactly(1).endGroup().optional().startGroup().notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startGroup().notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().endGroup().zeroOrMore().endGroup().toRegExp();
314
+ var inlineIngredientAlternativesRegex = new RegExp("\\|" + ingredientWithAlternativeRegex.source.slice(1));
315
+ var quantityAlternativeRegex = d().startNamedGroup("ingredientQuantityValue").notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("ingredientUnit").notAnyOf("|}").oneOrMore().endGroup().endGroup().optional().startGroup().literal("|").startNamedGroup("ingredientAltQuantity").startGroup().notAnyOf("}").oneOrMore().endGroup().zeroOrMore().endGroup().endGroup().optional().toRegExp();
316
+ var ingredientWithGroupKeyRegex = d().literal("@|").startNamedGroup("gIngredientGroupKey").notAnyOf(nonWordCharStrict).oneOrMore().endGroup().literal("|").startNamedGroup("gIngredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("gIngredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("gmIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("gsIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("gIngredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("gIngredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("gIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
333
317
  var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
334
- var multiwordCookware = d().literal("#").startNamedGroup("mCookwareModifiers").anyOf("\\-&?").zeroOrMore().endGroup().startNamedGroup("mCookwareName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().notAnyOf("\\." + nonWordChar).endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\})").literal("{").startNamedGroup("mCookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").toRegExp();
335
- var singleWordCookware = d().literal("#").startNamedGroup("sCookwareModifiers").anyOf("\\-&?").zeroOrMore().endGroup().startNamedGroup("sCookwareName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().startGroup().literal("{").startNamedGroup("sCookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").endGroup().optional().toRegExp();
336
- var timer = d().literal("~").startNamedGroup("timerName").anyCharacter().zeroOrMore().lazy().endGroup().literal("{").startNamedGroup("timerQuantity").anyCharacter().oneOrMore().lazy().endGroup().startGroup().literal("%").startNamedGroup("timerUnit").anyCharacter().oneOrMore().lazy().endGroup().endGroup().optional().literal("}").toRegExp();
318
+ var cookwareRegex = d().literal("#").startNamedGroup("cookwareModifiers").anyOf("\\-&?").zeroOrMore().endGroup().startGroup().startGroup().startNamedGroup("mCookwareName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\})").endGroup().or().startNamedGroup("sCookwareName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("cookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").endGroup().optional().toRegExp();
319
+ var timerRegex = d().literal("~").startNamedGroup("timerName").anyCharacter().zeroOrMore().lazy().endGroup().literal("{").startNamedGroup("timerQuantity").anyCharacter().oneOrMore().lazy().endGroup().startGroup().literal("%").startNamedGroup("timerUnit").anyCharacter().oneOrMore().lazy().endGroup().endGroup().optional().literal("}").toRegExp();
337
320
  var tokensRegex = new RegExp(
338
321
  [
339
- multiwordIngredient,
340
- singleWordIngredient,
341
- multiwordCookware,
342
- singleWordCookware,
343
- timer
322
+ ingredientWithGroupKeyRegex,
323
+ ingredientWithAlternativeRegex,
324
+ cookwareRegex,
325
+ timerRegex
344
326
  ].map((r2) => r2.source).join("|"),
345
327
  "gu"
346
328
  );
@@ -351,8 +333,7 @@ var rangeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/")
351
333
  var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
352
334
  var floatRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
353
335
 
354
- // src/units.ts
355
- var import_big = __toESM(require("big.js"), 1);
336
+ // src/units/definitions.ts
356
337
  var units = [
357
338
  // Mass (Metric)
358
339
  {
@@ -482,20 +463,19 @@ for (const unit of units) {
482
463
  function normalizeUnit(unit = "") {
483
464
  return unitMap.get(unit.toLowerCase().trim());
484
465
  }
485
- var CannotAddTextValueError = class extends Error {
486
- constructor() {
487
- super("Cannot add a quantity with a text value.");
488
- this.name = "CannotAddTextValueError";
489
- }
490
- };
491
- var IncompatibleUnitsError = class extends Error {
492
- constructor(unit1, unit2) {
493
- super(
494
- `Cannot add quantities with incompatible or unknown units: ${unit1} and ${unit2}`
495
- );
496
- this.name = "IncompatibleUnitsError";
497
- }
498
- };
466
+ var NO_UNIT = "__no-unit__";
467
+ function resolveUnit(name = NO_UNIT, integerProtected = false) {
468
+ const normalizedUnit = normalizeUnit(name);
469
+ const resolvedUnit = normalizedUnit ? { ...normalizedUnit, name } : { name, type: "other", system: "none" };
470
+ return integerProtected ? { ...resolvedUnit, integerProtected: true } : resolvedUnit;
471
+ }
472
+ function isNoUnit(unit) {
473
+ if (!unit) return true;
474
+ return resolveUnit(unit.name).name === NO_UNIT;
475
+ }
476
+
477
+ // src/quantities/numeric.ts
478
+ var import_big = __toESM(require("big.js"), 1);
499
479
  function gcd(a2, b) {
500
480
  return b === 0 ? a2 : gcd(b, a2 % b);
501
481
  }
@@ -511,14 +491,23 @@ function simplifyFraction(num, den) {
511
491
  simplifiedDen = -simplifiedDen;
512
492
  }
513
493
  if (simplifiedDen === 1) {
514
- return { type: "decimal", value: simplifiedNum };
494
+ return { type: "decimal", decimal: simplifiedNum };
515
495
  } else {
516
496
  return { type: "fraction", num: simplifiedNum, den: simplifiedDen };
517
497
  }
518
498
  }
499
+ function getNumericValue(v) {
500
+ if (v.type === "decimal") {
501
+ return v.decimal;
502
+ }
503
+ return v.num / v.den;
504
+ }
519
505
  function multiplyNumericValue(v, factor) {
520
506
  if (v.type === "decimal") {
521
- return { type: "decimal", value: (0, import_big.default)(v.value).times(factor).toNumber() };
507
+ return {
508
+ type: "decimal",
509
+ decimal: (0, import_big.default)(v.decimal).times(factor).toNumber()
510
+ };
522
511
  }
523
512
  return simplifyFraction((0, import_big.default)(v.num).times(factor).toNumber(), v.den);
524
513
  }
@@ -528,36 +517,36 @@ function addNumericValues(val1, val2) {
528
517
  let num2;
529
518
  let den2;
530
519
  if (val1.type === "decimal") {
531
- num1 = val1.value;
520
+ num1 = val1.decimal;
532
521
  den1 = 1;
533
522
  } else {
534
523
  num1 = val1.num;
535
524
  den1 = val1.den;
536
525
  }
537
526
  if (val2.type === "decimal") {
538
- num2 = val2.value;
527
+ num2 = val2.decimal;
539
528
  den2 = 1;
540
529
  } else {
541
530
  num2 = val2.num;
542
531
  den2 = val2.den;
543
532
  }
544
533
  if (num1 === 0 && num2 === 0) {
545
- return { type: "decimal", value: 0 };
534
+ return { type: "decimal", decimal: 0 };
546
535
  }
547
- if (val1.type === "fraction" && val2.type === "fraction" || val1.type === "fraction" && val2.type === "decimal" && val2.value === 0 || val2.type === "fraction" && val1.type === "decimal" && val1.value === 0) {
536
+ if (val1.type === "fraction" && val2.type === "fraction" || val1.type === "fraction" && val2.type === "decimal" && val2.decimal === 0 || val2.type === "fraction" && val1.type === "decimal" && val1.decimal === 0) {
548
537
  const commonDen = den1 * den2;
549
538
  const sumNum = num1 * den2 + num2 * den1;
550
539
  return simplifyFraction(sumNum, commonDen);
551
540
  } else {
552
541
  return {
553
542
  type: "decimal",
554
- value: (0, import_big.default)(num1).div(den1).add((0, import_big.default)(num2).div(den2)).toNumber()
543
+ decimal: (0, import_big.default)(num1).div(den1).add((0, import_big.default)(num2).div(den2)).toNumber()
555
544
  };
556
545
  }
557
546
  }
558
547
  var toRoundedDecimal = (v) => {
559
- const value = v.type === "decimal" ? v.value : v.num / v.den;
560
- return { type: "decimal", value: Math.floor(value * 100) / 100 };
548
+ const value = v.type === "decimal" ? v.decimal : v.num / v.den;
549
+ return { type: "decimal", decimal: Math.round(value * 1e3) / 1e3 };
561
550
  };
562
551
  function multiplyQuantityValue(value, factor) {
563
552
  if (value.type === "fixed") {
@@ -583,13 +572,139 @@ function multiplyQuantityValue(value, factor) {
583
572
  max: multiplyNumericValue(value.max, factor)
584
573
  };
585
574
  }
575
+ function getAverageValue(q) {
576
+ if (q.type === "fixed") {
577
+ return q.value.type === "text" ? q.value.text : getNumericValue(q.value);
578
+ } else {
579
+ return (getNumericValue(q.min) + getNumericValue(q.max)) / 2;
580
+ }
581
+ }
582
+
583
+ // src/errors.ts
584
+ var ReferencedItemCannotBeRedefinedError = class extends Error {
585
+ constructor(item_type, item_name, new_modifier) {
586
+ super(
587
+ `The referenced ${item_type} "${item_name}" cannot be redefined as ${new_modifier}.
588
+ You can either remove the reference to create a new ${item_type} defined as ${new_modifier} or add the ${new_modifier} flag to the original definition of the ${item_type}`
589
+ );
590
+ this.name = "ReferencedItemCannotBeRedefinedError";
591
+ }
592
+ };
593
+ var NoProductCatalogForCartError = class extends Error {
594
+ constructor() {
595
+ super(
596
+ `Cannot build a cart without a product catalog. Please set one using setProductCatalog()`
597
+ );
598
+ this.name = "NoProductCatalogForCartError";
599
+ }
600
+ };
601
+ var NoShoppingListForCartError = class extends Error {
602
+ constructor() {
603
+ super(
604
+ `Cannot build a cart without a shopping list. Please set one using setShoppingList()`
605
+ );
606
+ this.name = "NoShoppingListForCartError";
607
+ }
608
+ };
609
+ var NoProductMatchError = class extends Error {
610
+ constructor(item_name, code) {
611
+ const messageMap = {
612
+ incompatibleUnits: `The units of the products in the catalogue are incompatible with ingredient ${item_name} in the shopping list.`,
613
+ noProduct: "No product was found linked to ingredient name ${item_name} in the shopping list",
614
+ textValue: `Ingredient ${item_name} has a text value as quantity and can therefore not be matched with any product in the catalogue.`,
615
+ noQuantity: `Ingredient ${item_name} has no quantity and can therefore not be matched with any product in the catalogue.`,
616
+ textValue_incompatibleUnits: `Multiple alternative quantities were provided for ingredient ${item_name} in the shopping list but they were either text values or no product in catalog were found to have compatible units`
617
+ };
618
+ super(messageMap[code]);
619
+ __publicField(this, "code");
620
+ this.code = code;
621
+ this.name = "NoProductMatchError";
622
+ }
623
+ };
624
+ var InvalidProductCatalogFormat = class extends Error {
625
+ constructor() {
626
+ super("Invalid product catalog format.");
627
+ this.name = "InvalidProductCatalogFormat";
628
+ }
629
+ };
630
+ var CannotAddTextValueError = class extends Error {
631
+ constructor() {
632
+ super("Cannot add a quantity with a text value.");
633
+ this.name = "CannotAddTextValueError";
634
+ }
635
+ };
636
+ var IncompatibleUnitsError = class extends Error {
637
+ constructor(unit1, unit2) {
638
+ super(
639
+ `Cannot add quantities with incompatible or unknown units: ${unit1} and ${unit2}`
640
+ );
641
+ this.name = "IncompatibleUnitsError";
642
+ }
643
+ };
644
+ var InvalidQuantityFormat = class extends Error {
645
+ constructor(value) {
646
+ super(`Invalid quantity format found in: ${value}`);
647
+ this.name = "InvalidQuantityFormat";
648
+ }
649
+ };
650
+
651
+ // src/utils/type_guards.ts
652
+ function isGroup(x) {
653
+ return x && "type" in x;
654
+ }
655
+ function isOrGroup(x) {
656
+ return isGroup(x) && x.type === "or";
657
+ }
658
+ function isAndGroup(x) {
659
+ return isGroup(x) && x.type === "and";
660
+ }
661
+ function isQuantity(x) {
662
+ return x && typeof x === "object" && "quantity" in x;
663
+ }
664
+ function isNumericValueIntegerLike(v) {
665
+ if (v.type === "decimal") return Number.isInteger(v.decimal);
666
+ return v.num % v.den === 0;
667
+ }
668
+ function isValueIntegerLike(q) {
669
+ if (q.type === "fixed") {
670
+ if (q.value.type === "text") return false;
671
+ return isNumericValueIntegerLike(q.value);
672
+ }
673
+ return isNumericValueIntegerLike(q.min) && isNumericValueIntegerLike(q.max);
674
+ }
675
+
676
+ // src/quantities/mutations.ts
677
+ function extendAllUnits(q) {
678
+ if (isGroup(q)) {
679
+ return { ...q, entries: q.entries.map(extendAllUnits) };
680
+ } else {
681
+ const newQ = {
682
+ quantity: q.quantity
683
+ };
684
+ if (q.unit) {
685
+ newQ.unit = { name: q.unit };
686
+ }
687
+ return newQ;
688
+ }
689
+ }
690
+ function normalizeAllUnits(q) {
691
+ if (isGroup(q)) {
692
+ return { ...q, entries: q.entries.map(normalizeAllUnits) };
693
+ } else {
694
+ const newQ = {
695
+ quantity: q.quantity,
696
+ unit: resolveUnit(q.unit)
697
+ };
698
+ return newQ;
699
+ }
700
+ }
586
701
  var convertQuantityValue = (value, def, targetDef) => {
587
702
  if (def.name === targetDef.name) return value;
588
703
  const factor = def.toBase / targetDef.toBase;
589
704
  return multiplyQuantityValue(value, factor);
590
705
  };
591
706
  function getDefaultQuantityValue() {
592
- return { type: "fixed", value: { type: "decimal", value: 0 } };
707
+ return { type: "fixed", value: { type: "decimal", decimal: 0 } };
593
708
  }
594
709
  function addQuantityValues(v1, v2) {
595
710
  if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
@@ -615,28 +730,31 @@ function addQuantityValues(v1, v2) {
615
730
  return { type: "range", min: newMin, max: newMax };
616
731
  }
617
732
  function addQuantities(q1, q2) {
618
- const v1 = q1.value;
619
- const v2 = q2.value;
733
+ const v1 = q1.quantity;
734
+ const v2 = q2.quantity;
620
735
  if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
621
736
  throw new CannotAddTextValueError();
622
737
  }
623
- const unit1Def = normalizeUnit(q1.unit);
624
- const unit2Def = normalizeUnit(q2.unit);
625
- const addQuantityValuesAndSetUnit = (val1, val2, unit) => ({ value: addQuantityValues(val1, val2), unit });
626
- if ((q1.unit === "" || q1.unit === void 0) && q2.unit !== void 0) {
738
+ const unit1Def = normalizeUnit(q1.unit?.name);
739
+ const unit2Def = normalizeUnit(q2.unit?.name);
740
+ const addQuantityValuesAndSetUnit = (val1, val2, unit) => ({
741
+ quantity: addQuantityValues(val1, val2),
742
+ unit
743
+ });
744
+ if ((q1.unit?.name === "" || q1.unit === void 0) && q2.unit !== void 0) {
627
745
  return addQuantityValuesAndSetUnit(v1, v2, q2.unit);
628
746
  }
629
- if ((q2.unit === "" || q2.unit === void 0) && q1.unit !== void 0) {
747
+ if ((q2.unit?.name === "" || q2.unit === void 0) && q1.unit !== void 0) {
630
748
  return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
631
749
  }
632
- if (!q1.unit && !q2.unit || q1.unit && q2.unit && q1.unit.toLowerCase() === q2.unit.toLowerCase()) {
750
+ if (!q1.unit && !q2.unit || q1.unit && q2.unit && q1.unit.name.toLowerCase() === q2.unit.name.toLowerCase()) {
633
751
  return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
634
752
  }
635
753
  if (unit1Def && unit2Def) {
636
754
  if (unit1Def.type !== unit2Def.type) {
637
755
  throw new IncompatibleUnitsError(
638
- `${unit1Def.type} (${q1.unit})`,
639
- `${unit2Def.type} (${q2.unit})`
756
+ `${unit1Def.type} (${q1.unit?.name})`,
757
+ `${unit2Def.type} (${q2.unit?.name})`
640
758
  );
641
759
  }
642
760
  let targetUnitDef;
@@ -650,27 +768,131 @@ function addQuantities(q1, q2) {
650
768
  }
651
769
  const convertedV1 = convertQuantityValue(v1, unit1Def, targetUnitDef);
652
770
  const convertedV2 = convertQuantityValue(v2, unit2Def, targetUnitDef);
653
- return addQuantityValuesAndSetUnit(
654
- convertedV1,
655
- convertedV2,
656
- targetUnitDef.name
657
- );
771
+ const targetUnit = { name: targetUnitDef.name };
772
+ return addQuantityValuesAndSetUnit(convertedV1, convertedV2, targetUnit);
658
773
  }
659
- throw new IncompatibleUnitsError(q1.unit, q2.unit);
774
+ throw new IncompatibleUnitsError(
775
+ q1.unit?.name,
776
+ q2.unit?.name
777
+ );
660
778
  }
661
-
662
- // src/errors.ts
663
- var ReferencedItemCannotBeRedefinedError = class extends Error {
664
- constructor(item_type, item_name, new_modifier) {
665
- super(
666
- `The referenced ${item_type} "${item_name}" cannot be redefined as ${new_modifier}.
667
- You can either remove the reference to create a new ${item_type} defined as ${new_modifier} or add the ${new_modifier} flag to the original definition of the ${item_type}`
779
+ function toPlainUnit(quantity) {
780
+ if (isQuantity(quantity))
781
+ return quantity.unit ? { ...quantity, unit: quantity.unit.name } : quantity;
782
+ else {
783
+ return {
784
+ ...quantity,
785
+ entries: quantity.entries.map(toPlainUnit)
786
+ };
787
+ }
788
+ }
789
+ function toExtendedUnit(q) {
790
+ if (isQuantity(q)) {
791
+ return q.unit ? { ...q, unit: { name: q.unit } } : q;
792
+ } else {
793
+ return {
794
+ ...q,
795
+ entries: q.entries.map(
796
+ (entry) => isQuantity(entry) ? toExtendedUnit(entry) : toExtendedUnit(entry)
797
+ )
798
+ };
799
+ }
800
+ }
801
+ function deNormalizeQuantity(q) {
802
+ const result = {
803
+ quantity: q.quantity
804
+ };
805
+ if (!isNoUnit(q.unit)) {
806
+ result.unit = { name: q.unit.name };
807
+ }
808
+ return result;
809
+ }
810
+ var flattenPlainUnitGroup = (summed) => {
811
+ if (isOrGroup(summed)) {
812
+ const entries = summed.entries;
813
+ const andGroupEntry = entries.find(
814
+ (e2) => isGroup(e2) && e2.type === "and"
668
815
  );
669
- this.name = "ReferencedItemCannotBeRedefinedError";
816
+ if (andGroupEntry) {
817
+ const andEntries = [];
818
+ for (const entry of andGroupEntry.entries) {
819
+ if (isQuantity(entry)) {
820
+ andEntries.push({
821
+ quantity: entry.quantity,
822
+ unit: entry.unit
823
+ });
824
+ }
825
+ }
826
+ const equivalentsList = entries.filter((e2) => isQuantity(e2)).map((e2) => ({ quantity: e2.quantity, unit: e2.unit }));
827
+ if (equivalentsList.length > 0) {
828
+ return [
829
+ {
830
+ type: "and",
831
+ entries: andEntries,
832
+ equivalents: equivalentsList
833
+ }
834
+ ];
835
+ } else {
836
+ return andEntries.map((entry) => ({ groupQuantity: entry }));
837
+ }
838
+ }
839
+ const simpleEntries = entries.filter(
840
+ (e2) => isQuantity(e2)
841
+ );
842
+ if (simpleEntries.length > 0) {
843
+ const result = {
844
+ quantity: simpleEntries[0].quantity,
845
+ unit: simpleEntries[0].unit
846
+ };
847
+ if (simpleEntries.length > 1) {
848
+ result.equivalents = simpleEntries.slice(1);
849
+ }
850
+ return [{ groupQuantity: result }];
851
+ } else {
852
+ const first = entries[0];
853
+ return [
854
+ { groupQuantity: { quantity: first.quantity, unit: first.unit } }
855
+ ];
856
+ }
857
+ } else if (isGroup(summed)) {
858
+ const andEntries = [];
859
+ const equivalentsList = [];
860
+ for (const entry of summed.entries) {
861
+ if (isOrGroup(entry)) {
862
+ const orEntries = entry.entries.filter(
863
+ (e2) => isQuantity(e2)
864
+ );
865
+ if (orEntries.length > 0) {
866
+ andEntries.push({
867
+ quantity: orEntries[0].quantity,
868
+ unit: orEntries[0].unit
869
+ });
870
+ equivalentsList.push(...orEntries.slice(1));
871
+ }
872
+ } else if (isQuantity(entry)) {
873
+ andEntries.push({
874
+ quantity: entry.quantity,
875
+ unit: entry.unit
876
+ });
877
+ }
878
+ }
879
+ if (equivalentsList.length === 0) {
880
+ return andEntries.map((entry) => ({ groupQuantity: entry }));
881
+ }
882
+ const result = {
883
+ type: "and",
884
+ entries: andEntries,
885
+ equivalents: equivalentsList
886
+ };
887
+ return [result];
888
+ } else {
889
+ return [
890
+ { groupQuantity: { quantity: summed.quantity, unit: summed.unit } }
891
+ ];
670
892
  }
671
893
  };
672
894
 
673
- // src/parser_helpers.ts
895
+ // src/utils/parser_helpers.ts
674
896
  function flushPendingNote(section, note) {
675
897
  if (note.length > 0) {
676
898
  section.content.push({ type: "note", note });
@@ -687,7 +909,7 @@ function flushPendingItems(section, items) {
687
909
  return false;
688
910
  }
689
911
  function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
690
- const { name, quantity, unit } = newIngredient;
912
+ const { name } = newIngredient;
691
913
  if (isReference) {
692
914
  const indexFind = ingredients.findIndex(
693
915
  (i2) => i2.name.toLowerCase() === name.toLowerCase()
@@ -698,52 +920,28 @@ function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
698
920
  );
699
921
  }
700
922
  const existingIngredient = ingredients[indexFind];
701
- for (const flag of newIngredient.flags) {
702
- if (!existingIngredient.flags.includes(flag)) {
923
+ if (!newIngredient.flags) {
924
+ if (Array.isArray(existingIngredient.flags) && existingIngredient.flags.length > 0) {
703
925
  throw new ReferencedItemCannotBeRedefinedError(
704
926
  "ingredient",
705
927
  existingIngredient.name,
706
- flag
928
+ existingIngredient.flags[0]
707
929
  );
708
930
  }
709
- }
710
- let quantityPartIndex = void 0;
711
- if (quantity !== void 0) {
712
- const currentQuantity = {
713
- value: existingIngredient.quantity ?? getDefaultQuantityValue(),
714
- unit: existingIngredient.unit ?? ""
715
- };
716
- const newQuantity = { value: quantity, unit: unit ?? "" };
717
- try {
718
- const total = addQuantities(currentQuantity, newQuantity);
719
- existingIngredient.quantity = total.value;
720
- existingIngredient.unit = total.unit || void 0;
721
- if (existingIngredient.quantityParts) {
722
- existingIngredient.quantityParts.push(
723
- ...newIngredient.quantityParts
931
+ } else {
932
+ for (const flag of newIngredient.flags) {
933
+ if (existingIngredient.flags === void 0 || !existingIngredient.flags.includes(flag)) {
934
+ throw new ReferencedItemCannotBeRedefinedError(
935
+ "ingredient",
936
+ existingIngredient.name,
937
+ flag
724
938
  );
725
- } else {
726
- existingIngredient.quantityParts = newIngredient.quantityParts;
727
- }
728
- quantityPartIndex = existingIngredient.quantityParts.length - 1;
729
- } catch (e2) {
730
- if (e2 instanceof IncompatibleUnitsError || e2 instanceof CannotAddTextValueError) {
731
- return {
732
- ingredientIndex: ingredients.push(newIngredient) - 1,
733
- quantityPartIndex: 0
734
- };
735
939
  }
736
940
  }
737
941
  }
738
- return {
739
- ingredientIndex: indexFind,
740
- quantityPartIndex
741
- };
942
+ return indexFind;
742
943
  }
743
- return {
744
- ingredientIndex: ingredients.push(newIngredient) - 1,
745
- quantityPartIndex: newIngredient.quantity ? 0 : void 0
746
- };
944
+ return ingredients.push(newIngredient) - 1;
747
945
  }
748
946
  function findAndUpsertCookware(cookware, newCookware, isReference) {
749
947
  const { name, quantity } = newCookware;
@@ -757,58 +955,48 @@ function findAndUpsertCookware(cookware, newCookware, isReference) {
757
955
  );
758
956
  }
759
957
  const existingCookware = cookware[index];
760
- for (const flag of newCookware.flags) {
761
- if (!existingCookware.flags.includes(flag)) {
958
+ if (!newCookware.flags) {
959
+ if (Array.isArray(existingCookware.flags) && existingCookware.flags.length > 0) {
762
960
  throw new ReferencedItemCannotBeRedefinedError(
763
961
  "cookware",
764
962
  existingCookware.name,
765
- flag
963
+ existingCookware.flags[0]
766
964
  );
767
965
  }
966
+ } else {
967
+ for (const flag of newCookware.flags) {
968
+ if (existingCookware.flags === void 0 || !existingCookware.flags.includes(flag)) {
969
+ throw new ReferencedItemCannotBeRedefinedError(
970
+ "cookware",
971
+ existingCookware.name,
972
+ flag
973
+ );
974
+ }
975
+ }
768
976
  }
769
- let quantityPartIndex = void 0;
770
977
  if (quantity !== void 0) {
771
978
  if (!existingCookware.quantity) {
772
979
  existingCookware.quantity = quantity;
773
- existingCookware.quantityParts = newCookware.quantityParts;
774
- quantityPartIndex = 0;
775
980
  } else {
776
981
  try {
777
982
  existingCookware.quantity = addQuantityValues(
778
983
  existingCookware.quantity,
779
984
  quantity
780
985
  );
781
- if (!existingCookware.quantityParts) {
782
- existingCookware.quantityParts = newCookware.quantityParts;
783
- quantityPartIndex = 0;
784
- } else {
785
- quantityPartIndex = existingCookware.quantityParts.push(
786
- ...newCookware.quantityParts
787
- ) - 1;
788
- }
789
986
  } catch (e2) {
790
987
  if (e2 instanceof CannotAddTextValueError) {
791
- return {
792
- cookwareIndex: cookware.push(newCookware) - 1,
793
- quantityPartIndex: 0
794
- };
988
+ return cookware.push(newCookware) - 1;
795
989
  }
796
990
  }
797
991
  }
798
992
  }
799
- return {
800
- cookwareIndex: index,
801
- quantityPartIndex
802
- };
993
+ return index;
803
994
  }
804
- return {
805
- cookwareIndex: cookware.push(newCookware) - 1,
806
- quantityPartIndex: quantity ? 0 : void 0
807
- };
995
+ return cookware.push(newCookware) - 1;
808
996
  }
809
997
  var parseFixedValue = (input_str) => {
810
998
  if (!numberLikeRegex.test(input_str)) {
811
- return { type: "text", value: input_str };
999
+ return { type: "text", text: input_str };
812
1000
  }
813
1001
  const s = input_str.trim().replace(",", ".");
814
1002
  if (s.includes("/")) {
@@ -817,8 +1005,22 @@ var parseFixedValue = (input_str) => {
817
1005
  const den = Number(parts[1]);
818
1006
  return { type: "fraction", num, den };
819
1007
  }
820
- return { type: "decimal", value: Number(s) };
1008
+ return { type: "decimal", decimal: Number(s) };
821
1009
  };
1010
+ function stringifyQuantityValue(quantity) {
1011
+ if (quantity.type === "fixed") {
1012
+ return stringifyFixedValue(quantity);
1013
+ } else {
1014
+ return `${stringifyFixedValue({ type: "fixed", value: quantity.min })}-${stringifyFixedValue({ type: "fixed", value: quantity.max })}`;
1015
+ }
1016
+ }
1017
+ function stringifyFixedValue(quantity) {
1018
+ if (quantity.value.type === "fraction")
1019
+ return `${quantity.value.num}/${quantity.value.den}`;
1020
+ else if (quantity.value.type === "decimal")
1021
+ return String(quantity.value.decimal);
1022
+ else return quantity.value.text;
1023
+ }
822
1024
  function parseQuantityInput(input_str) {
823
1025
  const clean_str = String(input_str).trim();
824
1026
  if (rangeRegex.test(clean_str)) {
@@ -860,7 +1062,7 @@ function parseListMetaVar(content, varName) {
860
1062
  function extractMetadata(content) {
861
1063
  const metadata = {};
862
1064
  let servings = void 0;
863
- const metadataContent = content.match(metadataRegex)?.[1];
1065
+ const metadataContent = content.match(metadataRegex)?.[2];
864
1066
  if (!metadataContent) {
865
1067
  return { metadata };
866
1068
  }
@@ -905,74 +1107,1168 @@ function extractMetadata(content) {
905
1107
  }
906
1108
  return { metadata, servings };
907
1109
  }
1110
+ function isPositiveIntegerString(str) {
1111
+ return /^\d+$/.test(str);
1112
+ }
1113
+ function unionOfSets(s1, s2) {
1114
+ const result = new Set(s1);
1115
+ for (const item of s2) {
1116
+ result.add(item);
1117
+ }
1118
+ return result;
1119
+ }
1120
+ function getAlternativeSignature(alternatives) {
1121
+ if (!alternatives || alternatives.length === 0) return null;
1122
+ return alternatives.map((a2) => a2.index).sort((a2, b) => a2 - b).join(",");
1123
+ }
908
1124
 
909
- // src/classes/recipe.ts
910
- var import_big2 = __toESM(require("big.js"), 1);
911
- var Recipe = class _Recipe {
1125
+ // src/classes/product_catalog.ts
1126
+ var ProductCatalog = class {
1127
+ constructor(tomlContent) {
1128
+ __publicField(this, "products", []);
1129
+ if (tomlContent) this.parse(tomlContent);
1130
+ }
912
1131
  /**
913
- * Creates a new Recipe instance.
914
- * @param content - The recipe content to parse.
1132
+ * Parses a TOML string into a list of product options.
1133
+ * @param tomlContent - The TOML string to parse.
1134
+ * @returns A parsed list of `ProductOption`.
915
1135
  */
916
- constructor(content) {
917
- /**
918
- * The parsed recipe metadata.
919
- */
920
- __publicField(this, "metadata", {});
921
- /**
922
- * The parsed recipe ingredients.
923
- */
924
- __publicField(this, "ingredients", []);
925
- /**
926
- * The parsed recipe sections.
927
- */
928
- __publicField(this, "sections", []);
929
- /**
930
- * The parsed recipe cookware.
931
- */
932
- __publicField(this, "cookware", []);
933
- /**
934
- * The parsed recipe timers.
935
- */
936
- __publicField(this, "timers", []);
937
- /**
938
- * The parsed recipe servings. Used for scaling. Parsed from one of
939
- * {@link Metadata.servings}, {@link Metadata.yield} or {@link Metadata.serves}
940
- * metadata fields.
941
- *
942
- * @see {@link Recipe.scaleBy | scaleBy()} and {@link Recipe.scaleTo | scaleTo()} methods
943
- */
944
- __publicField(this, "servings");
945
- if (content) {
946
- this.parse(content);
1136
+ parse(tomlContent) {
1137
+ const catalogRaw = import_smol_toml.default.parse(tomlContent);
1138
+ this.products = [];
1139
+ if (!this.isValidTomlContent(catalogRaw)) {
1140
+ throw new InvalidProductCatalogFormat();
947
1141
  }
1142
+ for (const [ingredientName, ingredientData] of Object.entries(catalogRaw)) {
1143
+ const ingredientTable = ingredientData;
1144
+ const aliases = ingredientTable.aliases;
1145
+ for (const [key, productData] of Object.entries(ingredientTable)) {
1146
+ if (key === "aliases") {
1147
+ continue;
1148
+ }
1149
+ const productId = key;
1150
+ const { name, size, price, ...rest } = productData;
1151
+ const sizeStrings = Array.isArray(size) ? size : [size];
1152
+ const sizes = sizeStrings.map((sizeStr) => {
1153
+ const sizeAndUnitRaw = sizeStr.split("%");
1154
+ const sizeParsed = parseQuantityInput(
1155
+ sizeAndUnitRaw[0]
1156
+ );
1157
+ const productSize = { size: sizeParsed };
1158
+ if (sizeAndUnitRaw.length > 1) {
1159
+ productSize.unit = sizeAndUnitRaw[1];
1160
+ }
1161
+ return productSize;
1162
+ });
1163
+ const productOption = {
1164
+ id: productId,
1165
+ productName: name,
1166
+ ingredientName,
1167
+ price,
1168
+ sizes,
1169
+ ...rest
1170
+ };
1171
+ if (aliases) {
1172
+ productOption.ingredientAliases = aliases;
1173
+ }
1174
+ this.products.push(productOption);
1175
+ }
1176
+ }
1177
+ return this.products;
948
1178
  }
949
1179
  /**
950
- * Parses a recipe from a string.
951
- * @param content - The recipe content to parse.
1180
+ * Stringifies the catalog to a TOML string.
1181
+ * @returns The TOML string representation of the catalog.
952
1182
  */
953
- parse(content) {
954
- const cleanContent = content.replace(metadataRegex, "").replace(commentRegex, "").replace(blockCommentRegex, "").trim().split(/\r\n?|\n/);
955
- const { metadata, servings } = extractMetadata(content);
956
- this.metadata = metadata;
957
- this.servings = servings;
958
- let blankLineBefore = true;
959
- let section = new Section();
960
- const items = [];
961
- let note = "";
962
- let inNote = false;
963
- for (const line of cleanContent) {
964
- if (line.trim().length === 0) {
965
- flushPendingItems(section, items);
966
- note = flushPendingNote(section, note);
967
- blankLineBefore = true;
968
- inNote = false;
969
- continue;
1183
+ stringify() {
1184
+ const grouped = {};
1185
+ for (const product of this.products) {
1186
+ const {
1187
+ id,
1188
+ ingredientName,
1189
+ ingredientAliases,
1190
+ sizes,
1191
+ productName,
1192
+ ...rest
1193
+ } = product;
1194
+ if (!grouped[ingredientName]) {
1195
+ grouped[ingredientName] = {};
970
1196
  }
971
- if (line.startsWith("=")) {
972
- flushPendingItems(section, items);
973
- note = flushPendingNote(section, note);
974
- if (this.sections.length === 0 && section.isBlank()) {
975
- section.name = line.replace(/^=+|=+$/g, "").trim();
1197
+ if (ingredientAliases && !grouped[ingredientName].aliases) {
1198
+ grouped[ingredientName].aliases = ingredientAliases;
1199
+ }
1200
+ const sizeStrings = sizes.map(
1201
+ (s) => s.unit ? `${stringifyQuantityValue(s.size)}%${s.unit}` : stringifyQuantityValue(s.size)
1202
+ );
1203
+ grouped[ingredientName][id] = {
1204
+ ...rest,
1205
+ name: productName,
1206
+ // Use array if multiple sizes, otherwise single string
1207
+ size: sizeStrings.length === 1 ? sizeStrings[0] : sizeStrings
1208
+ };
1209
+ }
1210
+ return import_smol_toml.default.stringify(grouped);
1211
+ }
1212
+ /**
1213
+ * Adds a product to the catalog.
1214
+ * @param productOption - The product to add.
1215
+ */
1216
+ add(productOption) {
1217
+ this.products.push(productOption);
1218
+ }
1219
+ /**
1220
+ * Removes a product from the catalog by its ID.
1221
+ * @param productId - The ID of the product to remove.
1222
+ */
1223
+ remove(productId) {
1224
+ this.products = this.products.filter((product) => product.id !== productId);
1225
+ }
1226
+ isValidTomlContent(catalog) {
1227
+ for (const productsRaw of Object.values(catalog)) {
1228
+ if (typeof productsRaw !== "object" || productsRaw === null) {
1229
+ return false;
1230
+ }
1231
+ for (const [id, obj] of Object.entries(productsRaw)) {
1232
+ if (id === "aliases") {
1233
+ if (!Array.isArray(obj)) {
1234
+ return false;
1235
+ }
1236
+ } else {
1237
+ if (!isPositiveIntegerString(id)) {
1238
+ return false;
1239
+ }
1240
+ if (typeof obj !== "object" || obj === null) {
1241
+ return false;
1242
+ }
1243
+ const record = obj;
1244
+ const keys = Object.keys(record);
1245
+ const mandatoryKeys = ["name", "size", "price"];
1246
+ if (mandatoryKeys.some((key) => !keys.includes(key))) {
1247
+ return false;
1248
+ }
1249
+ const hasProductName = typeof record.name === "string";
1250
+ const hasSize = typeof record.size === "string" || Array.isArray(record.size) && record.size.every((s) => typeof s === "string");
1251
+ const hasPrice = typeof record.price === "number";
1252
+ if (!(hasProductName && hasSize && hasPrice)) {
1253
+ return false;
1254
+ }
1255
+ }
1256
+ }
1257
+ }
1258
+ return true;
1259
+ }
1260
+ };
1261
+
1262
+ // src/classes/section.ts
1263
+ var Section = class {
1264
+ /**
1265
+ * Creates an instance of Section.
1266
+ * @param name - The name of the section. Defaults to an empty string.
1267
+ */
1268
+ constructor(name = "") {
1269
+ /**
1270
+ * The name of the section. Can be an empty string for the default (first) section.
1271
+ * @defaultValue `""`
1272
+ */
1273
+ __publicField(this, "name");
1274
+ /** An array of steps and notes that make up the content of the section. */
1275
+ __publicField(this, "content", []);
1276
+ this.name = name;
1277
+ }
1278
+ /**
1279
+ * Checks if the section is blank (has no name and no content).
1280
+ * Used during recipe parsing
1281
+ * @returns `true` if the section is blank, otherwise `false`.
1282
+ */
1283
+ isBlank() {
1284
+ return this.name === "" && this.content.length === 0;
1285
+ }
1286
+ };
1287
+
1288
+ // src/quantities/alternatives.ts
1289
+ var import_big3 = __toESM(require("big.js"), 1);
1290
+
1291
+ // src/units/conversion.ts
1292
+ var import_big2 = __toESM(require("big.js"), 1);
1293
+ function getUnitRatio(q1, q2) {
1294
+ const q1Value = getAverageValue(q1.quantity);
1295
+ const q2Value = getAverageValue(q2.quantity);
1296
+ const factor = "toBase" in q1.unit && "toBase" in q2.unit ? q1.unit.toBase / q2.unit.toBase : 1;
1297
+ if (typeof q1Value !== "number" || typeof q2Value !== "number") {
1298
+ throw Error(
1299
+ "One of both values is not a number, so a ratio cannot be computed"
1300
+ );
1301
+ }
1302
+ return (0, import_big2.default)(q1Value).times(factor).div(q2Value);
1303
+ }
1304
+ function getBaseUnitRatio(q, qRef) {
1305
+ if ("toBase" in q.unit && "toBase" in qRef.unit) {
1306
+ return q.unit.toBase / qRef.unit.toBase;
1307
+ } else {
1308
+ return 1;
1309
+ }
1310
+ }
1311
+
1312
+ // src/units/lookup.ts
1313
+ function areUnitsCompatible(u1, u2) {
1314
+ if (u1.name === u2.name) {
1315
+ return true;
1316
+ }
1317
+ if (u1.type !== "other" && u1.type === u2.type && u1.system === u2.system) {
1318
+ return true;
1319
+ }
1320
+ return false;
1321
+ }
1322
+ function findListWithCompatibleQuantity(list, quantity) {
1323
+ const quantityWithUnitDef = {
1324
+ ...quantity,
1325
+ unit: resolveUnit(quantity.unit?.name)
1326
+ };
1327
+ return list.find(
1328
+ (l) => l.some((lq) => areUnitsCompatible(lq.unit, quantityWithUnitDef.unit))
1329
+ );
1330
+ }
1331
+ function findCompatibleQuantityWithinList(list, quantity) {
1332
+ const quantityWithUnitDef = {
1333
+ ...quantity,
1334
+ unit: resolveUnit(quantity.unit?.name)
1335
+ };
1336
+ return list.find(
1337
+ (q) => q.unit.name === quantityWithUnitDef.unit.name || q.unit.type === quantityWithUnitDef.unit.type && q.unit.type !== "other"
1338
+ );
1339
+ }
1340
+
1341
+ // src/utils/general.ts
1342
+ var legacyDeepClone = (v) => {
1343
+ if (v === null || typeof v !== "object") {
1344
+ return v;
1345
+ }
1346
+ if (v instanceof Map) {
1347
+ return new Map(
1348
+ Array.from(v.entries()).map(([k, val]) => [
1349
+ legacyDeepClone(k),
1350
+ legacyDeepClone(val)
1351
+ ])
1352
+ );
1353
+ }
1354
+ if (v instanceof Set) {
1355
+ return new Set(Array.from(v).map((val) => legacyDeepClone(val)));
1356
+ }
1357
+ if (v instanceof Date) {
1358
+ return new Date(v.getTime());
1359
+ }
1360
+ if (Array.isArray(v)) {
1361
+ return v.map((item) => legacyDeepClone(item));
1362
+ }
1363
+ const cloned = {};
1364
+ for (const key of Object.keys(v)) {
1365
+ cloned[key] = legacyDeepClone(v[key]);
1366
+ }
1367
+ return cloned;
1368
+ };
1369
+ var deepClone = (v) => typeof structuredClone === "function" ? structuredClone(v) : legacyDeepClone(v);
1370
+
1371
+ // src/quantities/alternatives.ts
1372
+ function getEquivalentUnitsLists(...quantities) {
1373
+ const quantitiesCopy = deepClone(quantities);
1374
+ const OrGroups = quantitiesCopy.filter(isOrGroup).filter((q) => q.entries.length > 1);
1375
+ const unitLists = [];
1376
+ const normalizeOrGroup = (og) => ({
1377
+ ...og,
1378
+ entries: og.entries.map((q) => ({
1379
+ ...q,
1380
+ unit: resolveUnit(q.unit?.name, q.unit?.integerProtected)
1381
+ }))
1382
+ });
1383
+ function findLinkIndexForUnits(lists, unitsToCheck) {
1384
+ return lists.findIndex((l) => {
1385
+ const listItem = l.map((q) => resolveUnit(q.unit?.name));
1386
+ return unitsToCheck.some(
1387
+ (u) => listItem.some(
1388
+ (lu) => lu.name === u?.name || lu.system === u?.system && lu.type === u?.type && lu.type !== "other"
1389
+ )
1390
+ );
1391
+ });
1392
+ }
1393
+ function mergeOrGroupIntoList(lists, idx, og) {
1394
+ let unitRatio;
1395
+ const commonUnitList = lists[idx].reduce((acc, v) => {
1396
+ const normalizedV = {
1397
+ ...v,
1398
+ unit: resolveUnit(v.unit?.name, v.unit?.integerProtected)
1399
+ };
1400
+ const commonQuantity = og.entries.find(
1401
+ (q) => isQuantity(q) && areUnitsCompatible(q.unit, normalizedV.unit)
1402
+ );
1403
+ if (commonQuantity) {
1404
+ acc.push(normalizedV);
1405
+ unitRatio = getUnitRatio(normalizedV, commonQuantity);
1406
+ }
1407
+ return acc;
1408
+ }, []);
1409
+ for (const newQ of og.entries) {
1410
+ if (commonUnitList.some((q) => areUnitsCompatible(q.unit, newQ.unit))) {
1411
+ continue;
1412
+ } else {
1413
+ const scaledQuantity = multiplyQuantityValue(newQ.quantity, unitRatio);
1414
+ lists[idx].push({ ...newQ, quantity: scaledQuantity });
1415
+ }
1416
+ }
1417
+ }
1418
+ for (const orGroup of OrGroups) {
1419
+ const orGroupModified = normalizeOrGroup(orGroup);
1420
+ const units2 = orGroupModified.entries.map((q) => q.unit);
1421
+ const linkIndex = findLinkIndexForUnits(unitLists, units2);
1422
+ if (linkIndex === -1) {
1423
+ unitLists.push(orGroupModified.entries);
1424
+ } else {
1425
+ mergeOrGroupIntoList(unitLists, linkIndex, orGroupModified);
1426
+ }
1427
+ }
1428
+ return unitLists;
1429
+ }
1430
+ function sortUnitList(list) {
1431
+ if (!list || list.length <= 1) return list;
1432
+ const priorityList = [];
1433
+ const nonPriorityList = [];
1434
+ for (const q of list) {
1435
+ if (q.unit.integerProtected || q.unit.system === "none") {
1436
+ priorityList.push(q);
1437
+ } else {
1438
+ nonPriorityList.push(q);
1439
+ }
1440
+ }
1441
+ return priorityList.sort((a2, b) => {
1442
+ const prefixA = a2.unit.integerProtected ? "___" : "";
1443
+ const prefixB = b.unit.integerProtected ? "___" : "";
1444
+ return (prefixA + a2.unit.name).localeCompare(prefixB + b.unit.name, "en");
1445
+ }).concat(nonPriorityList);
1446
+ }
1447
+ function reduceOrsToFirstEquivalent(unitList, quantities) {
1448
+ function reduceToQuantity(firstQuantity) {
1449
+ const equivalentList = sortUnitList(
1450
+ findListWithCompatibleQuantity(unitList, firstQuantity)
1451
+ );
1452
+ if (!equivalentList) return firstQuantity;
1453
+ const firstQuantityInList = findCompatibleQuantityWithinList(
1454
+ equivalentList,
1455
+ firstQuantity
1456
+ );
1457
+ const normalizedFirstQuantity = {
1458
+ ...firstQuantity,
1459
+ unit: resolveUnit(firstQuantity.unit?.name)
1460
+ };
1461
+ if (firstQuantityInList.unit.integerProtected) {
1462
+ const resultQuantity = {
1463
+ quantity: firstQuantity.quantity
1464
+ };
1465
+ if (!isNoUnit(normalizedFirstQuantity.unit)) {
1466
+ resultQuantity.unit = { name: normalizedFirstQuantity.unit.name };
1467
+ }
1468
+ return resultQuantity;
1469
+ } else {
1470
+ let nextProtected;
1471
+ const equivalentListTemp = [...equivalentList];
1472
+ while (nextProtected !== -1) {
1473
+ nextProtected = equivalentListTemp.findIndex(
1474
+ (eq) => eq.unit?.integerProtected
1475
+ );
1476
+ if (nextProtected !== -1) {
1477
+ const unitRatio2 = getUnitRatio(
1478
+ equivalentListTemp[nextProtected],
1479
+ firstQuantityInList
1480
+ );
1481
+ const nextProtectedQuantityValue = multiplyQuantityValue(
1482
+ firstQuantity.quantity,
1483
+ unitRatio2
1484
+ );
1485
+ if (isValueIntegerLike(nextProtectedQuantityValue)) {
1486
+ const nextProtectedQuantity = {
1487
+ quantity: nextProtectedQuantityValue
1488
+ };
1489
+ if (!isNoUnit(equivalentListTemp[nextProtected].unit)) {
1490
+ nextProtectedQuantity.unit = {
1491
+ name: equivalentListTemp[nextProtected].unit.name
1492
+ };
1493
+ }
1494
+ return nextProtectedQuantity;
1495
+ } else {
1496
+ equivalentListTemp.splice(nextProtected, 1);
1497
+ }
1498
+ }
1499
+ }
1500
+ const firstNonIntegerProtected = equivalentListTemp.filter(
1501
+ (q) => !q.unit.integerProtected
1502
+ )[0];
1503
+ const unitRatio = getUnitRatio(
1504
+ firstNonIntegerProtected,
1505
+ firstQuantityInList
1506
+ ).times(getBaseUnitRatio(normalizedFirstQuantity, firstQuantityInList));
1507
+ const firstEqQuantity = {
1508
+ quantity: firstNonIntegerProtected.unit.name === firstQuantity.unit.name ? firstQuantity.quantity : multiplyQuantityValue(firstQuantity.quantity, unitRatio)
1509
+ };
1510
+ if (!isNoUnit(firstNonIntegerProtected.unit)) {
1511
+ firstEqQuantity.unit = { name: firstNonIntegerProtected.unit.name };
1512
+ }
1513
+ return firstEqQuantity;
1514
+ }
1515
+ }
1516
+ return quantities.map((q) => {
1517
+ if (isQuantity(q)) return reduceToQuantity(q);
1518
+ const qListModified = sortUnitList(
1519
+ q.entries.map((qq) => ({
1520
+ ...qq,
1521
+ unit: resolveUnit(qq.unit?.name, qq.unit?.integerProtected)
1522
+ }))
1523
+ );
1524
+ return reduceToQuantity(qListModified[0]);
1525
+ });
1526
+ }
1527
+ function addQuantitiesOrGroups(...quantities) {
1528
+ if (quantities.length === 0)
1529
+ return {
1530
+ sum: {
1531
+ quantity: getDefaultQuantityValue(),
1532
+ unit: resolveUnit()
1533
+ },
1534
+ unitsLists: []
1535
+ };
1536
+ if (quantities.length === 1) {
1537
+ if (isQuantity(quantities[0]))
1538
+ return {
1539
+ sum: {
1540
+ ...quantities[0],
1541
+ unit: resolveUnit(quantities[0].unit?.name)
1542
+ },
1543
+ unitsLists: []
1544
+ };
1545
+ }
1546
+ const unitsLists = getEquivalentUnitsLists(...quantities);
1547
+ const reducedQuantities = reduceOrsToFirstEquivalent(unitsLists, quantities);
1548
+ const sum = [];
1549
+ for (const nextQ of reducedQuantities) {
1550
+ const existingQ = findCompatibleQuantityWithinList(sum, nextQ);
1551
+ if (existingQ === void 0) {
1552
+ sum.push({
1553
+ ...nextQ,
1554
+ unit: resolveUnit(nextQ.unit?.name)
1555
+ });
1556
+ } else {
1557
+ const sumQ = addQuantities(existingQ, nextQ);
1558
+ existingQ.quantity = sumQ.quantity;
1559
+ existingQ.unit = resolveUnit(sumQ.unit?.name);
1560
+ }
1561
+ }
1562
+ if (sum.length === 1) {
1563
+ return { sum: sum[0], unitsLists };
1564
+ }
1565
+ return { sum: { type: "and", entries: sum }, unitsLists };
1566
+ }
1567
+ function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
1568
+ const sumQuantities = isGroup(sum) ? sum.entries : [sum];
1569
+ const result = [];
1570
+ const processedQuantities = /* @__PURE__ */ new Set();
1571
+ for (const list of unitsLists) {
1572
+ const listCopy = deepClone(list);
1573
+ const main = [];
1574
+ const mainCandidates = sumQuantities.filter(
1575
+ (q) => !processedQuantities.has(q)
1576
+ );
1577
+ if (mainCandidates.length === 0) continue;
1578
+ mainCandidates.forEach((q) => {
1579
+ const mainInList = findCompatibleQuantityWithinList(listCopy, q);
1580
+ if (mainInList !== void 0) {
1581
+ processedQuantities.add(q);
1582
+ main.push(q);
1583
+ listCopy.splice(listCopy.indexOf(mainInList), 1);
1584
+ }
1585
+ });
1586
+ const equivalents = sortUnitList(listCopy).map((equiv) => {
1587
+ const initialValue = {
1588
+ quantity: getDefaultQuantityValue()
1589
+ };
1590
+ if (equiv.unit) {
1591
+ initialValue.unit = { name: equiv.unit.name };
1592
+ }
1593
+ return main.reduce((acc, v) => {
1594
+ const mainInList = findCompatibleQuantityWithinList(list, v);
1595
+ const newValue = {
1596
+ quantity: multiplyQuantityValue(
1597
+ v.quantity,
1598
+ (0, import_big3.default)(getAverageValue(equiv.quantity)).div(
1599
+ getAverageValue(mainInList.quantity)
1600
+ )
1601
+ )
1602
+ };
1603
+ if (equiv.unit && !isNoUnit(equiv.unit)) {
1604
+ newValue.unit = { name: equiv.unit.name };
1605
+ }
1606
+ return addQuantities(acc, newValue);
1607
+ }, initialValue);
1608
+ });
1609
+ if (main.length + equivalents.length > 1) {
1610
+ const resultMain = main.length > 1 ? {
1611
+ type: "and",
1612
+ entries: main.map(deNormalizeQuantity)
1613
+ } : deNormalizeQuantity(main[0]);
1614
+ result.push({
1615
+ type: "or",
1616
+ entries: [resultMain, ...equivalents]
1617
+ });
1618
+ } else {
1619
+ result.push(deNormalizeQuantity(main[0]));
1620
+ }
1621
+ }
1622
+ sumQuantities.filter((q) => !processedQuantities.has(q)).forEach((q) => result.push(deNormalizeQuantity(q)));
1623
+ return result;
1624
+ }
1625
+ function addEquivalentsAndSimplify(...quantities) {
1626
+ if (quantities.length === 1) {
1627
+ return toPlainUnit(quantities[0]);
1628
+ }
1629
+ const { sum, unitsLists } = addQuantitiesOrGroups(...quantities);
1630
+ const regrouped = regroupQuantitiesAndExpandEquivalents(sum, unitsLists);
1631
+ if (regrouped.length === 1) {
1632
+ return toPlainUnit(regrouped[0]);
1633
+ } else {
1634
+ return { type: "and", entries: regrouped.map(toPlainUnit) };
1635
+ }
1636
+ }
1637
+
1638
+ // src/classes/recipe.ts
1639
+ var import_big4 = __toESM(require("big.js"), 1);
1640
+ var _Recipe = class _Recipe {
1641
+ /**
1642
+ * Creates a new Recipe instance.
1643
+ * @param content - The recipe content to parse.
1644
+ */
1645
+ constructor(content) {
1646
+ /**
1647
+ * The parsed recipe metadata.
1648
+ */
1649
+ __publicField(this, "metadata", {});
1650
+ /**
1651
+ * The default or manual choice of alternative ingredients.
1652
+ * Contains the full context including alternatives list and active selection index.
1653
+ */
1654
+ __publicField(this, "choices", {
1655
+ ingredientItems: /* @__PURE__ */ new Map(),
1656
+ ingredientGroups: /* @__PURE__ */ new Map()
1657
+ });
1658
+ /**
1659
+ * The parsed recipe ingredients.
1660
+ */
1661
+ __publicField(this, "ingredients", []);
1662
+ /**
1663
+ * The parsed recipe sections.
1664
+ */
1665
+ __publicField(this, "sections", []);
1666
+ /**
1667
+ * The parsed recipe cookware.
1668
+ */
1669
+ __publicField(this, "cookware", []);
1670
+ /**
1671
+ * The parsed recipe timers.
1672
+ */
1673
+ __publicField(this, "timers", []);
1674
+ /**
1675
+ * The parsed recipe servings. Used for scaling. Parsed from one of
1676
+ * {@link Metadata.servings}, {@link Metadata.yield} or {@link Metadata.serves}
1677
+ * metadata fields.
1678
+ *
1679
+ * @see {@link Recipe.scaleBy | scaleBy()} and {@link Recipe.scaleTo | scaleTo()} methods
1680
+ */
1681
+ __publicField(this, "servings");
1682
+ _Recipe.itemCounts.set(this, 0);
1683
+ if (content) {
1684
+ this.parse(content);
1685
+ }
1686
+ }
1687
+ /**
1688
+ * Gets the current item count for this recipe.
1689
+ */
1690
+ getItemCount() {
1691
+ return _Recipe.itemCounts.get(this);
1692
+ }
1693
+ /**
1694
+ * Gets the current item count and increments it.
1695
+ */
1696
+ getAndIncrementItemCount() {
1697
+ const current = this.getItemCount();
1698
+ _Recipe.itemCounts.set(this, current + 1);
1699
+ return current;
1700
+ }
1701
+ _parseQuantityRecursive(quantityRaw) {
1702
+ let quantityMatch = quantityRaw.match(quantityAlternativeRegex);
1703
+ const quantities = [];
1704
+ while (quantityMatch?.groups) {
1705
+ const value = quantityMatch.groups.ingredientQuantityValue ? parseQuantityInput(quantityMatch.groups.ingredientQuantityValue) : void 0;
1706
+ const unit = quantityMatch.groups.ingredientUnit;
1707
+ if (value) {
1708
+ const newQuantity = { quantity: value };
1709
+ if (unit) {
1710
+ if (unit.startsWith("=")) {
1711
+ newQuantity.unit = {
1712
+ name: unit.substring(1),
1713
+ integerProtected: true
1714
+ };
1715
+ } else {
1716
+ newQuantity.unit = { name: unit };
1717
+ }
1718
+ }
1719
+ quantities.push(newQuantity);
1720
+ } else {
1721
+ throw new InvalidQuantityFormat(quantityRaw);
1722
+ }
1723
+ quantityMatch = quantityMatch.groups.ingredientAltQuantity ? quantityMatch.groups.ingredientAltQuantity.match(
1724
+ quantityAlternativeRegex
1725
+ ) : null;
1726
+ }
1727
+ return quantities;
1728
+ }
1729
+ _parseIngredientWithAlternativeRecursive(ingredientMatchString, items) {
1730
+ const alternatives = [];
1731
+ let testString = ingredientMatchString;
1732
+ while (true) {
1733
+ const match = testString.match(
1734
+ alternatives.length > 0 ? inlineIngredientAlternativesRegex : ingredientWithAlternativeRegex
1735
+ );
1736
+ if (!match?.groups) break;
1737
+ const groups = match.groups;
1738
+ let name = groups.mIngredientName || groups.sIngredientName;
1739
+ const preparation = groups.ingredientPreparation;
1740
+ const modifiers = groups.ingredientModifiers;
1741
+ const reference = modifiers !== void 0 && modifiers.includes("&");
1742
+ const flags = [];
1743
+ if (modifiers !== void 0 && modifiers.includes("?")) {
1744
+ flags.push("optional");
1745
+ }
1746
+ if (modifiers !== void 0 && modifiers.includes("-")) {
1747
+ flags.push("hidden");
1748
+ }
1749
+ if (modifiers !== void 0 && modifiers.includes("@") || groups.ingredientRecipeAnchor) {
1750
+ flags.push("recipe");
1751
+ }
1752
+ let extras = void 0;
1753
+ if (flags.includes("recipe")) {
1754
+ extras = { path: `${name}.cook` };
1755
+ name = name.substring(name.lastIndexOf("/") + 1);
1756
+ }
1757
+ const aliasMatch = name.match(ingredientAliasRegex);
1758
+ let listName, displayName;
1759
+ if (aliasMatch && aliasMatch.groups.ingredientListName.trim().length > 0 && aliasMatch.groups.ingredientDisplayName.trim().length > 0) {
1760
+ listName = aliasMatch.groups.ingredientListName.trim();
1761
+ displayName = aliasMatch.groups.ingredientDisplayName.trim();
1762
+ } else {
1763
+ listName = name;
1764
+ displayName = name;
1765
+ }
1766
+ const newIngredient = {
1767
+ name: listName
1768
+ };
1769
+ if (preparation) {
1770
+ newIngredient.preparation = preparation;
1771
+ }
1772
+ if (flags.length > 0) {
1773
+ newIngredient.flags = flags;
1774
+ }
1775
+ if (extras) {
1776
+ newIngredient.extras = extras;
1777
+ }
1778
+ const idxInList = findAndUpsertIngredient(
1779
+ this.ingredients,
1780
+ newIngredient,
1781
+ reference
1782
+ );
1783
+ let itemQuantity = void 0;
1784
+ if (groups.ingredientQuantity) {
1785
+ const parsedQuantities = this._parseQuantityRecursive(
1786
+ groups.ingredientQuantity
1787
+ );
1788
+ const [primary, ...rest] = parsedQuantities;
1789
+ if (primary) {
1790
+ itemQuantity = {
1791
+ ...primary,
1792
+ scalable: groups.ingredientQuantityModifier !== "="
1793
+ };
1794
+ if (rest.length > 0) {
1795
+ itemQuantity.equivalents = rest;
1796
+ }
1797
+ }
1798
+ }
1799
+ const alternative = {
1800
+ index: idxInList,
1801
+ displayName
1802
+ };
1803
+ const note = groups.ingredientNote?.trim();
1804
+ if (note) {
1805
+ alternative.note = note;
1806
+ }
1807
+ if (itemQuantity) {
1808
+ alternative.itemQuantity = itemQuantity;
1809
+ }
1810
+ alternatives.push(alternative);
1811
+ testString = groups.ingredientAlternative || "";
1812
+ }
1813
+ if (alternatives.length > 1) {
1814
+ const alternativesIndexes = alternatives.map((alt) => alt.index);
1815
+ for (const ingredientIndex of alternativesIndexes) {
1816
+ const ingredient = this.ingredients[ingredientIndex];
1817
+ if (ingredient) {
1818
+ if (!ingredient.alternatives) {
1819
+ ingredient.alternatives = new Set(
1820
+ alternativesIndexes.filter((index) => index !== ingredientIndex)
1821
+ );
1822
+ } else {
1823
+ ingredient.alternatives = unionOfSets(
1824
+ ingredient.alternatives,
1825
+ new Set(
1826
+ alternativesIndexes.filter(
1827
+ (index) => index !== ingredientIndex
1828
+ )
1829
+ )
1830
+ );
1831
+ }
1832
+ }
1833
+ }
1834
+ }
1835
+ const id = `ingredient-item-${this.getAndIncrementItemCount()}`;
1836
+ const newItem = {
1837
+ type: "ingredient",
1838
+ id,
1839
+ alternatives
1840
+ };
1841
+ items.push(newItem);
1842
+ if (alternatives.length > 1) {
1843
+ this.choices.ingredientItems.set(id, alternatives);
1844
+ }
1845
+ }
1846
+ _parseIngredientWithGroupKey(ingredientMatchString, items) {
1847
+ const match = ingredientMatchString.match(ingredientWithGroupKeyRegex);
1848
+ if (!match?.groups) return;
1849
+ const groups = match.groups;
1850
+ const groupKey = groups.gIngredientGroupKey;
1851
+ let name = groups.gmIngredientName || groups.gsIngredientName;
1852
+ const preparation = groups.gIngredientPreparation;
1853
+ const modifiers = groups.gIngredientModifiers;
1854
+ const reference = modifiers !== void 0 && modifiers.includes("&");
1855
+ const flags = [];
1856
+ if (modifiers !== void 0 && modifiers.includes("?")) {
1857
+ flags.push("optional");
1858
+ }
1859
+ if (modifiers !== void 0 && modifiers.includes("-")) {
1860
+ flags.push("hidden");
1861
+ }
1862
+ if (modifiers !== void 0 && modifiers.includes("@") || groups.gIngredientRecipeAnchor) {
1863
+ flags.push("recipe");
1864
+ }
1865
+ let extras = void 0;
1866
+ if (flags.includes("recipe")) {
1867
+ extras = { path: `${name}.cook` };
1868
+ name = name.substring(name.lastIndexOf("/") + 1);
1869
+ }
1870
+ const aliasMatch = name.match(ingredientAliasRegex);
1871
+ let listName, displayName;
1872
+ if (aliasMatch && aliasMatch.groups.ingredientListName.trim().length > 0 && aliasMatch.groups.ingredientDisplayName.trim().length > 0) {
1873
+ listName = aliasMatch.groups.ingredientListName.trim();
1874
+ displayName = aliasMatch.groups.ingredientDisplayName.trim();
1875
+ } else {
1876
+ listName = name;
1877
+ displayName = name;
1878
+ }
1879
+ const newIngredient = {
1880
+ name: listName
1881
+ };
1882
+ if (preparation) {
1883
+ newIngredient.preparation = preparation;
1884
+ }
1885
+ if (flags.length > 0) {
1886
+ newIngredient.flags = flags;
1887
+ }
1888
+ if (extras) {
1889
+ newIngredient.extras = extras;
1890
+ }
1891
+ const idxInList = findAndUpsertIngredient(
1892
+ this.ingredients,
1893
+ newIngredient,
1894
+ reference
1895
+ );
1896
+ let itemQuantity = void 0;
1897
+ if (groups.gIngredientQuantity) {
1898
+ const parsedQuantities = this._parseQuantityRecursive(
1899
+ groups.gIngredientQuantity
1900
+ );
1901
+ const [primary, ...rest] = parsedQuantities;
1902
+ itemQuantity = {
1903
+ ...primary,
1904
+ // there's necessarily a primary quantity as the match group was detected
1905
+ scalable: groups.gIngredientQuantityModifier !== "="
1906
+ };
1907
+ if (rest.length > 0) {
1908
+ itemQuantity.equivalents = rest;
1909
+ }
1910
+ }
1911
+ const alternative = {
1912
+ index: idxInList,
1913
+ displayName
1914
+ };
1915
+ if (itemQuantity) {
1916
+ alternative.itemQuantity = itemQuantity;
1917
+ }
1918
+ const existingAlternatives = this.choices.ingredientGroups.get(groupKey);
1919
+ function upsertAlternativeToIngredient(ingredients, ingredientIdx, newAlternativeIdx) {
1920
+ const ingredient = ingredients[ingredientIdx];
1921
+ if (ingredient) {
1922
+ if (ingredient.alternatives === void 0) {
1923
+ ingredient.alternatives = /* @__PURE__ */ new Set([newAlternativeIdx]);
1924
+ } else {
1925
+ ingredient.alternatives.add(newAlternativeIdx);
1926
+ }
1927
+ }
1928
+ }
1929
+ if (existingAlternatives) {
1930
+ for (const alt of existingAlternatives) {
1931
+ upsertAlternativeToIngredient(this.ingredients, alt.index, idxInList);
1932
+ upsertAlternativeToIngredient(this.ingredients, idxInList, alt.index);
1933
+ }
1934
+ }
1935
+ const id = `ingredient-item-${this.getAndIncrementItemCount()}`;
1936
+ const newItem = {
1937
+ type: "ingredient",
1938
+ id,
1939
+ group: groupKey,
1940
+ alternatives: [alternative]
1941
+ };
1942
+ items.push(newItem);
1943
+ const choiceAlternative = deepClone(alternative);
1944
+ choiceAlternative.itemId = id;
1945
+ const existingChoice = this.choices.ingredientGroups.get(groupKey);
1946
+ if (!existingChoice) {
1947
+ this.choices.ingredientGroups.set(groupKey, [choiceAlternative]);
1948
+ } else {
1949
+ existingChoice.push(choiceAlternative);
1950
+ }
1951
+ }
1952
+ /**
1953
+ * Populates the `quantities` property for each ingredient based on
1954
+ * how they appear in the recipe preparation. Only primary ingredients
1955
+ * get quantities populated. Primary ingredients get `usedAsPrimary: true` flag.
1956
+ *
1957
+ * For inline alternatives (e.g. `\@a|b|c`), the first alternative is primary.
1958
+ * For grouped alternatives (e.g. `\@|group|a`, `\@|group|b`), the first item in the group is primary.
1959
+ *
1960
+ * Quantities are grouped by their alternative signature and summed using addEquivalentsAndSimplify.
1961
+ * @internal
1962
+ */
1963
+ _populate_ingredient_quantities() {
1964
+ this.ingredients = this.ingredients.map((ing) => {
1965
+ if (ing.quantities) {
1966
+ delete ing.quantities;
1967
+ }
1968
+ if (ing.usedAsPrimary) {
1969
+ delete ing.usedAsPrimary;
1970
+ }
1971
+ return ing;
1972
+ });
1973
+ const seenGroups = /* @__PURE__ */ new Set();
1974
+ const ingredientGroups = /* @__PURE__ */ new Map();
1975
+ for (const section of this.sections) {
1976
+ for (const step of section.content.filter(
1977
+ (item) => item.type === "step"
1978
+ )) {
1979
+ for (const item of step.items.filter(
1980
+ (item2) => item2.type === "ingredient"
1981
+ )) {
1982
+ const isGroupedItem = "group" in item && item.group !== void 0;
1983
+ const isFirstInGroup = isGroupedItem && !seenGroups.has(item.group);
1984
+ if (isGroupedItem) {
1985
+ seenGroups.add(item.group);
1986
+ }
1987
+ const isPrimary = !isGroupedItem || isFirstInGroup;
1988
+ const alternative = item.alternatives[0];
1989
+ if (isPrimary) {
1990
+ const primaryIngredient = this.ingredients[alternative.index];
1991
+ if (primaryIngredient) {
1992
+ primaryIngredient.usedAsPrimary = true;
1993
+ }
1994
+ }
1995
+ if (!isPrimary || !alternative.itemQuantity) continue;
1996
+ const allQuantities = [
1997
+ {
1998
+ quantity: alternative.itemQuantity.quantity,
1999
+ unit: alternative.itemQuantity.unit
2000
+ }
2001
+ ];
2002
+ if (alternative.itemQuantity.equivalents) {
2003
+ allQuantities.push(...alternative.itemQuantity.equivalents);
2004
+ }
2005
+ const quantityEntry = allQuantities.length === 1 ? allQuantities[0] : { type: "or", entries: allQuantities };
2006
+ const hasInlineAlternatives = item.alternatives.length > 1;
2007
+ const hasGroupedAlternatives = isGroupedItem && this.choices.ingredientGroups.has(item.group);
2008
+ let alternativeRefs;
2009
+ if (hasInlineAlternatives) {
2010
+ alternativeRefs = [];
2011
+ for (let j = 1; j < item.alternatives.length; j++) {
2012
+ const otherAlt = item.alternatives[j];
2013
+ const newRef = {
2014
+ index: otherAlt.index
2015
+ };
2016
+ if (otherAlt.itemQuantity) {
2017
+ const altQty = {
2018
+ quantity: otherAlt.itemQuantity.quantity
2019
+ };
2020
+ if (otherAlt.itemQuantity.unit) {
2021
+ altQty.unit = otherAlt.itemQuantity.unit.name;
2022
+ }
2023
+ if (otherAlt.itemQuantity.equivalents) {
2024
+ altQty.equivalents = otherAlt.itemQuantity.equivalents.map(
2025
+ (eq) => toPlainUnit(eq)
2026
+ );
2027
+ }
2028
+ newRef.alternativeQuantities = [altQty];
2029
+ }
2030
+ alternativeRefs.push(newRef);
2031
+ }
2032
+ } else if (hasGroupedAlternatives) {
2033
+ const groupAlternatives = this.choices.ingredientGroups.get(
2034
+ item.group
2035
+ );
2036
+ alternativeRefs = [];
2037
+ for (let j = 1; j < groupAlternatives.length; j++) {
2038
+ const otherAlt = groupAlternatives[j];
2039
+ if (otherAlt.itemQuantity) {
2040
+ const altQty = {
2041
+ quantity: otherAlt.itemQuantity.quantity
2042
+ };
2043
+ if (otherAlt.itemQuantity.unit) {
2044
+ altQty.unit = otherAlt.itemQuantity.unit.name;
2045
+ }
2046
+ if (otherAlt.itemQuantity.equivalents) {
2047
+ altQty.equivalents = otherAlt.itemQuantity.equivalents.map(
2048
+ (eq) => toPlainUnit(eq)
2049
+ );
2050
+ }
2051
+ alternativeRefs.push({
2052
+ index: otherAlt.index,
2053
+ alternativeQuantities: [altQty]
2054
+ });
2055
+ }
2056
+ }
2057
+ if (alternativeRefs.length === 0) {
2058
+ alternativeRefs = void 0;
2059
+ }
2060
+ }
2061
+ if (!ingredientGroups.has(alternative.index)) {
2062
+ ingredientGroups.set(alternative.index, /* @__PURE__ */ new Map());
2063
+ }
2064
+ const groupsForIngredient = ingredientGroups.get(alternative.index);
2065
+ const baseSignature = getAlternativeSignature(alternativeRefs);
2066
+ const signature = isGroupedItem ? `group:${item.group}|${baseSignature ?? ""}` : baseSignature;
2067
+ if (!groupsForIngredient.has(signature)) {
2068
+ groupsForIngredient.set(signature, {
2069
+ alternativeQuantities: /* @__PURE__ */ new Map(),
2070
+ quantities: []
2071
+ });
2072
+ }
2073
+ const group = groupsForIngredient.get(signature);
2074
+ group.quantities.push(quantityEntry);
2075
+ if (alternativeRefs) {
2076
+ for (const ref of alternativeRefs) {
2077
+ if (!group.alternativeQuantities.has(ref.index)) {
2078
+ group.alternativeQuantities.set(ref.index, []);
2079
+ }
2080
+ if (ref.alternativeQuantities && ref.alternativeQuantities.length > 0) {
2081
+ for (const altQty of ref.alternativeQuantities) {
2082
+ if (altQty.equivalents && altQty.equivalents.length > 0) {
2083
+ const entries = [
2084
+ toExtendedUnit({
2085
+ quantity: altQty.quantity,
2086
+ unit: altQty.unit
2087
+ }),
2088
+ ...altQty.equivalents.map((eq) => toExtendedUnit(eq))
2089
+ ];
2090
+ group.alternativeQuantities.get(ref.index).push({ type: "or", entries });
2091
+ } else {
2092
+ group.alternativeQuantities.get(ref.index).push(
2093
+ toExtendedUnit({
2094
+ quantity: altQty.quantity,
2095
+ unit: altQty.unit
2096
+ })
2097
+ );
2098
+ }
2099
+ }
2100
+ }
2101
+ }
2102
+ }
2103
+ }
2104
+ }
2105
+ }
2106
+ for (const [index, groupsForIngredient] of ingredientGroups) {
2107
+ const ingredient = this.ingredients[index];
2108
+ const quantityGroups = [];
2109
+ for (const [, group] of groupsForIngredient) {
2110
+ const summedGroupQuantity = addEquivalentsAndSimplify(
2111
+ ...group.quantities
2112
+ );
2113
+ const groupQuantities = flattenPlainUnitGroup(summedGroupQuantity);
2114
+ let alternatives;
2115
+ if (group.alternativeQuantities.size > 0) {
2116
+ alternatives = [];
2117
+ for (const [altIndex, altQuantities] of group.alternativeQuantities) {
2118
+ const ref = { index: altIndex };
2119
+ if (altQuantities.length > 0) {
2120
+ const summedAltQuantity = addEquivalentsAndSimplify(
2121
+ ...altQuantities
2122
+ );
2123
+ const flattenedAlt = flattenPlainUnitGroup(summedAltQuantity);
2124
+ ref.alternativeQuantities = flattenedAlt.flatMap((item) => {
2125
+ if ("groupQuantity" in item) {
2126
+ return [item.groupQuantity];
2127
+ } else {
2128
+ return item.entries;
2129
+ }
2130
+ });
2131
+ }
2132
+ alternatives.push(ref);
2133
+ }
2134
+ }
2135
+ for (const gq of groupQuantities) {
2136
+ if ("type" in gq && gq.type === "and") {
2137
+ const andGroup = {
2138
+ type: "and",
2139
+ entries: gq.entries
2140
+ };
2141
+ if (gq.equivalents && gq.equivalents.length > 0) {
2142
+ andGroup.equivalents = gq.equivalents;
2143
+ }
2144
+ if (alternatives && alternatives.length > 0) {
2145
+ andGroup.alternatives = alternatives;
2146
+ }
2147
+ quantityGroups.push(andGroup);
2148
+ } else {
2149
+ const quantityGroup = gq;
2150
+ if (alternatives && alternatives.length > 0) {
2151
+ quantityGroup.alternatives = alternatives;
2152
+ }
2153
+ quantityGroups.push(quantityGroup);
2154
+ }
2155
+ }
2156
+ }
2157
+ if (quantityGroups.length > 0) {
2158
+ ingredient.quantities = quantityGroups;
2159
+ }
2160
+ }
2161
+ }
2162
+ /**
2163
+ * Calculates ingredient quantities based on the provided choices.
2164
+ * Returns a list of computed ingredients with their total quantities.
2165
+ *
2166
+ * @param choices - The recipe choices to apply when computing quantities.
2167
+ * If not provided, uses the default choices (first alternative for each item).
2168
+ * @returns An array of ComputedIngredient with quantityTotal calculated based on choices.
2169
+ */
2170
+ calc_ingredient_quantities(choices) {
2171
+ const effectiveChoices = choices || {
2172
+ ingredientItems: new Map(
2173
+ Array.from(this.choices.ingredientItems.keys()).map((k) => [k, 0])
2174
+ ),
2175
+ ingredientGroups: new Map(
2176
+ Array.from(this.choices.ingredientGroups.keys()).map((k) => [k, 0])
2177
+ )
2178
+ };
2179
+ const ingredientQuantities = /* @__PURE__ */ new Map();
2180
+ const selectedIngredientIndices = /* @__PURE__ */ new Set();
2181
+ for (const section of this.sections) {
2182
+ for (const step of section.content.filter(
2183
+ (item) => item.type === "step"
2184
+ )) {
2185
+ for (const item of step.items.filter(
2186
+ (item2) => item2.type === "ingredient"
2187
+ )) {
2188
+ for (let i2 = 0; i2 < item.alternatives.length; i2++) {
2189
+ const alternative = item.alternatives[i2];
2190
+ const isAlternativeChoiceItem = effectiveChoices.ingredientItems?.get(item.id) === i2;
2191
+ const alternativeChoiceGroupIdx = item.group ? effectiveChoices.ingredientGroups?.get(item.group) : void 0;
2192
+ const alternativeChoiceGroup = item.group ? this.choices.ingredientGroups.get(item.group) : void 0;
2193
+ const isAlternativeChoiceGroup = alternativeChoiceGroup && alternativeChoiceGroupIdx !== void 0 ? alternativeChoiceGroup[alternativeChoiceGroupIdx]?.itemId === item.id : false;
2194
+ const isSelected = !("group" in item) && (item.alternatives.length === 1 || isAlternativeChoiceItem) || isAlternativeChoiceGroup;
2195
+ if (isSelected) {
2196
+ selectedIngredientIndices.add(alternative.index);
2197
+ if (alternative.itemQuantity) {
2198
+ const allQuantities = [
2199
+ {
2200
+ quantity: alternative.itemQuantity.quantity,
2201
+ unit: alternative.itemQuantity.unit
2202
+ }
2203
+ ];
2204
+ if (alternative.itemQuantity.equivalents) {
2205
+ allQuantities.push(...alternative.itemQuantity.equivalents);
2206
+ }
2207
+ const equivalents = allQuantities.length === 1 ? allQuantities[0] : {
2208
+ type: "or",
2209
+ entries: allQuantities
2210
+ };
2211
+ ingredientQuantities.set(alternative.index, [
2212
+ ...ingredientQuantities.get(alternative.index) || [],
2213
+ equivalents
2214
+ ]);
2215
+ }
2216
+ }
2217
+ }
2218
+ }
2219
+ }
2220
+ }
2221
+ const computedIngredients = [];
2222
+ for (let index = 0; index < this.ingredients.length; index++) {
2223
+ if (!selectedIngredientIndices.has(index)) continue;
2224
+ const ing = this.ingredients[index];
2225
+ const computed = {
2226
+ name: ing.name
2227
+ };
2228
+ if (ing.preparation) {
2229
+ computed.preparation = ing.preparation;
2230
+ }
2231
+ if (ing.flags) {
2232
+ computed.flags = ing.flags;
2233
+ }
2234
+ if (ing.extras) {
2235
+ computed.extras = ing.extras;
2236
+ }
2237
+ const quantities = ingredientQuantities.get(index);
2238
+ if (quantities && quantities.length > 0) {
2239
+ computed.quantityTotal = addEquivalentsAndSimplify(...quantities);
2240
+ }
2241
+ computedIngredients.push(computed);
2242
+ }
2243
+ return computedIngredients;
2244
+ }
2245
+ /**
2246
+ * Parses a recipe from a string.
2247
+ * @param content - The recipe content to parse.
2248
+ */
2249
+ parse(content) {
2250
+ const cleanContent = content.replace(metadataRegex, "").replace(commentRegex, "").replace(blockCommentRegex, "").trim().split(/\r\n?|\n/);
2251
+ const { metadata, servings } = extractMetadata(content);
2252
+ this.metadata = metadata;
2253
+ this.servings = servings;
2254
+ let blankLineBefore = true;
2255
+ let section = new Section();
2256
+ const items = [];
2257
+ let note = "";
2258
+ let inNote = false;
2259
+ for (const line of cleanContent) {
2260
+ if (line.trim().length === 0) {
2261
+ flushPendingItems(section, items);
2262
+ note = flushPendingNote(section, note);
2263
+ blankLineBefore = true;
2264
+ inNote = false;
2265
+ continue;
2266
+ }
2267
+ if (line.startsWith("=")) {
2268
+ flushPendingItems(section, items);
2269
+ note = flushPendingNote(section, note);
2270
+ if (this.sections.length === 0 && section.isBlank()) {
2271
+ section.name = line.replace(/^=+|=+$/g, "").trim();
976
2272
  } else {
977
2273
  if (!section.isBlank()) {
978
2274
  this.sections.push(section);
@@ -1009,12 +2305,13 @@ var Recipe = class _Recipe {
1009
2305
  }
1010
2306
  const groups = match.groups;
1011
2307
  if (groups.mIngredientName || groups.sIngredientName) {
1012
- let name = groups.mIngredientName || groups.sIngredientName;
1013
- const scalableQuantity = (groups.mIngredientQuantityModifier || groups.sIngredientQuantityModifier) !== "=";
1014
- const quantityRaw = groups.mIngredientQuantity || groups.sIngredientQuantity;
1015
- const unit = groups.mIngredientUnit || groups.sIngredientUnit;
1016
- const preparation = groups.mIngredientPreparation || groups.sIngredientPreparation;
1017
- const modifiers = groups.mIngredientModifiers || groups.sIngredientModifiers;
2308
+ this._parseIngredientWithAlternativeRecursive(match[0], items);
2309
+ } else if (groups.gmIngredientName || groups.gsIngredientName) {
2310
+ this._parseIngredientWithGroupKey(match[0], items);
2311
+ } else if (groups.mCookwareName || groups.sCookwareName) {
2312
+ const name = groups.mCookwareName || groups.sCookwareName;
2313
+ const modifiers = groups.cookwareModifiers;
2314
+ const quantityRaw = groups.cookwareQuantity;
1018
2315
  const reference = modifiers !== void 0 && modifiers.includes("&");
1019
2316
  const flags = [];
1020
2317
  if (modifiers !== void 0 && modifiers.includes("?")) {
@@ -1023,83 +2320,29 @@ var Recipe = class _Recipe {
1023
2320
  if (modifiers !== void 0 && modifiers.includes("-")) {
1024
2321
  flags.push("hidden");
1025
2322
  }
1026
- if (modifiers !== void 0 && modifiers.includes("@") || groups.mIngredientRecipeAnchor || groups.sIngredientRecipeAnchor) {
1027
- flags.push("recipe");
1028
- }
1029
- let extras = void 0;
1030
- if (flags.includes("recipe")) {
1031
- extras = { path: `${name}.cook` };
1032
- name = name.substring(name.lastIndexOf("/") + 1);
1033
- }
1034
2323
  const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
1035
- const aliasMatch = name.match(ingredientAliasRegex);
1036
- let listName, displayName;
1037
- if (aliasMatch && aliasMatch.groups.ingredientListName.trim().length > 0 && aliasMatch.groups.ingredientDisplayName.trim().length > 0) {
1038
- listName = aliasMatch.groups.ingredientListName.trim();
1039
- displayName = aliasMatch.groups.ingredientDisplayName.trim();
1040
- } else {
1041
- listName = name;
1042
- displayName = name;
1043
- }
1044
- const newIngredient = {
1045
- name: listName,
1046
- quantity,
1047
- quantityParts: quantity ? [
1048
- {
1049
- value: quantity,
1050
- unit,
1051
- scalable: scalableQuantity
1052
- }
1053
- ] : void 0,
1054
- unit,
1055
- preparation,
1056
- flags
2324
+ const newCookware = {
2325
+ name
1057
2326
  };
1058
- if (extras) {
1059
- newIngredient.extras = extras;
2327
+ if (quantity) {
2328
+ newCookware.quantity = quantity;
2329
+ }
2330
+ if (flags.length > 0) {
2331
+ newCookware.flags = flags;
1060
2332
  }
1061
- const idxsInList = findAndUpsertIngredient(
1062
- this.ingredients,
1063
- newIngredient,
2333
+ const idxInList = findAndUpsertCookware(
2334
+ this.cookware,
2335
+ newCookware,
1064
2336
  reference
1065
2337
  );
1066
2338
  const newItem = {
1067
- type: "ingredient",
1068
- index: idxsInList.ingredientIndex,
1069
- displayName
2339
+ type: "cookware",
2340
+ index: idxInList
1070
2341
  };
1071
- if (idxsInList.quantityPartIndex !== void 0) {
1072
- newItem.quantityPartIndex = idxsInList.quantityPartIndex;
2342
+ if (quantity) {
2343
+ newItem.quantity = quantity;
1073
2344
  }
1074
2345
  items.push(newItem);
1075
- } else if (groups.mCookwareName || groups.sCookwareName) {
1076
- const name = groups.mCookwareName || groups.sCookwareName;
1077
- const modifiers = groups.mCookwareModifiers || groups.sCookwareModifiers;
1078
- const quantityRaw = groups.mCookwareQuantity || groups.sCookwareQuantity;
1079
- const reference = modifiers !== void 0 && modifiers.includes("&");
1080
- const flags = [];
1081
- if (modifiers !== void 0 && modifiers.includes("?")) {
1082
- flags.push("optional");
1083
- }
1084
- if (modifiers !== void 0 && modifiers.includes("-")) {
1085
- flags.push("hidden");
1086
- }
1087
- const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
1088
- const idxsInList = findAndUpsertCookware(
1089
- this.cookware,
1090
- {
1091
- name,
1092
- quantity,
1093
- quantityParts: quantity ? [quantity] : void 0,
1094
- flags
1095
- },
1096
- reference
1097
- );
1098
- items.push({
1099
- type: "cookware",
1100
- index: idxsInList.cookwareIndex,
1101
- quantityPartIndex: idxsInList.quantityPartIndex
1102
- });
1103
2346
  } else {
1104
2347
  const durationStr = groups.timerQuantity.trim();
1105
2348
  const unit = (groups.timerUnit || "").trim();
@@ -1127,6 +2370,7 @@ var Recipe = class _Recipe {
1127
2370
  if (!section.isBlank()) {
1128
2371
  this.sections.push(section);
1129
2372
  }
2373
+ this._populate_ingredient_quantities();
1130
2374
  }
1131
2375
  /**
1132
2376
  * Scales the recipe to a new number of servings. In practice, it calls
@@ -1141,7 +2385,7 @@ var Recipe = class _Recipe {
1141
2385
  if (originalServings === void 0 || originalServings === 0) {
1142
2386
  throw new Error("Error scaling recipe: no initial servings value set");
1143
2387
  }
1144
- const factor = (0, import_big2.default)(newServings).div(originalServings);
2388
+ const factor = (0, import_big4.default)(newServings).div(originalServings);
1145
2389
  return this.scaleBy(factor);
1146
2390
  }
1147
2391
  /**
@@ -1156,44 +2400,62 @@ var Recipe = class _Recipe {
1156
2400
  if (originalServings === void 0 || originalServings === 0) {
1157
2401
  throw new Error("Error scaling recipe: no initial servings value set");
1158
2402
  }
1159
- newRecipe.ingredients = newRecipe.ingredients.map((ingredient) => {
1160
- if (ingredient.quantityParts) {
1161
- ingredient.quantityParts = ingredient.quantityParts.map(
1162
- (quantityPart) => {
1163
- if (quantityPart.value.type === "fixed" && quantityPart.value.value.type === "text") {
1164
- return quantityPart;
1165
- }
1166
- return {
1167
- ...quantityPart,
1168
- value: multiplyQuantityValue(
1169
- quantityPart.value,
1170
- quantityPart.scalable ? (0, import_big2.default)(factor) : 1
1171
- )
1172
- };
2403
+ function scaleAlternativesBy(alternatives, factor2) {
2404
+ for (const alternative of alternatives) {
2405
+ if (alternative.itemQuantity) {
2406
+ const scaleFactor = alternative.itemQuantity.scalable ? (0, import_big4.default)(factor2) : 1;
2407
+ if (alternative.itemQuantity.quantity.type !== "fixed" || alternative.itemQuantity.quantity.value.type !== "text") {
2408
+ alternative.itemQuantity.quantity = multiplyQuantityValue(
2409
+ alternative.itemQuantity.quantity,
2410
+ scaleFactor
2411
+ );
2412
+ }
2413
+ if (alternative.itemQuantity.equivalents) {
2414
+ alternative.itemQuantity.equivalents = alternative.itemQuantity.equivalents.map(
2415
+ (altQuantity) => {
2416
+ if (altQuantity.quantity.type === "fixed" && altQuantity.quantity.value.type === "text") {
2417
+ return altQuantity;
2418
+ } else {
2419
+ return {
2420
+ ...altQuantity,
2421
+ quantity: multiplyQuantityValue(
2422
+ altQuantity.quantity,
2423
+ scaleFactor
2424
+ )
2425
+ };
2426
+ }
2427
+ }
2428
+ );
1173
2429
  }
1174
- );
1175
- if (ingredient.quantityParts.length === 1) {
1176
- ingredient.quantity = ingredient.quantityParts[0].value;
1177
- ingredient.unit = ingredient.quantityParts[0].unit;
1178
- } else {
1179
- const totalQuantity = ingredient.quantityParts.reduce(
1180
- (acc, val) => addQuantities(acc, { value: val.value, unit: val.unit }),
1181
- { value: getDefaultQuantityValue() }
1182
- );
1183
- ingredient.quantity = totalQuantity.value;
1184
- ingredient.unit = totalQuantity.unit;
1185
2430
  }
1186
2431
  }
1187
- return ingredient;
1188
- }).filter((ingredient) => ingredient.quantity !== null);
1189
- newRecipe.servings = (0, import_big2.default)(originalServings).times(factor).toNumber();
2432
+ }
2433
+ for (const section of newRecipe.sections) {
2434
+ for (const step of section.content.filter(
2435
+ (item) => item.type === "step"
2436
+ )) {
2437
+ for (const item of step.items.filter(
2438
+ (item2) => item2.type === "ingredient"
2439
+ )) {
2440
+ scaleAlternativesBy(item.alternatives, factor);
2441
+ }
2442
+ }
2443
+ }
2444
+ for (const alternatives of newRecipe.choices.ingredientGroups.values()) {
2445
+ scaleAlternativesBy(alternatives, factor);
2446
+ }
2447
+ for (const alternatives of newRecipe.choices.ingredientItems.values()) {
2448
+ scaleAlternativesBy(alternatives, factor);
2449
+ }
2450
+ newRecipe._populate_ingredient_quantities();
2451
+ newRecipe.servings = (0, import_big4.default)(originalServings).times(factor).toNumber();
1190
2452
  if (newRecipe.metadata.servings && this.metadata.servings) {
1191
2453
  if (floatRegex.test(String(this.metadata.servings).replace(",", ".").trim())) {
1192
2454
  const servingsValue = parseFloat(
1193
2455
  String(this.metadata.servings).replace(",", ".")
1194
2456
  );
1195
2457
  newRecipe.metadata.servings = String(
1196
- (0, import_big2.default)(servingsValue).times(factor).toNumber()
2458
+ (0, import_big4.default)(servingsValue).times(factor).toNumber()
1197
2459
  );
1198
2460
  }
1199
2461
  }
@@ -1203,7 +2465,7 @@ var Recipe = class _Recipe {
1203
2465
  String(this.metadata.yield).replace(",", ".")
1204
2466
  );
1205
2467
  newRecipe.metadata.yield = String(
1206
- (0, import_big2.default)(yieldValue).times(factor).toNumber()
2468
+ (0, import_big4.default)(yieldValue).times(factor).toNumber()
1207
2469
  );
1208
2470
  }
1209
2471
  }
@@ -1213,7 +2475,7 @@ var Recipe = class _Recipe {
1213
2475
  String(this.metadata.serves).replace(",", ".")
1214
2476
  );
1215
2477
  newRecipe.metadata.serves = String(
1216
- (0, import_big2.default)(servesValue).times(factor).toNumber()
2478
+ (0, import_big4.default)(servesValue).times(factor).toNumber()
1217
2479
  );
1218
2480
  }
1219
2481
  }
@@ -1236,19 +2498,27 @@ var Recipe = class _Recipe {
1236
2498
  */
1237
2499
  clone() {
1238
2500
  const newRecipe = new _Recipe();
1239
- newRecipe.metadata = JSON.parse(JSON.stringify(this.metadata));
1240
- newRecipe.ingredients = JSON.parse(
1241
- JSON.stringify(this.ingredients)
1242
- );
1243
- newRecipe.sections = JSON.parse(JSON.stringify(this.sections));
1244
- newRecipe.cookware = JSON.parse(
1245
- JSON.stringify(this.cookware)
1246
- );
1247
- newRecipe.timers = JSON.parse(JSON.stringify(this.timers));
2501
+ newRecipe.choices = deepClone(this.choices);
2502
+ _Recipe.itemCounts.set(newRecipe, this.getItemCount());
2503
+ newRecipe.metadata = deepClone(this.metadata);
2504
+ newRecipe.ingredients = deepClone(this.ingredients);
2505
+ newRecipe.sections = this.sections.map((section) => {
2506
+ const newSection = new Section(section.name);
2507
+ newSection.content = deepClone(section.content);
2508
+ return newSection;
2509
+ });
2510
+ newRecipe.cookware = deepClone(this.cookware);
2511
+ newRecipe.timers = deepClone(this.timers);
1248
2512
  newRecipe.servings = this.servings;
1249
2513
  return newRecipe;
1250
2514
  }
1251
2515
  };
2516
+ /**
2517
+ * External storage for item count (not a property on instances).
2518
+ * Used for giving ID numbers to items during parsing.
2519
+ */
2520
+ __publicField(_Recipe, "itemCounts", /* @__PURE__ */ new WeakMap());
2521
+ var Recipe = _Recipe;
1252
2522
 
1253
2523
  // src/classes/shopping_list.ts
1254
2524
  var ShoppingList = class {
@@ -1257,6 +2527,7 @@ var ShoppingList = class {
1257
2527
  * @param category_config_str - The category configuration to parse.
1258
2528
  */
1259
2529
  constructor(category_config_str) {
2530
+ // TODO: backport type change
1260
2531
  /**
1261
2532
  * The ingredients in the shopping list.
1262
2533
  */
@@ -1279,6 +2550,33 @@ var ShoppingList = class {
1279
2550
  }
1280
2551
  calculate_ingredients() {
1281
2552
  this.ingredients = [];
2553
+ const addIngredientQuantity = (name, quantityTotal) => {
2554
+ const quantityTotalExtended = extendAllUnits(quantityTotal);
2555
+ const newQuantities = isAndGroup(quantityTotalExtended) ? quantityTotalExtended.entries : [quantityTotalExtended];
2556
+ const existing = this.ingredients.find((i2) => i2.name === name);
2557
+ if (existing) {
2558
+ if (!existing.quantityTotal) {
2559
+ existing.quantityTotal = quantityTotal;
2560
+ return;
2561
+ }
2562
+ try {
2563
+ const existingQuantityTotalExtended = extendAllUnits(
2564
+ existing.quantityTotal
2565
+ );
2566
+ const existingQuantities = isAndGroup(existingQuantityTotalExtended) ? existingQuantityTotalExtended.entries : [existingQuantityTotalExtended];
2567
+ existing.quantityTotal = addEquivalentsAndSimplify(
2568
+ ...existingQuantities,
2569
+ ...newQuantities
2570
+ );
2571
+ return;
2572
+ } catch {
2573
+ }
2574
+ }
2575
+ this.ingredients.push({
2576
+ name,
2577
+ quantityTotal
2578
+ });
2579
+ };
1282
2580
  for (const addedRecipe of this.recipes) {
1283
2581
  let scaledRecipe;
1284
2582
  if ("factor" in addedRecipe) {
@@ -1287,62 +2585,47 @@ var ShoppingList = class {
1287
2585
  } else {
1288
2586
  scaledRecipe = addedRecipe.recipe.scaleTo(addedRecipe.servings);
1289
2587
  }
1290
- for (const ingredient of scaledRecipe.ingredients) {
2588
+ const computedIngredients = scaledRecipe.calc_ingredient_quantities(
2589
+ addedRecipe.choices
2590
+ );
2591
+ for (const ingredient of computedIngredients) {
1291
2592
  if (ingredient.flags && ingredient.flags.includes("hidden")) {
1292
2593
  continue;
1293
2594
  }
1294
- const existingIngredient = this.ingredients.find(
1295
- (i2) => i2.name === ingredient.name
1296
- );
1297
- let addSeparate = false;
1298
- try {
1299
- if (existingIngredient && ingredient.quantity) {
1300
- if (existingIngredient.quantity) {
1301
- const newQuantity = addQuantities(
1302
- {
1303
- value: existingIngredient.quantity,
1304
- unit: existingIngredient.unit ?? ""
1305
- },
1306
- {
1307
- value: ingredient.quantity,
1308
- unit: ingredient.unit ?? ""
1309
- }
1310
- );
1311
- existingIngredient.quantity = newQuantity.value;
1312
- if (newQuantity.unit) {
1313
- existingIngredient.unit = newQuantity.unit;
1314
- }
1315
- } else {
1316
- existingIngredient.quantity = ingredient.quantity;
1317
- if (ingredient.unit) {
1318
- existingIngredient.unit = ingredient.unit;
1319
- }
1320
- }
1321
- }
1322
- } catch {
1323
- addSeparate = true;
1324
- }
1325
- if (!existingIngredient || addSeparate) {
1326
- const newIngredient = { name: ingredient.name };
1327
- if (ingredient.quantity) {
1328
- newIngredient.quantity = ingredient.quantity;
1329
- }
1330
- if (ingredient.unit) {
1331
- newIngredient.unit = ingredient.unit;
1332
- }
1333
- this.ingredients.push(newIngredient);
2595
+ if (ingredient.quantityTotal) {
2596
+ addIngredientQuantity(ingredient.name, ingredient.quantityTotal);
2597
+ } else if (!this.ingredients.some((i2) => i2.name === ingredient.name)) {
2598
+ this.ingredients.push({ name: ingredient.name });
1334
2599
  }
1335
2600
  }
1336
2601
  }
1337
2602
  }
1338
- add_recipe(recipe, scaling) {
1339
- if (typeof scaling === "number" || scaling === void 0) {
1340
- this.recipes.push({ recipe, factor: scaling ?? 1 });
2603
+ /**
2604
+ * Adds a recipe to the shopping list, then automatically
2605
+ * recalculates the quantities and recategorize the ingredients.
2606
+ * @param recipe - The recipe to add.
2607
+ * @param options - Options for adding the recipe.
2608
+ */
2609
+ add_recipe(recipe, options = {}) {
2610
+ if (!options.scaling) {
2611
+ this.recipes.push({
2612
+ recipe,
2613
+ factor: options.scaling ?? 1,
2614
+ choices: options.choices
2615
+ });
1341
2616
  } else {
1342
- if ("factor" in scaling) {
1343
- this.recipes.push({ recipe, factor: scaling.factor });
2617
+ if ("factor" in options.scaling) {
2618
+ this.recipes.push({
2619
+ recipe,
2620
+ factor: options.scaling.factor,
2621
+ choices: options.choices
2622
+ });
1344
2623
  } else {
1345
- this.recipes.push({ recipe, servings: scaling.servings });
2624
+ this.recipes.push({
2625
+ recipe,
2626
+ servings: options.scaling.servings,
2627
+ choices: options.choices
2628
+ });
1346
2629
  }
1347
2630
  }
1348
2631
  this.calculate_ingredients();
@@ -1407,15 +2690,276 @@ var ShoppingList = class {
1407
2690
  this.categories = categories;
1408
2691
  }
1409
2692
  };
2693
+
2694
+ // src/classes/shopping_cart.ts
2695
+ var import_yalps = require("yalps");
2696
+ var ShoppingCart = class {
2697
+ /**
2698
+ * Creates a new ShoppingCart instance
2699
+ * @param options - {@link ShoppingCartOptions | Options} for the constructor
2700
+ */
2701
+ constructor(options) {
2702
+ /**
2703
+ * The product catalog to use for matching products
2704
+ */
2705
+ __publicField(this, "productCatalog");
2706
+ /**
2707
+ * The shopping list to build the cart from
2708
+ */
2709
+ __publicField(this, "shoppingList");
2710
+ /**
2711
+ * The content of the cart
2712
+ */
2713
+ __publicField(this, "cart", []);
2714
+ /**
2715
+ * The ingredients that were successfully matched with products
2716
+ */
2717
+ __publicField(this, "match", []);
2718
+ /**
2719
+ * The ingredients that could not be matched with products
2720
+ */
2721
+ __publicField(this, "misMatch", []);
2722
+ /**
2723
+ * Key information about the shopping cart
2724
+ */
2725
+ __publicField(this, "summary");
2726
+ if (options?.catalog) this.productCatalog = options.catalog;
2727
+ if (options?.list) this.shoppingList = options.list;
2728
+ this.summary = { totalPrice: 0, totalItems: 0 };
2729
+ }
2730
+ /**
2731
+ * Sets the product catalog to use for matching products
2732
+ * To use if a catalog was not provided at the creation of the instance
2733
+ * @param catalog - The {@link ProductCatalog} to set
2734
+ */
2735
+ setProductCatalog(catalog) {
2736
+ this.productCatalog = catalog;
2737
+ }
2738
+ // TODO: harmonize recipe name to use underscores
2739
+ /**
2740
+ * Sets the shopping list to build the cart from.
2741
+ * To use if a shopping list was not provided at the creation of the instance
2742
+ * @param list - The {@link ShoppingList} to set
2743
+ */
2744
+ setShoppingList(list) {
2745
+ this.shoppingList = list;
2746
+ }
2747
+ /**
2748
+ * Builds the cart from the shopping list and product catalog
2749
+ * @remarks
2750
+ * - If a combination of product(s) is successfully found for a given ingredient, the latter will be listed in the {@link ShoppingCart.match | match} array
2751
+ * in addition to that combination being added to the {@link ShoppingCart.cart | cart}.
2752
+ * - Otherwise, the latter will be listed in the {@link ShoppingCart.misMatch | misMatch} array. Possible causes can be:
2753
+ * - No product is listed in the catalog for that ingredient
2754
+ * - The ingredient has no quantity, a text quantity
2755
+ * - The ingredient's quantity unit is incompatible with the units of the candidate products listed in the catalog
2756
+ * @throws {@link NoProductCatalogForCartError} if no product catalog is set
2757
+ * @throws {@link NoShoppingListForCartError} if no shopping list is set
2758
+ * @returns `true` if all ingredients in the shopping list have been matched to products in the catalog, or `false` otherwise
2759
+ */
2760
+ buildCart() {
2761
+ this.resetCart();
2762
+ if (this.productCatalog === void 0) {
2763
+ throw new NoProductCatalogForCartError();
2764
+ } else if (this.shoppingList === void 0) {
2765
+ throw new NoShoppingListForCartError();
2766
+ }
2767
+ for (const ingredient of this.shoppingList.ingredients) {
2768
+ const productOptions = this.getProductOptions(ingredient);
2769
+ try {
2770
+ const optimumMatch = this.getOptimumMatch(ingredient, productOptions);
2771
+ this.cart.push(...optimumMatch);
2772
+ this.match.push({ ingredient, selection: optimumMatch });
2773
+ } catch (error) {
2774
+ if (error instanceof NoProductMatchError) {
2775
+ this.misMatch.push({ ingredient, reason: error.code });
2776
+ }
2777
+ }
2778
+ }
2779
+ this.summarize();
2780
+ return this.misMatch.length > 0;
2781
+ }
2782
+ /**
2783
+ * Gets the product options for a given ingredient
2784
+ * @param ingredient - The ingredient to get the product options for
2785
+ * @returns An array of {@link ProductOption}
2786
+ */
2787
+ getProductOptions(ingredient) {
2788
+ return this.productCatalog.products.filter(
2789
+ (product) => product.ingredientName === ingredient.name || product.ingredientAliases?.includes(ingredient.name)
2790
+ );
2791
+ }
2792
+ /**
2793
+ * Gets the optimum match for a given ingredient and product option
2794
+ * @param ingredient - The ingredient to match
2795
+ * @param options - The product options to choose from
2796
+ * @returns An array of {@link ProductSelection}
2797
+ * @throws {@link NoProductMatchError} if no match can be found
2798
+ */
2799
+ getOptimumMatch(ingredient, options) {
2800
+ if (options.length === 0)
2801
+ throw new NoProductMatchError(ingredient.name, "noProduct");
2802
+ if (!ingredient.quantityTotal)
2803
+ throw new NoProductMatchError(ingredient.name, "noQuantity");
2804
+ const normalizedOptions = options.map(
2805
+ (option) => ({
2806
+ ...option,
2807
+ sizes: option.sizes.map((s) => {
2808
+ const resolvedUnit = resolveUnit(s.unit);
2809
+ return {
2810
+ size: resolvedUnit && "toBase" in resolvedUnit ? multiplyQuantityValue(
2811
+ s.size,
2812
+ resolvedUnit.toBase
2813
+ ) : s.size,
2814
+ unit: resolvedUnit
2815
+ };
2816
+ })
2817
+ })
2818
+ );
2819
+ const normalizedQuantityTotal = normalizeAllUnits(ingredient.quantityTotal);
2820
+ function getOptimumMatchForQuantityParts(normalizedQuantities, normalizedOptions2, selection = []) {
2821
+ if (isAndGroup(normalizedQuantities)) {
2822
+ for (const q of normalizedQuantities.entries) {
2823
+ const result = getOptimumMatchForQuantityParts(
2824
+ q,
2825
+ normalizedOptions2,
2826
+ selection
2827
+ );
2828
+ selection.push(...result);
2829
+ }
2830
+ } else {
2831
+ const alternativeUnitsOfQuantity = isOrGroup(normalizedQuantities) ? normalizedQuantities.entries : [normalizedQuantities];
2832
+ const solutions = [];
2833
+ const errors = /* @__PURE__ */ new Set();
2834
+ for (const alternative of alternativeUnitsOfQuantity) {
2835
+ if (alternative.quantity.type === "fixed" && alternative.quantity.value.type === "text") {
2836
+ errors.add("textValue");
2837
+ continue;
2838
+ }
2839
+ const scaledQuantity = multiplyQuantityValue(
2840
+ alternative.quantity,
2841
+ "toBase" in alternative.unit ? alternative.unit.toBase : 1
2842
+ );
2843
+ alternative.quantity = scaledQuantity;
2844
+ const matchOptions = normalizedOptions2.filter(
2845
+ (option) => option.sizes.some(
2846
+ (s) => areUnitsCompatible(alternative.unit, s.unit)
2847
+ )
2848
+ );
2849
+ if (matchOptions.length > 0) {
2850
+ const findCompatibleSize = (option) => option.sizes.find(
2851
+ (s) => areUnitsCompatible(alternative.unit, s.unit)
2852
+ );
2853
+ if (matchOptions.length == 1) {
2854
+ const matchedOption = matchOptions[0];
2855
+ const compatibleSize = findCompatibleSize(matchedOption);
2856
+ const product = options.find(
2857
+ (opt) => opt.id === matchedOption.id
2858
+ );
2859
+ const targetQuantity = scaledQuantity.type === "fixed" ? scaledQuantity.value : scaledQuantity.min;
2860
+ const resQuantity = Math.ceil(
2861
+ getNumericValue(targetQuantity) / getNumericValue(compatibleSize.size.value)
2862
+ );
2863
+ solutions.push([
2864
+ {
2865
+ product,
2866
+ quantity: resQuantity,
2867
+ totalPrice: resQuantity * matchedOption.price
2868
+ }
2869
+ ]);
2870
+ continue;
2871
+ }
2872
+ const model = {
2873
+ direction: "minimize",
2874
+ objective: "price",
2875
+ integers: true,
2876
+ constraints: {
2877
+ size: {
2878
+ min: scaledQuantity.type === "fixed" ? getNumericValue(scaledQuantity.value) : getNumericValue(scaledQuantity.min)
2879
+ }
2880
+ },
2881
+ variables: matchOptions.reduce(
2882
+ (acc, option) => {
2883
+ const compatibleSize = findCompatibleSize(option);
2884
+ acc[option.id] = {
2885
+ price: option.price,
2886
+ size: getNumericValue(compatibleSize.size.value)
2887
+ };
2888
+ return acc;
2889
+ },
2890
+ {}
2891
+ )
2892
+ };
2893
+ const solution = (0, import_yalps.solve)(model);
2894
+ solutions.push(
2895
+ solution.variables.map((variable) => {
2896
+ const resProductSelection = {
2897
+ product: options.find((option) => option.id === variable[0]),
2898
+ quantity: variable[1]
2899
+ };
2900
+ return {
2901
+ ...resProductSelection,
2902
+ totalPrice: resProductSelection.quantity * resProductSelection.product.price
2903
+ };
2904
+ })
2905
+ );
2906
+ } else {
2907
+ errors.add("incompatibleUnits");
2908
+ }
2909
+ }
2910
+ if (solutions.length === 0) {
2911
+ throw new NoProductMatchError(
2912
+ ingredient.name,
2913
+ errors.size === 1 ? errors.values().next().value : "textValue_incompatibleUnits"
2914
+ );
2915
+ } else {
2916
+ return solutions.sort(
2917
+ (a2, b) => a2.reduce((acc, item) => acc + item.totalPrice, 0) - b.reduce((acc, item) => acc + item.totalPrice, 0)
2918
+ )[0];
2919
+ }
2920
+ }
2921
+ return selection;
2922
+ }
2923
+ return getOptimumMatchForQuantityParts(
2924
+ normalizedQuantityTotal,
2925
+ normalizedOptions
2926
+ );
2927
+ }
2928
+ /**
2929
+ * Reset the cart's properties
2930
+ */
2931
+ resetCart() {
2932
+ this.cart = [];
2933
+ this.match = [];
2934
+ this.misMatch = [];
2935
+ this.summary = { totalPrice: 0, totalItems: 0 };
2936
+ }
2937
+ /**
2938
+ * Calculate the cart's key info and store it in the cart's {@link ShoppingCart.summary | summary} property.
2939
+ * This function is automatically invoked by {@link ShoppingCart.buildCart | buildCart() } method.
2940
+ * @returns the total price and number of items in the cart
2941
+ */
2942
+ summarize() {
2943
+ this.summary.totalPrice = this.cart.reduce(
2944
+ (acc, item) => acc + item.totalPrice,
2945
+ 0
2946
+ );
2947
+ this.summary.totalItems = this.cart.length;
2948
+ return this.summary;
2949
+ }
2950
+ };
1410
2951
  // Annotate the CommonJS export names for ESM import in node:
1411
2952
  0 && (module.exports = {
1412
2953
  CategoryConfig,
2954
+ NoProductCatalogForCartError,
2955
+ NoShoppingListForCartError,
2956
+ ProductCatalog,
1413
2957
  Recipe,
1414
2958
  Section,
2959
+ ShoppingCart,
1415
2960
  ShoppingList
1416
2961
  });
1417
2962
  /* v8 ignore else -- @preserve */
1418
- /* v8 ignore else -- expliciting error types -- @preserve */
1419
2963
  /* v8 ignore else -- expliciting error type -- @preserve */
1420
- /* v8 ignore else -- only set unit if it is given -- @preserve */
2964
+ /* v8 ignore if -- @preserve */
1421
2965
  //# sourceMappingURL=index.cjs.map