@tmlmt/cooklang-parser 1.4.4 → 2.0.1

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.js CHANGED
@@ -289,13 +289,14 @@ var i = (() => {
289
289
 
290
290
  // src/regex.ts
291
291
  var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
292
- var nonWordChar = "\\s@#~\\[\\]{(.,;:!?";
293
- 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();
294
- 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();
292
+ 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();
293
+ var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
294
+ 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();
295
+ 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();
295
296
  var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
296
- 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();
297
- 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();
298
- 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();
297
+ 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();
298
+ 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();
299
+ 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();
299
300
  var tokensRegex = new RegExp(
300
301
  [
301
302
  multiwordIngredient,
@@ -311,6 +312,7 @@ var blockCommentRegex = d().whitespace().zeroOrMore().literal("[-").anyCharacter
311
312
  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();
312
313
  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();
313
314
  var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
315
+ var floatRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
314
316
 
315
317
  // src/units.ts
316
318
  var units = [
@@ -425,7 +427,7 @@ for (const unit of units) {
425
427
  unitMap.set(alias.toLowerCase(), unit);
426
428
  }
427
429
  }
428
- function normalizeUnit(unit) {
430
+ function normalizeUnit(unit = "") {
429
431
  return unitMap.get(unit.toLowerCase().trim());
430
432
  }
431
433
  var CannotAddTextValueError = class extends Error {
@@ -487,7 +489,10 @@ function addNumericValues(val1, val2) {
487
489
  num2 = val2.num;
488
490
  den2 = val2.den;
489
491
  }
490
- if (val1.type === "fraction" && val2.type === "fraction") {
492
+ if (num1 === 0 && num2 === 0) {
493
+ return { type: "decimal", value: 0 };
494
+ }
495
+ 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) {
491
496
  const commonDen = den1 * den2;
492
497
  const sumNum = num1 * den2 + num2 * den1;
493
498
  return simplifyFraction(sumNum, commonDen);
@@ -528,6 +533,32 @@ var convertQuantityValue = (value, def, targetDef) => {
528
533
  const factor = def.toBase / targetDef.toBase;
529
534
  return multiplyQuantityValue(value, factor);
530
535
  };
536
+ function getDefaultQuantityValue() {
537
+ return { type: "fixed", value: { type: "decimal", value: 0 } };
538
+ }
539
+ function addQuantityValues(v1, v2) {
540
+ if (v1.type === "fixed" && v1.value.type === "text" || v2.type === "fixed" && v2.value.type === "text") {
541
+ throw new CannotAddTextValueError();
542
+ }
543
+ if (v1.type === "fixed" && v2.type === "fixed") {
544
+ const res = addNumericValues(
545
+ v1.value,
546
+ v2.value
547
+ );
548
+ return { type: "fixed", value: res };
549
+ }
550
+ const r1 = v1.type === "range" ? v1 : { type: "range", min: v1.value, max: v1.value };
551
+ const r2 = v2.type === "range" ? v2 : { type: "range", min: v2.value, max: v2.value };
552
+ const newMin = addNumericValues(
553
+ r1.min,
554
+ r2.min
555
+ );
556
+ const newMax = addNumericValues(
557
+ r1.max,
558
+ r2.max
559
+ );
560
+ return { type: "range", min: newMin, max: newMax };
561
+ }
531
562
  function addQuantities(q1, q2) {
532
563
  const v1 = q1.value;
533
564
  const v2 = q2.value;
@@ -536,33 +567,14 @@ function addQuantities(q1, q2) {
536
567
  }
537
568
  const unit1Def = normalizeUnit(q1.unit);
538
569
  const unit2Def = normalizeUnit(q2.unit);
539
- const addQuantityValuesAndSetUnit = (val1, val2, unit) => {
540
- if (val1.type === "fixed" && val2.type === "fixed") {
541
- const res = addNumericValues(
542
- val1.value,
543
- val2.value
544
- );
545
- return { value: { type: "fixed", value: res }, unit };
546
- }
547
- const r1 = val1.type === "range" ? val1 : { type: "range", min: val1.value, max: val1.value };
548
- const r2 = val2.type === "range" ? val2 : { type: "range", min: val2.value, max: val2.value };
549
- const newMin = addNumericValues(
550
- r1.min,
551
- r2.min
552
- );
553
- const newMax = addNumericValues(
554
- r1.max,
555
- r2.max
556
- );
557
- return { value: { type: "range", min: newMin, max: newMax }, unit };
558
- };
559
- if (q1.unit === "" && unit2Def) {
570
+ const addQuantityValuesAndSetUnit = (val1, val2, unit) => ({ value: addQuantityValues(val1, val2), unit });
571
+ if ((q1.unit === "" || q1.unit === void 0) && q2.unit !== void 0) {
560
572
  return addQuantityValuesAndSetUnit(v1, v2, q2.unit);
561
573
  }
562
- if (q2.unit === "" && unit1Def) {
574
+ if ((q2.unit === "" || q2.unit === void 0) && q1.unit !== void 0) {
563
575
  return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
564
576
  }
565
- if (q1.unit.toLowerCase() === q2.unit.toLowerCase()) {
577
+ if (!q1.unit && !q2.unit || q1.unit && q2.unit && q1.unit.toLowerCase() === q2.unit.toLowerCase()) {
566
578
  return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
567
579
  }
568
580
  if (unit1Def && unit2Def) {
@@ -592,6 +604,17 @@ function addQuantities(q1, q2) {
592
604
  throw new IncompatibleUnitsError(q1.unit, q2.unit);
593
605
  }
594
606
 
607
+ // src/errors.ts
608
+ var ReferencedItemCannotBeRedefinedError = class extends Error {
609
+ constructor(item_type, item_name, new_modifier) {
610
+ super(
611
+ `The referenced ${item_type} "${item_name}" cannot be redefined as ${new_modifier}.
612
+ 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}`
613
+ );
614
+ this.name = "ReferencedItemCannotBeRedefinedError";
615
+ }
616
+ };
617
+
595
618
  // src/parser_helpers.ts
596
619
  function flushPendingNote(section, note) {
597
620
  if (note.length > 0) {
@@ -611,21 +634,28 @@ function flushPendingItems(section, items) {
611
634
  function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
612
635
  const { name, quantity, unit } = newIngredient;
613
636
  if (isReference) {
614
- const index = ingredients.findIndex(
637
+ const indexFind = ingredients.findIndex(
615
638
  (i2) => i2.name.toLowerCase() === name.toLowerCase()
616
639
  );
617
- if (index === -1) {
640
+ if (indexFind === -1) {
618
641
  throw new Error(
619
642
  `Referenced ingredient "${name}" not found. A referenced ingredient must be declared before being referenced with '&'.`
620
643
  );
621
644
  }
622
- const existingIngredient = ingredients[index];
645
+ const existingIngredient = ingredients[indexFind];
646
+ for (const flag of newIngredient.flags) {
647
+ if (!existingIngredient.flags.includes(flag)) {
648
+ throw new ReferencedItemCannotBeRedefinedError(
649
+ "ingredient",
650
+ existingIngredient.name,
651
+ flag
652
+ );
653
+ }
654
+ }
655
+ let quantityPartIndex = void 0;
623
656
  if (quantity !== void 0) {
624
657
  const currentQuantity = {
625
- value: existingIngredient.quantity ?? {
626
- type: "fixed",
627
- value: { type: "decimal", value: 0 }
628
- },
658
+ value: existingIngredient.quantity ?? getDefaultQuantityValue(),
629
659
  unit: existingIngredient.unit ?? ""
630
660
  };
631
661
  const newQuantity = { value: quantity, unit: unit ?? "" };
@@ -633,18 +663,35 @@ function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
633
663
  const total = addQuantities(currentQuantity, newQuantity);
634
664
  existingIngredient.quantity = total.value;
635
665
  existingIngredient.unit = total.unit || void 0;
666
+ if (existingIngredient.quantityParts) {
667
+ existingIngredient.quantityParts.push(
668
+ ...newIngredient.quantityParts
669
+ );
670
+ } else {
671
+ existingIngredient.quantityParts = newIngredient.quantityParts;
672
+ }
673
+ quantityPartIndex = existingIngredient.quantityParts.length - 1;
636
674
  } catch (e2) {
637
675
  if (e2 instanceof IncompatibleUnitsError || e2 instanceof CannotAddTextValueError) {
638
- return ingredients.push(newIngredient) - 1;
676
+ return {
677
+ ingredientIndex: ingredients.push(newIngredient) - 1,
678
+ quantityPartIndex: 0
679
+ };
639
680
  }
640
681
  }
641
682
  }
642
- return index;
683
+ return {
684
+ ingredientIndex: indexFind,
685
+ quantityPartIndex
686
+ };
643
687
  }
644
- return ingredients.push(newIngredient) - 1;
688
+ return {
689
+ ingredientIndex: ingredients.push(newIngredient) - 1,
690
+ quantityPartIndex: 0
691
+ };
645
692
  }
646
693
  function findAndUpsertCookware(cookware, newCookware, isReference) {
647
- const { name } = newCookware;
694
+ const { name, quantity } = newCookware;
648
695
  if (isReference) {
649
696
  const index = cookware.findIndex(
650
697
  (i2) => i2.name.toLowerCase() === name.toLowerCase()
@@ -654,9 +701,55 @@ function findAndUpsertCookware(cookware, newCookware, isReference) {
654
701
  `Referenced cookware "${name}" not found. A referenced cookware must be declared before being referenced with '&'.`
655
702
  );
656
703
  }
657
- return index;
704
+ const existingCookware = cookware[index];
705
+ for (const flag of newCookware.flags) {
706
+ if (!existingCookware.flags.includes(flag)) {
707
+ throw new ReferencedItemCannotBeRedefinedError(
708
+ "cookware",
709
+ existingCookware.name,
710
+ flag
711
+ );
712
+ }
713
+ }
714
+ let quantityPartIndex = void 0;
715
+ if (quantity !== void 0) {
716
+ if (!existingCookware.quantity) {
717
+ existingCookware.quantity = quantity;
718
+ existingCookware.quantityParts = newCookware.quantityParts;
719
+ quantityPartIndex = 0;
720
+ } else {
721
+ try {
722
+ existingCookware.quantity = addQuantityValues(
723
+ existingCookware.quantity,
724
+ quantity
725
+ );
726
+ if (!existingCookware.quantityParts) {
727
+ existingCookware.quantityParts = newCookware.quantityParts;
728
+ quantityPartIndex = 0;
729
+ } else {
730
+ quantityPartIndex = existingCookware.quantityParts.push(
731
+ ...newCookware.quantityParts
732
+ ) - 1;
733
+ }
734
+ } catch (e2) {
735
+ if (e2 instanceof CannotAddTextValueError) {
736
+ return {
737
+ cookwareIndex: cookware.push(newCookware) - 1,
738
+ quantityPartIndex: 0
739
+ };
740
+ }
741
+ }
742
+ }
743
+ }
744
+ return {
745
+ cookwareIndex: index,
746
+ quantityPartIndex
747
+ };
658
748
  }
659
- return cookware.push(newCookware) - 1;
749
+ return {
750
+ cookwareIndex: cookware.push(newCookware) - 1,
751
+ quantityPartIndex: quantity ? 0 : void 0
752
+ };
660
753
  }
661
754
  var parseFixedValue = (input_str) => {
662
755
  if (!numberLikeRegex.test(input_str)) {
@@ -688,9 +781,7 @@ function parseSimpleMetaVar(content, varName) {
688
781
  return varMatch ? varMatch[1]?.trim().replace(/\s*\r?\n\s+/g, " ") : void 0;
689
782
  }
690
783
  function parseScalingMetaVar(content, varName) {
691
- const varMatch = content.match(
692
- new RegExp(`^${varName}:[\\t ]*(([^,\\n]*),? ?(?:.*)?)`, "m")
693
- );
784
+ const varMatch = content.match(scalingMetaValueRegex(varName));
694
785
  if (!varMatch) return void 0;
695
786
  if (isNaN(Number(varMatch[2]?.trim()))) {
696
787
  throw new Error("Scaling variables should be numbers");
@@ -746,7 +837,7 @@ function extractMetadata(content) {
746
837
  const stringMetaValue = parseSimpleMetaVar(metadataContent, metaVar);
747
838
  if (stringMetaValue) metadata[metaVar] = stringMetaValue;
748
839
  }
749
- for (const metaVar of ["servings", "yield", "serves"]) {
840
+ for (const metaVar of ["serves", "yield", "servings"]) {
750
841
  const scalingMetaValue = parseScalingMetaVar(metadataContent, metaVar);
751
842
  if (scalingMetaValue && scalingMetaValue[1]) {
752
843
  metadata[metaVar] = scalingMetaValue[1];
@@ -862,15 +953,28 @@ var Recipe = class _Recipe {
862
953
  }
863
954
  const groups = match.groups;
864
955
  if (groups.mIngredientName || groups.sIngredientName) {
865
- const name = groups.mIngredientName || groups.sIngredientName;
956
+ let name = groups.mIngredientName || groups.sIngredientName;
957
+ const scalableQuantity = (groups.mIngredientQuantityModifier || groups.sIngredientQuantityModifier) !== "=";
866
958
  const quantityRaw = groups.mIngredientQuantity || groups.sIngredientQuantity;
867
- const units2 = groups.mIngredientUnits || groups.sIngredientUnits;
959
+ const unit = groups.mIngredientUnit || groups.sIngredientUnit;
868
960
  const preparation = groups.mIngredientPreparation || groups.sIngredientPreparation;
869
- const modifier = groups.mIngredientModifier || groups.sIngredientModifier;
870
- const optional = modifier === "?";
871
- const hidden = modifier === "-";
872
- const reference = modifier === "&";
873
- const isRecipe = modifier === "@";
961
+ const modifiers = groups.mIngredientModifiers || groups.sIngredientModifiers;
962
+ const reference = modifiers !== void 0 && modifiers.includes("&");
963
+ const flags = [];
964
+ if (modifiers !== void 0 && modifiers.includes("?")) {
965
+ flags.push("optional");
966
+ }
967
+ if (modifiers !== void 0 && modifiers.includes("-")) {
968
+ flags.push("hidden");
969
+ }
970
+ if (modifiers !== void 0 && modifiers.includes("@") || groups.mIngredientRecipeAnchor || groups.sIngredientRecipeAnchor) {
971
+ flags.push("recipe");
972
+ }
973
+ let extras = void 0;
974
+ if (flags.includes("recipe")) {
975
+ extras = { path: `${name}.cook` };
976
+ name = name.substring(name.lastIndexOf("/") + 1);
977
+ }
874
978
  const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
875
979
  const aliasMatch = name.match(ingredientAliasRegex);
876
980
  let listName, displayName;
@@ -881,50 +985,70 @@ var Recipe = class _Recipe {
881
985
  listName = name;
882
986
  displayName = name;
883
987
  }
884
- const idxInList = findAndUpsertIngredient(
988
+ const newIngredient = {
989
+ name: listName,
990
+ quantity,
991
+ quantityParts: quantity ? [
992
+ {
993
+ value: quantity,
994
+ unit,
995
+ scalable: scalableQuantity
996
+ }
997
+ ] : void 0,
998
+ unit,
999
+ preparation,
1000
+ flags
1001
+ };
1002
+ if (extras) {
1003
+ newIngredient.extras = extras;
1004
+ }
1005
+ const idxsInList = findAndUpsertIngredient(
885
1006
  this.ingredients,
886
- {
887
- name: listName,
888
- quantity,
889
- unit: units2,
890
- optional,
891
- hidden,
892
- preparation,
893
- isRecipe
894
- },
1007
+ newIngredient,
895
1008
  reference
896
1009
  );
897
1010
  const newItem = {
898
1011
  type: "ingredient",
899
- value: idxInList,
900
- itemQuantity: quantity,
901
- itemUnit: units2,
1012
+ index: idxsInList.ingredientIndex,
902
1013
  displayName
903
1014
  };
1015
+ if (idxsInList.quantityPartIndex !== void 0) {
1016
+ newItem.quantityPartIndex = idxsInList.quantityPartIndex;
1017
+ }
904
1018
  items.push(newItem);
905
1019
  } else if (groups.mCookwareName || groups.sCookwareName) {
906
1020
  const name = groups.mCookwareName || groups.sCookwareName;
907
- const modifier = groups.mCookwareModifier || groups.sCookwareModifier;
1021
+ const modifiers = groups.mCookwareModifiers || groups.sCookwareModifiers;
908
1022
  const quantityRaw = groups.mCookwareQuantity || groups.sCookwareQuantity;
909
- const optional = modifier === "?";
910
- const hidden = modifier === "-";
911
- const reference = modifier === "&";
1023
+ const reference = modifiers !== void 0 && modifiers.includes("&");
1024
+ const flags = [];
1025
+ if (modifiers !== void 0 && modifiers.includes("?")) {
1026
+ flags.push("optional");
1027
+ }
1028
+ if (modifiers !== void 0 && modifiers.includes("-")) {
1029
+ flags.push("hidden");
1030
+ }
912
1031
  const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
913
- const idxInList = findAndUpsertCookware(
1032
+ const idxsInList = findAndUpsertCookware(
914
1033
  this.cookware,
915
- { name, quantity, optional, hidden },
1034
+ {
1035
+ name,
1036
+ quantity,
1037
+ quantityParts: quantity ? [quantity] : void 0,
1038
+ flags
1039
+ },
916
1040
  reference
917
1041
  );
918
1042
  items.push({
919
1043
  type: "cookware",
920
- value: idxInList,
921
- itemQuantity: quantity
1044
+ index: idxsInList.cookwareIndex,
1045
+ quantityPartIndex: idxsInList.quantityPartIndex
922
1046
  });
923
- } else if (groups.timerQuantity !== void 0) {
1047
+ } else {
924
1048
  const durationStr = groups.timerQuantity.trim();
925
- const unit = (groups.timerUnits || "").trim();
1049
+ const unit = (groups.timerUnit || "").trim();
926
1050
  if (!unit) {
927
- throw new Error("Timer missing units");
1051
+ throw new Error("Timer missing unit");
928
1052
  }
929
1053
  const name = groups.timerName || void 0;
930
1054
  const duration = parseQuantityInput(durationStr);
@@ -933,7 +1057,7 @@ var Recipe = class _Recipe {
933
1057
  duration,
934
1058
  unit
935
1059
  };
936
- items.push({ type: "timer", value: this.timers.push(timerObj) - 1 });
1060
+ items.push({ type: "timer", index: this.timers.push(timerObj) - 1 });
937
1061
  }
938
1062
  cursor = idx + match[0].length;
939
1063
  }
@@ -976,30 +1100,57 @@ var Recipe = class _Recipe {
976
1100
  throw new Error("Error scaling recipe: no initial servings value set");
977
1101
  }
978
1102
  newRecipe.ingredients = newRecipe.ingredients.map((ingredient) => {
979
- if (ingredient.quantity && !(ingredient.quantity.type === "fixed" && ingredient.quantity.value.type === "text")) {
980
- ingredient.quantity = multiplyQuantityValue(
981
- ingredient.quantity,
982
- factor
1103
+ if (ingredient.quantityParts) {
1104
+ ingredient.quantityParts = ingredient.quantityParts.map(
1105
+ (quantityPart) => {
1106
+ if (quantityPart.value.type === "fixed" && quantityPart.value.value.type === "text") {
1107
+ return quantityPart;
1108
+ }
1109
+ return {
1110
+ ...quantityPart,
1111
+ value: multiplyQuantityValue(
1112
+ quantityPart.value,
1113
+ quantityPart.scalable ? factor : 1
1114
+ )
1115
+ };
1116
+ }
983
1117
  );
1118
+ if (ingredient.quantityParts.length === 1) {
1119
+ ingredient.quantity = ingredient.quantityParts[0].value;
1120
+ ingredient.unit = ingredient.quantityParts[0].unit;
1121
+ } else {
1122
+ const totalQuantity = ingredient.quantityParts.reduce(
1123
+ (acc, val) => addQuantities(acc, { value: val.value, unit: val.unit }),
1124
+ { value: getDefaultQuantityValue() }
1125
+ );
1126
+ ingredient.quantity = totalQuantity.value;
1127
+ ingredient.unit = totalQuantity.unit;
1128
+ }
984
1129
  }
985
1130
  return ingredient;
986
1131
  }).filter((ingredient) => ingredient.quantity !== null);
987
1132
  newRecipe.servings = originalServings * factor;
988
1133
  if (newRecipe.metadata.servings && this.metadata.servings) {
989
- const servingsValue = parseFloat(this.metadata.servings);
990
- if (!isNaN(servingsValue)) {
1134
+ if (floatRegex.test(String(this.metadata.servings).replace(",", ".").trim())) {
1135
+ const servingsValue = parseFloat(
1136
+ String(this.metadata.servings).replace(",", ".")
1137
+ );
991
1138
  newRecipe.metadata.servings = String(servingsValue * factor);
992
1139
  }
993
1140
  }
994
1141
  if (newRecipe.metadata.yield && this.metadata.yield) {
995
- const yieldValue = parseFloat(this.metadata.yield);
996
- if (!isNaN(yieldValue)) {
1142
+ if (floatRegex.test(String(this.metadata.yield).replace(",", ".").trim())) {
1143
+ const yieldValue = parseFloat(
1144
+ String(this.metadata.yield).replace(",", ".")
1145
+ );
997
1146
  newRecipe.metadata.yield = String(yieldValue * factor);
998
1147
  }
999
1148
  }
1000
1149
  if (newRecipe.metadata.serves && this.metadata.serves) {
1001
- const servesValue = parseFloat(this.metadata.serves);
1002
- if (!isNaN(servesValue)) {
1150
+ if (floatRegex.test(String(this.metadata.serves).replace(",", ".").trim())) {
1151
+ const servesValue = parseFloat(
1152
+ String(this.metadata.serves).replace(",", ".")
1153
+ );
1003
1154
  newRecipe.metadata.serves = String(servesValue * factor);
1004
1155
  }
1005
1156
  }
@@ -1068,7 +1219,7 @@ var ShoppingList = class {
1068
1219
  for (const { recipe, factor } of this.recipes) {
1069
1220
  const scaledRecipe = factor === 1 ? recipe : recipe.scaleBy(factor);
1070
1221
  for (const ingredient of scaledRecipe.ingredients) {
1071
- if (ingredient.hidden) {
1222
+ if (ingredient.flags && ingredient.flags.includes("hidden")) {
1072
1223
  continue;
1073
1224
  }
1074
1225
  const existingIngredient = this.ingredients.find(
@@ -1076,8 +1227,8 @@ var ShoppingList = class {
1076
1227
  );
1077
1228
  let addSeparate = false;
1078
1229
  try {
1079
- if (existingIngredient) {
1080
- if (existingIngredient.quantity && ingredient.quantity) {
1230
+ if (existingIngredient && ingredient.quantity) {
1231
+ if (existingIngredient.quantity) {
1081
1232
  const newQuantity = addQuantities(
1082
1233
  {
1083
1234
  value: existingIngredient.quantity,
@@ -1092,7 +1243,7 @@ var ShoppingList = class {
1092
1243
  if (newQuantity.unit) {
1093
1244
  existingIngredient.unit = newQuantity.unit;
1094
1245
  }
1095
- } else if (ingredient.quantity) {
1246
+ } else {
1096
1247
  existingIngredient.quantity = ingredient.quantity;
1097
1248
  if (ingredient.unit) {
1098
1249
  existingIngredient.unit = ingredient.unit;
@@ -1191,4 +1342,8 @@ export {
1191
1342
  Section,
1192
1343
  ShoppingList
1193
1344
  };
1345
+ /* v8 ignore else -- @preserve */
1346
+ /* v8 ignore else -- expliciting error types -- @preserve */
1347
+ /* v8 ignore else -- expliciting error type -- @preserve */
1348
+ /* v8 ignore else -- only set unit if it is given -- @preserve */
1194
1349
  //# sourceMappingURL=index.js.map