@tmlmt/cooklang-parser 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -22,23 +22,22 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
22
22
  // src/index.ts
23
23
  var index_exports = {};
24
24
  __export(index_exports, {
25
- AisleConfig: () => AisleConfig,
25
+ CategoryConfig: () => CategoryConfig,
26
26
  Recipe: () => Recipe,
27
27
  Section: () => Section,
28
28
  ShoppingList: () => ShoppingList
29
29
  });
30
30
  module.exports = __toCommonJS(index_exports);
31
31
 
32
- // src/classes/aisle_config.ts
33
- var AisleConfig = class {
32
+ // src/classes/category_config.ts
33
+ var CategoryConfig = class {
34
34
  /**
35
- * Creates a new AisleConfig instance.
36
- * @param config - The aisle configuration to parse.
35
+ * Creates a new CategoryConfig instance.
36
+ * @param config - The category configuration to parse.
37
37
  */
38
38
  constructor(config) {
39
39
  /**
40
- * The categories of aisles.
41
- * @see {@link AisleCategory}
40
+ * The parsed categories of ingredients.
42
41
  */
43
42
  __publicField(this, "categories", []);
44
43
  if (config) {
@@ -46,8 +45,9 @@ var AisleConfig = class {
46
45
  }
47
46
  }
48
47
  /**
49
- * Parses an aisle configuration from a string.
50
- * @param config - The aisle configuration to parse.
48
+ * Parses a category configuration from a string into property
49
+ * {@link CategoryConfig.categories | categories}
50
+ * @param config - The category configuration to parse.
51
51
  */
52
52
  parse(config) {
53
53
  let currentCategory = null;
@@ -97,7 +97,10 @@ var Section = class {
97
97
  * @param name - The name of the section. Defaults to an empty string.
98
98
  */
99
99
  constructor(name = "") {
100
- /** The name of the section. Can be an empty string for the default (first) section. */
100
+ /**
101
+ * The name of the section. Can be an empty string for the default (first) section.
102
+ * @defaultValue `""`
103
+ */
101
104
  __publicField(this, "name");
102
105
  /** An array of steps and notes that make up the content of the section. */
103
106
  __publicField(this, "content", []);
@@ -313,13 +316,14 @@ var i = (() => {
313
316
 
314
317
  // src/regex.ts
315
318
  var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
316
- var nonWordChar = "\\s@#~\\[\\]{(.,;:!?";
317
- var multiwordIngredient = d().literal("@").startNamedGroup("mIngredientModifier").anyOf("@\\-&?").endGroup().optional().startNamedGroup("mIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").startGroup().literal("{").startNamedGroup("mIngredientQuantity").notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("mIngredientUnits").notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("mIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
318
- var singleWordIngredient = d().literal("@").startNamedGroup("sIngredientModifier").anyOf("@\\-&?").endGroup().optional().startNamedGroup("sIngredientName").notAnyOf(nonWordChar).oneOrMore().endGroup().startGroup().literal("{").startNamedGroup("sIngredientQuantity").notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("sIngredientUnits").notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("sIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().toRegExp();
319
+ var 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();
320
+ var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
321
+ 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().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();
322
+ var singleWordIngredient = d().literal("@").startNamedGroup("sIngredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("sIngredientRecipeAnchor").literal("./").endGroup().optional().startNamedGroup("sIngredientName").notAnyOf(nonWordChar).oneOrMore().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();
319
323
  var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
320
- var multiwordCookware = d().literal("#").startNamedGroup("mCookwareModifier").anyOf("\\-&?").endGroup().optional().startNamedGroup("mCookwareName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\})").literal("{").startNamedGroup("mCookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").toRegExp();
321
- var singleWordCookware = d().literal("#").startNamedGroup("sCookwareModifier").anyOf("\\-&?").endGroup().optional().startNamedGroup("sCookwareName").notAnyOf(nonWordChar).oneOrMore().endGroup().startGroup().literal("{").startNamedGroup("sCookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").endGroup().optional().toRegExp();
322
- var timer = d().literal("~").startNamedGroup("timerName").anyCharacter().zeroOrMore().lazy().endGroup().literal("{").startNamedGroup("timerQuantity").anyCharacter().oneOrMore().lazy().endGroup().startGroup().literal("%").startNamedGroup("timerUnits").anyCharacter().oneOrMore().lazy().endGroup().endGroup().optional().literal("}").toRegExp();
324
+ var multiwordCookware = d().literal("#").startNamedGroup("mCookwareModifiers").anyOf("\\-&?").zeroOrMore().endGroup().startNamedGroup("mCookwareName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\})").literal("{").startNamedGroup("mCookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").toRegExp();
325
+ var singleWordCookware = d().literal("#").startNamedGroup("sCookwareModifiers").anyOf("\\-&?").zeroOrMore().endGroup().startNamedGroup("sCookwareName").notAnyOf(nonWordChar).oneOrMore().endGroup().startGroup().literal("{").startNamedGroup("sCookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").endGroup().optional().toRegExp();
326
+ 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();
323
327
  var tokensRegex = new RegExp(
324
328
  [
325
329
  multiwordIngredient,
@@ -333,8 +337,9 @@ var tokensRegex = new RegExp(
333
337
  var commentRegex = d().literal("--").anyCharacter().zeroOrMore().global().toRegExp();
334
338
  var blockCommentRegex = d().whitespace().zeroOrMore().literal("[-").anyCharacter().zeroOrMore().lazy().literal("-]").whitespace().zeroOrMore().global().toRegExp();
335
339
  var shoppingListRegex = d().literal("[").startNamedGroup("name").anyCharacter().oneOrMore().endGroup().literal("]").newline().startNamedGroup("items").anyCharacter().zeroOrMore().lazy().endGroup().startGroup().newline().newline().or().endAnchor().endGroup().global().toRegExp();
336
- var rangeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().literal("-").startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
340
+ var rangeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().literal("-").digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
337
341
  var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
342
+ var floatRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
338
343
 
339
344
  // src/units.ts
340
345
  var units = [
@@ -343,14 +348,14 @@ var units = [
343
348
  name: "g",
344
349
  type: "mass",
345
350
  system: "metric",
346
- aliases: ["gram", "grams"],
351
+ aliases: ["gram", "grams", "grammes"],
347
352
  toBase: 1
348
353
  },
349
354
  {
350
355
  name: "kg",
351
356
  type: "mass",
352
357
  system: "metric",
353
- aliases: ["kilogram", "kilograms"],
358
+ aliases: ["kilogram", "kilograms", "kilogrammes", "kilos", "kilo"],
354
359
  toBase: 1e3
355
360
  },
356
361
  // Mass (Imperial)
@@ -373,7 +378,7 @@ var units = [
373
378
  name: "ml",
374
379
  type: "volume",
375
380
  system: "metric",
376
- aliases: ["milliliter", "milliliters", "millilitre", "millilitres"],
381
+ aliases: ["milliliter", "milliliters", "millilitre", "millilitres", "cc"],
377
382
  toBase: 1
378
383
  },
379
384
  {
@@ -438,7 +443,7 @@ var units = [
438
443
  name: "piece",
439
444
  type: "count",
440
445
  system: "metric",
441
- aliases: ["pieces"],
446
+ aliases: ["pieces", "pc"],
442
447
  toBase: 1
443
448
  }
444
449
  ];
@@ -449,7 +454,7 @@ for (const unit of units) {
449
454
  unitMap.set(alias.toLowerCase(), unit);
450
455
  }
451
456
  }
452
- function normalizeUnit(unit) {
457
+ function normalizeUnit(unit = "") {
453
458
  return unitMap.get(unit.toLowerCase().trim());
454
459
  }
455
460
  var CannotAddTextValueError = class extends Error {
@@ -511,7 +516,10 @@ function addNumericValues(val1, val2) {
511
516
  num2 = val2.num;
512
517
  den2 = val2.den;
513
518
  }
514
- if (val1.type === "fraction" && val2.type === "fraction") {
519
+ if (num1 === 0 && num2 === 0) {
520
+ return { type: "decimal", value: 0 };
521
+ }
522
+ 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) {
515
523
  const commonDen = den1 * den2;
516
524
  const sumNum = num1 * den2 + num2 * den1;
517
525
  return simplifyFraction(sumNum, commonDen);
@@ -525,24 +533,26 @@ var toRoundedDecimal = (v) => {
525
533
  };
526
534
  function multiplyQuantityValue(value, factor) {
527
535
  if (value.type === "fixed") {
536
+ const newValue = multiplyNumericValue(
537
+ value.value,
538
+ factor
539
+ );
540
+ if (factor === parseInt(factor.toString()) || // e.g. 2 === int
541
+ 1 / factor === parseInt((1 / factor).toString())) {
542
+ return {
543
+ type: "fixed",
544
+ value: newValue
545
+ };
546
+ }
528
547
  return {
529
548
  type: "fixed",
530
- value: toRoundedDecimal(
531
- multiplyNumericValue(
532
- value.value,
533
- factor
534
- )
535
- )
549
+ value: toRoundedDecimal(newValue)
536
550
  };
537
551
  }
538
552
  return {
539
553
  type: "range",
540
- min: toRoundedDecimal(
541
- multiplyNumericValue(value.min, factor)
542
- ),
543
- max: toRoundedDecimal(
544
- multiplyNumericValue(value.max, factor)
545
- )
554
+ min: toRoundedDecimal(multiplyNumericValue(value.min, factor)),
555
+ max: toRoundedDecimal(multiplyNumericValue(value.max, factor))
546
556
  };
547
557
  }
548
558
  var convertQuantityValue = (value, def, targetDef) => {
@@ -550,6 +560,32 @@ var convertQuantityValue = (value, def, targetDef) => {
550
560
  const factor = def.toBase / targetDef.toBase;
551
561
  return multiplyQuantityValue(value, factor);
552
562
  };
563
+ function getDefaultQuantityValue() {
564
+ return { type: "fixed", value: { type: "decimal", value: 0 } };
565
+ }
566
+ function addQuantityValues(v1, v2) {
567
+ if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
568
+ throw new CannotAddTextValueError();
569
+ }
570
+ if (v1.type === "fixed" && v2.type === "fixed") {
571
+ const res = addNumericValues(
572
+ v1.value,
573
+ v2.value
574
+ );
575
+ return { type: "fixed", value: res };
576
+ }
577
+ const r1 = v1.type === "range" ? v1 : { type: "range", min: v1.value, max: v1.value };
578
+ const r2 = v2.type === "range" ? v2 : { type: "range", min: v2.value, max: v2.value };
579
+ const newMin = addNumericValues(
580
+ r1.min,
581
+ r2.min
582
+ );
583
+ const newMax = addNumericValues(
584
+ r1.max,
585
+ r2.max
586
+ );
587
+ return { type: "range", min: newMin, max: newMax };
588
+ }
553
589
  function addQuantities(q1, q2) {
554
590
  const v1 = q1.value;
555
591
  const v2 = q2.value;
@@ -558,33 +594,14 @@ function addQuantities(q1, q2) {
558
594
  }
559
595
  const unit1Def = normalizeUnit(q1.unit);
560
596
  const unit2Def = normalizeUnit(q2.unit);
561
- const addQuantityValuesAndSetUnit = (val1, val2, unit) => {
562
- if (val1.type === "fixed" && val2.type === "fixed") {
563
- const res = addNumericValues(
564
- val1.value,
565
- val2.value
566
- );
567
- return { value: { type: "fixed", value: res }, unit };
568
- }
569
- const r1 = val1.type === "range" ? val1 : { type: "range", min: val1.value, max: val1.value };
570
- const r2 = val2.type === "range" ? val2 : { type: "range", min: val2.value, max: val2.value };
571
- const newMin = addNumericValues(
572
- r1.min,
573
- r2.min
574
- );
575
- const newMax = addNumericValues(
576
- r1.max,
577
- r2.max
578
- );
579
- return { value: { type: "range", min: newMin, max: newMax }, unit };
580
- };
581
- if (q1.unit === "" && unit2Def) {
597
+ const addQuantityValuesAndSetUnit = (val1, val2, unit) => ({ value: addQuantityValues(val1, val2), unit });
598
+ if ((q1.unit === "" || q1.unit === void 0) && q2.unit !== void 0) {
582
599
  return addQuantityValuesAndSetUnit(v1, v2, q2.unit);
583
600
  }
584
- if (q2.unit === "" && unit1Def) {
601
+ if ((q2.unit === "" || q2.unit === void 0) && q1.unit !== void 0) {
585
602
  return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
586
603
  }
587
- if (q1.unit.toLowerCase() === q2.unit.toLowerCase()) {
604
+ if (!q1.unit && !q2.unit || q1.unit && q2.unit && q1.unit.toLowerCase() === q2.unit.toLowerCase()) {
588
605
  return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
589
606
  }
590
607
  if (unit1Def && unit2Def) {
@@ -614,14 +631,18 @@ function addQuantities(q1, q2) {
614
631
  throw new IncompatibleUnitsError(q1.unit, q2.unit);
615
632
  }
616
633
 
617
- // src/parser_helpers.ts
618
- function findOrPush(list, finder, creator) {
619
- let index = list.findIndex(finder);
620
- if (index === -1) {
621
- index = list.push(creator()) - 1;
634
+ // src/errors.ts
635
+ var ReferencedItemCannotBeRedefinedError = class extends Error {
636
+ constructor(item_type, item_name, new_modifier) {
637
+ super(
638
+ `The referenced ${item_type} "${item_name}" cannot be redefined as ${new_modifier}.
639
+ 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}`
640
+ );
641
+ this.name = "ReferencedItemCannotBeRedefinedError";
622
642
  }
623
- return index;
624
- }
643
+ };
644
+
645
+ // src/parser_helpers.ts
625
646
  function flushPendingNote(section, note) {
626
647
  if (note.length > 0) {
627
648
  section.content.push({ type: "note", note });
@@ -640,21 +661,28 @@ function flushPendingItems(section, items) {
640
661
  function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
641
662
  const { name, quantity, unit } = newIngredient;
642
663
  if (isReference) {
643
- const index = ingredients.findIndex(
664
+ const indexFind = ingredients.findIndex(
644
665
  (i2) => i2.name.toLowerCase() === name.toLowerCase()
645
666
  );
646
- if (index === -1) {
667
+ if (indexFind === -1) {
647
668
  throw new Error(
648
669
  `Referenced ingredient "${name}" not found. A referenced ingredient must be declared before being referenced with '&'.`
649
670
  );
650
671
  }
651
- const existingIngredient = ingredients[index];
672
+ const existingIngredient = ingredients[indexFind];
673
+ for (const flag of newIngredient.flags) {
674
+ if (!existingIngredient.flags.includes(flag)) {
675
+ throw new ReferencedItemCannotBeRedefinedError(
676
+ "ingredient",
677
+ existingIngredient.name,
678
+ flag
679
+ );
680
+ }
681
+ }
682
+ let quantityPartIndex = void 0;
652
683
  if (quantity !== void 0) {
653
684
  const currentQuantity = {
654
- value: existingIngredient.quantity ?? {
655
- type: "fixed",
656
- value: { type: "decimal", value: 0 }
657
- },
685
+ value: existingIngredient.quantity ?? getDefaultQuantityValue(),
658
686
  unit: existingIngredient.unit ?? ""
659
687
  };
660
688
  const newQuantity = { value: quantity, unit: unit ?? "" };
@@ -662,18 +690,35 @@ function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
662
690
  const total = addQuantities(currentQuantity, newQuantity);
663
691
  existingIngredient.quantity = total.value;
664
692
  existingIngredient.unit = total.unit || void 0;
693
+ if (existingIngredient.quantityParts) {
694
+ existingIngredient.quantityParts.push(
695
+ ...newIngredient.quantityParts
696
+ );
697
+ } else {
698
+ existingIngredient.quantityParts = newIngredient.quantityParts;
699
+ }
700
+ quantityPartIndex = existingIngredient.quantityParts.length - 1;
665
701
  } catch (e2) {
666
702
  if (e2 instanceof IncompatibleUnitsError || e2 instanceof CannotAddTextValueError) {
667
- return ingredients.push(newIngredient) - 1;
703
+ return {
704
+ ingredientIndex: ingredients.push(newIngredient) - 1,
705
+ quantityPartIndex: 0
706
+ };
668
707
  }
669
708
  }
670
709
  }
671
- return index;
710
+ return {
711
+ ingredientIndex: indexFind,
712
+ quantityPartIndex
713
+ };
672
714
  }
673
- return ingredients.push(newIngredient) - 1;
715
+ return {
716
+ ingredientIndex: ingredients.push(newIngredient) - 1,
717
+ quantityPartIndex: newIngredient.quantity ? 0 : void 0
718
+ };
674
719
  }
675
720
  function findAndUpsertCookware(cookware, newCookware, isReference) {
676
- const { name } = newCookware;
721
+ const { name, quantity } = newCookware;
677
722
  if (isReference) {
678
723
  const index = cookware.findIndex(
679
724
  (i2) => i2.name.toLowerCase() === name.toLowerCase()
@@ -683,9 +728,55 @@ function findAndUpsertCookware(cookware, newCookware, isReference) {
683
728
  `Referenced cookware "${name}" not found. A referenced cookware must be declared before being referenced with '&'.`
684
729
  );
685
730
  }
686
- return index;
731
+ const existingCookware = cookware[index];
732
+ for (const flag of newCookware.flags) {
733
+ if (!existingCookware.flags.includes(flag)) {
734
+ throw new ReferencedItemCannotBeRedefinedError(
735
+ "cookware",
736
+ existingCookware.name,
737
+ flag
738
+ );
739
+ }
740
+ }
741
+ let quantityPartIndex = void 0;
742
+ if (quantity !== void 0) {
743
+ if (!existingCookware.quantity) {
744
+ existingCookware.quantity = quantity;
745
+ existingCookware.quantityParts = newCookware.quantityParts;
746
+ quantityPartIndex = 0;
747
+ } else {
748
+ try {
749
+ existingCookware.quantity = addQuantityValues(
750
+ existingCookware.quantity,
751
+ quantity
752
+ );
753
+ if (!existingCookware.quantityParts) {
754
+ existingCookware.quantityParts = newCookware.quantityParts;
755
+ quantityPartIndex = 0;
756
+ } else {
757
+ quantityPartIndex = existingCookware.quantityParts.push(
758
+ ...newCookware.quantityParts
759
+ ) - 1;
760
+ }
761
+ } catch (e2) {
762
+ if (e2 instanceof CannotAddTextValueError) {
763
+ return {
764
+ cookwareIndex: cookware.push(newCookware) - 1,
765
+ quantityPartIndex: 0
766
+ };
767
+ }
768
+ }
769
+ }
770
+ }
771
+ return {
772
+ cookwareIndex: index,
773
+ quantityPartIndex
774
+ };
687
775
  }
688
- return cookware.push(newCookware) - 1;
776
+ return {
777
+ cookwareIndex: cookware.push(newCookware) - 1,
778
+ quantityPartIndex: quantity ? 0 : void 0
779
+ };
689
780
  }
690
781
  var parseFixedValue = (input_str) => {
691
782
  if (!numberLikeRegex.test(input_str)) {
@@ -717,14 +808,12 @@ function parseSimpleMetaVar(content, varName) {
717
808
  return varMatch ? varMatch[1]?.trim().replace(/\s*\r?\n\s+/g, " ") : void 0;
718
809
  }
719
810
  function parseScalingMetaVar(content, varName) {
720
- const varMatch = content.match(
721
- new RegExp(`^${varName}:[\\t ]*(([^,\\n]*),? ?(?:.*)?)`, "m")
722
- );
811
+ const varMatch = content.match(scalingMetaValueRegex(varName));
723
812
  if (!varMatch) return void 0;
724
813
  if (isNaN(Number(varMatch[2]?.trim()))) {
725
814
  throw new Error("Scaling variables should be numbers");
726
815
  }
727
- return [Number(varMatch[2]?.trim()), varMatch[1]?.trim()];
816
+ return [Number(varMatch[2]?.trim()), varMatch[1].trim()];
728
817
  }
729
818
  function parseListMetaVar(content, varName) {
730
819
  const listMatch = content.match(
@@ -775,7 +864,7 @@ function extractMetadata(content) {
775
864
  const stringMetaValue = parseSimpleMetaVar(metadataContent, metaVar);
776
865
  if (stringMetaValue) metadata[metaVar] = stringMetaValue;
777
866
  }
778
- for (const metaVar of ["servings", "yield", "serves"]) {
867
+ for (const metaVar of ["serves", "yield", "servings"]) {
779
868
  const scalingMetaValue = parseScalingMetaVar(metadataContent, metaVar);
780
869
  if (scalingMetaValue && scalingMetaValue[1]) {
781
870
  metadata[metaVar] = scalingMetaValue[1];
@@ -797,32 +886,31 @@ var Recipe = class _Recipe {
797
886
  */
798
887
  constructor(content) {
799
888
  /**
800
- * The recipe's metadata.
801
- * @see {@link Metadata}
889
+ * The parsed recipe metadata.
802
890
  */
803
891
  __publicField(this, "metadata", {});
804
892
  /**
805
- * The recipe's ingredients.
806
- * @see {@link Ingredient}
893
+ * The parsed recipe ingredients.
807
894
  */
808
895
  __publicField(this, "ingredients", []);
809
896
  /**
810
- * The recipe's sections.
811
- * @see {@link Section}
897
+ * The parsed recipe sections.
812
898
  */
813
899
  __publicField(this, "sections", []);
814
900
  /**
815
- * The recipe's cookware.
816
- * @see {@link Cookware}
901
+ * The parsed recipe cookware.
817
902
  */
818
903
  __publicField(this, "cookware", []);
819
904
  /**
820
- * The recipe's timers.
821
- * @see {@link Timer}
905
+ * The parsed recipe timers.
822
906
  */
823
907
  __publicField(this, "timers", []);
824
908
  /**
825
- * The recipe's servings. Used for scaling
909
+ * The parsed recipe servings. Used for scaling. Parsed from one of
910
+ * {@link Metadata.servings}, {@link Metadata.yield} or {@link Metadata.serves}
911
+ * metadata fields.
912
+ *
913
+ * @see {@link Recipe.scaleBy | scaleBy()} and {@link Recipe.scaleTo | scaleTo()} methods
826
914
  */
827
915
  __publicField(this, "servings");
828
916
  if (content) {
@@ -892,15 +980,28 @@ var Recipe = class _Recipe {
892
980
  }
893
981
  const groups = match.groups;
894
982
  if (groups.mIngredientName || groups.sIngredientName) {
895
- const name = groups.mIngredientName || groups.sIngredientName;
983
+ let name = groups.mIngredientName || groups.sIngredientName;
984
+ const scalableQuantity = (groups.mIngredientQuantityModifier || groups.sIngredientQuantityModifier) !== "=";
896
985
  const quantityRaw = groups.mIngredientQuantity || groups.sIngredientQuantity;
897
- const units2 = groups.mIngredientUnits || groups.sIngredientUnits;
986
+ const unit = groups.mIngredientUnit || groups.sIngredientUnit;
898
987
  const preparation = groups.mIngredientPreparation || groups.sIngredientPreparation;
899
- const modifier = groups.mIngredientModifier || groups.sIngredientModifier;
900
- const optional = modifier === "?";
901
- const hidden = modifier === "-";
902
- const reference = modifier === "&";
903
- const isRecipe = modifier === "@";
988
+ const modifiers = groups.mIngredientModifiers || groups.sIngredientModifiers;
989
+ const reference = modifiers !== void 0 && modifiers.includes("&");
990
+ const flags = [];
991
+ if (modifiers !== void 0 && modifiers.includes("?")) {
992
+ flags.push("optional");
993
+ }
994
+ if (modifiers !== void 0 && modifiers.includes("-")) {
995
+ flags.push("hidden");
996
+ }
997
+ if (modifiers !== void 0 && modifiers.includes("@") || groups.mIngredientRecipeAnchor || groups.sIngredientRecipeAnchor) {
998
+ flags.push("recipe");
999
+ }
1000
+ let extras = void 0;
1001
+ if (flags.includes("recipe")) {
1002
+ extras = { path: `${name}.cook` };
1003
+ name = name.substring(name.lastIndexOf("/") + 1);
1004
+ }
904
1005
  const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
905
1006
  const aliasMatch = name.match(ingredientAliasRegex);
906
1007
  let listName, displayName;
@@ -911,50 +1012,70 @@ var Recipe = class _Recipe {
911
1012
  listName = name;
912
1013
  displayName = name;
913
1014
  }
914
- const idxInList = findAndUpsertIngredient(
1015
+ const newIngredient = {
1016
+ name: listName,
1017
+ quantity,
1018
+ quantityParts: quantity ? [
1019
+ {
1020
+ value: quantity,
1021
+ unit,
1022
+ scalable: scalableQuantity
1023
+ }
1024
+ ] : void 0,
1025
+ unit,
1026
+ preparation,
1027
+ flags
1028
+ };
1029
+ if (extras) {
1030
+ newIngredient.extras = extras;
1031
+ }
1032
+ const idxsInList = findAndUpsertIngredient(
915
1033
  this.ingredients,
916
- {
917
- name: listName,
918
- quantity,
919
- unit: units2,
920
- optional,
921
- hidden,
922
- preparation,
923
- isRecipe
924
- },
1034
+ newIngredient,
925
1035
  reference
926
1036
  );
927
1037
  const newItem = {
928
1038
  type: "ingredient",
929
- value: idxInList,
930
- itemQuantity: quantity,
931
- itemUnit: units2,
1039
+ index: idxsInList.ingredientIndex,
932
1040
  displayName
933
1041
  };
1042
+ if (idxsInList.quantityPartIndex !== void 0) {
1043
+ newItem.quantityPartIndex = idxsInList.quantityPartIndex;
1044
+ }
934
1045
  items.push(newItem);
935
1046
  } else if (groups.mCookwareName || groups.sCookwareName) {
936
1047
  const name = groups.mCookwareName || groups.sCookwareName;
937
- const modifier = groups.mCookwareModifier || groups.sCookwareModifier;
1048
+ const modifiers = groups.mCookwareModifiers || groups.sCookwareModifiers;
938
1049
  const quantityRaw = groups.mCookwareQuantity || groups.sCookwareQuantity;
939
- const optional = modifier === "?";
940
- const hidden = modifier === "-";
941
- const reference = modifier === "&";
1050
+ const reference = modifiers !== void 0 && modifiers.includes("&");
1051
+ const flags = [];
1052
+ if (modifiers !== void 0 && modifiers.includes("?")) {
1053
+ flags.push("optional");
1054
+ }
1055
+ if (modifiers !== void 0 && modifiers.includes("-")) {
1056
+ flags.push("hidden");
1057
+ }
942
1058
  const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
943
- const idxInList = findAndUpsertCookware(
1059
+ const idxsInList = findAndUpsertCookware(
944
1060
  this.cookware,
945
- { name, quantity, optional, hidden },
1061
+ {
1062
+ name,
1063
+ quantity,
1064
+ quantityParts: quantity ? [quantity] : void 0,
1065
+ flags
1066
+ },
946
1067
  reference
947
1068
  );
948
1069
  items.push({
949
1070
  type: "cookware",
950
- value: idxInList,
951
- itemQuantity: quantity
1071
+ index: idxsInList.cookwareIndex,
1072
+ quantityPartIndex: idxsInList.quantityPartIndex
952
1073
  });
953
- } else if (groups.timerQuantity !== void 0) {
1074
+ } else {
954
1075
  const durationStr = groups.timerQuantity.trim();
955
- const unit = (groups.timerUnits || "").trim();
1076
+ const unit = (groups.timerUnit || "").trim();
956
1077
  if (!unit) {
957
- throw new Error("Timer missing units");
1078
+ throw new Error("Timer missing unit");
958
1079
  }
959
1080
  const name = groups.timerName || void 0;
960
1081
  const duration = parseQuantityInput(durationStr);
@@ -963,12 +1084,7 @@ var Recipe = class _Recipe {
963
1084
  duration,
964
1085
  unit
965
1086
  };
966
- const idxInList = findOrPush(
967
- this.timers,
968
- (t2) => t2.name === timerObj.name && t2.duration === timerObj.duration && t2.unit === timerObj.unit,
969
- () => timerObj
970
- );
971
- items.push({ type: "timer", value: idxInList });
1087
+ items.push({ type: "timer", index: this.timers.push(timerObj) - 1 });
972
1088
  }
973
1089
  cursor = idx + match[0].length;
974
1090
  }
@@ -984,9 +1100,12 @@ var Recipe = class _Recipe {
984
1100
  }
985
1101
  }
986
1102
  /**
987
- * Scales the recipe to a new number of servings.
1103
+ * Scales the recipe to a new number of servings. In practice, it calls
1104
+ * {@link Recipe.scaleBy | scaleBy} with a factor corresponding to the ratio between `newServings`
1105
+ * and the recipe's {@link Recipe.servings | servings} value.
988
1106
  * @param newServings - The new number of servings.
989
1107
  * @returns A new Recipe instance with the scaled ingredients.
1108
+ * @throws `Error` if the recipe does not contains an initial {@link Recipe.servings | servings} value
990
1109
  */
991
1110
  scaleTo(newServings) {
992
1111
  const originalServings = this.getServings();
@@ -1008,30 +1127,57 @@ var Recipe = class _Recipe {
1008
1127
  throw new Error("Error scaling recipe: no initial servings value set");
1009
1128
  }
1010
1129
  newRecipe.ingredients = newRecipe.ingredients.map((ingredient) => {
1011
- if (ingredient.quantity && !(ingredient.quantity.type === "fixed" && ingredient.quantity.value.type === "text")) {
1012
- ingredient.quantity = multiplyQuantityValue(
1013
- ingredient.quantity,
1014
- factor
1130
+ if (ingredient.quantityParts) {
1131
+ ingredient.quantityParts = ingredient.quantityParts.map(
1132
+ (quantityPart) => {
1133
+ if (quantityPart.value.type === "fixed" && quantityPart.value.value.type === "text") {
1134
+ return quantityPart;
1135
+ }
1136
+ return {
1137
+ ...quantityPart,
1138
+ value: multiplyQuantityValue(
1139
+ quantityPart.value,
1140
+ quantityPart.scalable ? factor : 1
1141
+ )
1142
+ };
1143
+ }
1015
1144
  );
1145
+ if (ingredient.quantityParts.length === 1) {
1146
+ ingredient.quantity = ingredient.quantityParts[0].value;
1147
+ ingredient.unit = ingredient.quantityParts[0].unit;
1148
+ } else {
1149
+ const totalQuantity = ingredient.quantityParts.reduce(
1150
+ (acc, val) => addQuantities(acc, { value: val.value, unit: val.unit }),
1151
+ { value: getDefaultQuantityValue() }
1152
+ );
1153
+ ingredient.quantity = totalQuantity.value;
1154
+ ingredient.unit = totalQuantity.unit;
1155
+ }
1016
1156
  }
1017
1157
  return ingredient;
1018
1158
  }).filter((ingredient) => ingredient.quantity !== null);
1019
1159
  newRecipe.servings = originalServings * factor;
1020
1160
  if (newRecipe.metadata.servings && this.metadata.servings) {
1021
- const servingsValue = parseFloat(this.metadata.servings);
1022
- if (!isNaN(servingsValue)) {
1161
+ if (floatRegex.test(String(this.metadata.servings).replace(",", ".").trim())) {
1162
+ const servingsValue = parseFloat(
1163
+ String(this.metadata.servings).replace(",", ".")
1164
+ );
1023
1165
  newRecipe.metadata.servings = String(servingsValue * factor);
1024
1166
  }
1025
1167
  }
1026
1168
  if (newRecipe.metadata.yield && this.metadata.yield) {
1027
- const yieldValue = parseFloat(this.metadata.yield);
1028
- if (!isNaN(yieldValue)) {
1169
+ if (floatRegex.test(String(this.metadata.yield).replace(",", ".").trim())) {
1170
+ const yieldValue = parseFloat(
1171
+ String(this.metadata.yield).replace(",", ".")
1172
+ );
1029
1173
  newRecipe.metadata.yield = String(yieldValue * factor);
1030
1174
  }
1031
1175
  }
1032
1176
  if (newRecipe.metadata.serves && this.metadata.serves) {
1033
- const servesValue = parseFloat(this.metadata.serves);
1034
- if (!isNaN(servesValue)) {
1177
+ if (floatRegex.test(String(this.metadata.serves).replace(",", ".").trim())) {
1178
+ const servesValue = parseFloat(
1179
+ String(this.metadata.serves).replace(",", ".")
1180
+ );
1035
1181
  newRecipe.metadata.serves = String(servesValue * factor);
1036
1182
  }
1037
1183
  }
@@ -1055,9 +1201,13 @@ var Recipe = class _Recipe {
1055
1201
  clone() {
1056
1202
  const newRecipe = new _Recipe();
1057
1203
  newRecipe.metadata = JSON.parse(JSON.stringify(this.metadata));
1058
- newRecipe.ingredients = JSON.parse(JSON.stringify(this.ingredients));
1204
+ newRecipe.ingredients = JSON.parse(
1205
+ JSON.stringify(this.ingredients)
1206
+ );
1059
1207
  newRecipe.sections = JSON.parse(JSON.stringify(this.sections));
1060
- newRecipe.cookware = JSON.parse(JSON.stringify(this.cookware));
1208
+ newRecipe.cookware = JSON.parse(
1209
+ JSON.stringify(this.cookware)
1210
+ );
1061
1211
  newRecipe.timers = JSON.parse(JSON.stringify(this.timers));
1062
1212
  newRecipe.servings = this.servings;
1063
1213
  return newRecipe;
@@ -1067,32 +1217,28 @@ var Recipe = class _Recipe {
1067
1217
  // src/classes/shopping_list.ts
1068
1218
  var ShoppingList = class {
1069
1219
  /**
1070
- * Creates a new ShoppingList instance.
1071
- * @param aisle_config_str - The aisle configuration to parse.
1220
+ * Creates a new ShoppingList instance
1221
+ * @param category_config_str - The category configuration to parse.
1072
1222
  */
1073
- constructor(aisle_config_str) {
1223
+ constructor(category_config_str) {
1074
1224
  /**
1075
1225
  * The ingredients in the shopping list.
1076
- * @see {@link Ingredient}
1077
1226
  */
1078
1227
  __publicField(this, "ingredients", []);
1079
1228
  /**
1080
1229
  * The recipes in the shopping list.
1081
- * @see {@link AddedRecipe}
1082
1230
  */
1083
1231
  __publicField(this, "recipes", []);
1084
1232
  /**
1085
- * The aisle configuration for the shopping list.
1086
- * @see {@link AisleConfig}
1233
+ * The category configuration for the shopping list.
1087
1234
  */
1088
- __publicField(this, "aisle_config");
1235
+ __publicField(this, "category_config");
1089
1236
  /**
1090
1237
  * The categorized ingredients in the shopping list.
1091
- * @see {@link CategorizedIngredients}
1092
1238
  */
1093
1239
  __publicField(this, "categories");
1094
- if (aisle_config_str) {
1095
- this.set_aisle_config(aisle_config_str);
1240
+ if (category_config_str) {
1241
+ this.set_category_config(category_config_str);
1096
1242
  }
1097
1243
  }
1098
1244
  calculate_ingredients() {
@@ -1100,7 +1246,7 @@ var ShoppingList = class {
1100
1246
  for (const { recipe, factor } of this.recipes) {
1101
1247
  const scaledRecipe = factor === 1 ? recipe : recipe.scaleBy(factor);
1102
1248
  for (const ingredient of scaledRecipe.ingredients) {
1103
- if (ingredient.hidden) {
1249
+ if (ingredient.flags && ingredient.flags.includes("hidden")) {
1104
1250
  continue;
1105
1251
  }
1106
1252
  const existingIngredient = this.ingredients.find(
@@ -1108,8 +1254,8 @@ var ShoppingList = class {
1108
1254
  );
1109
1255
  let addSeparate = false;
1110
1256
  try {
1111
- if (existingIngredient) {
1112
- if (existingIngredient.quantity && ingredient.quantity) {
1257
+ if (existingIngredient && ingredient.quantity) {
1258
+ if (existingIngredient.quantity) {
1113
1259
  const newQuantity = addQuantities(
1114
1260
  {
1115
1261
  value: existingIngredient.quantity,
@@ -1124,7 +1270,7 @@ var ShoppingList = class {
1124
1270
  if (newQuantity.unit) {
1125
1271
  existingIngredient.unit = newQuantity.unit;
1126
1272
  }
1127
- } else if (ingredient.quantity) {
1273
+ } else {
1128
1274
  existingIngredient.quantity = ingredient.quantity;
1129
1275
  if (ingredient.unit) {
1130
1276
  existingIngredient.unit = ingredient.unit;
@@ -1148,7 +1294,8 @@ var ShoppingList = class {
1148
1294
  }
1149
1295
  }
1150
1296
  /**
1151
- * Adds a recipe to the shopping list.
1297
+ * Adds a recipe to the shopping list, then automatically
1298
+ * recalculates the quantities and recategorize the ingredients.
1152
1299
  * @param recipe - The recipe to add.
1153
1300
  * @param factor - The factor to scale the recipe by.
1154
1301
  */
@@ -1158,7 +1305,8 @@ var ShoppingList = class {
1158
1305
  this.categorize();
1159
1306
  }
1160
1307
  /**
1161
- * Removes a recipe from the shopping list.
1308
+ * Removes a recipe from the shopping list, then automatically
1309
+ * recalculates the quantities and recategorize the ingredients.s
1162
1310
  * @param index - The index of the recipe to remove.
1163
1311
  */
1164
1312
  remove_recipe(index) {
@@ -1170,31 +1318,35 @@ var ShoppingList = class {
1170
1318
  this.categorize();
1171
1319
  }
1172
1320
  /**
1173
- * Sets the aisle configuration for the shopping list.
1174
- * @param config - The aisle configuration to parse.
1321
+ * Sets the category configuration for the shopping list
1322
+ * and automatically categorize current ingredients from the list.
1323
+ * @param config - The category configuration to parse.
1175
1324
  */
1176
- set_aisle_config(config) {
1177
- this.aisle_config = new AisleConfig(config);
1325
+ set_category_config(config) {
1326
+ if (typeof config === "string")
1327
+ this.category_config = new CategoryConfig(config);
1328
+ else if (config instanceof CategoryConfig) this.category_config = config;
1329
+ else throw new Error("Invalid category configuration");
1178
1330
  this.categorize();
1179
1331
  }
1180
1332
  /**
1181
1333
  * Categorizes the ingredients in the shopping list
1182
- * Will use the aisle config if any, otherwise all ingredients will be placed in the "other" category
1334
+ * Will use the category config if any, otherwise all ingredients will be placed in the "other" category
1183
1335
  */
1184
1336
  categorize() {
1185
- if (!this.aisle_config) {
1337
+ if (!this.category_config) {
1186
1338
  this.categories = { other: this.ingredients };
1187
1339
  return;
1188
1340
  }
1189
1341
  const categories = { other: [] };
1190
- for (const category of this.aisle_config.categories) {
1342
+ for (const category of this.category_config.categories) {
1191
1343
  categories[category.name] = [];
1192
1344
  }
1193
1345
  for (const ingredient of this.ingredients) {
1194
1346
  let found = false;
1195
- for (const category of this.aisle_config.categories) {
1196
- for (const aisleIngredient of category.ingredients) {
1197
- if (aisleIngredient.aliases.includes(ingredient.name)) {
1347
+ for (const category of this.category_config.categories) {
1348
+ for (const categoryIngredient of category.ingredients) {
1349
+ if (categoryIngredient.aliases.includes(ingredient.name)) {
1198
1350
  categories[category.name].push(ingredient);
1199
1351
  found = true;
1200
1352
  break;
@@ -1213,9 +1365,13 @@ var ShoppingList = class {
1213
1365
  };
1214
1366
  // Annotate the CommonJS export names for ESM import in node:
1215
1367
  0 && (module.exports = {
1216
- AisleConfig,
1368
+ CategoryConfig,
1217
1369
  Recipe,
1218
1370
  Section,
1219
1371
  ShoppingList
1220
1372
  });
1373
+ /* v8 ignore else -- @preserve */
1374
+ /* v8 ignore else -- expliciting error types -- @preserve */
1375
+ /* v8 ignore else -- expliciting error type -- @preserve */
1376
+ /* v8 ignore else -- only set unit if it is given -- @preserve */
1221
1377
  //# sourceMappingURL=index.cjs.map