@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/README.md CHANGED
@@ -60,7 +60,10 @@ console.log(recipe.timers); // [{ duration: 15, unit: "minutes", name: undefined
60
60
 
61
61
  ## Future plans
62
62
 
63
- I plan to further develop features depending on the needs or bugs I will encounter in using this library in a practical application.
63
+ I plan to further develop features depending on the needs or bugs I will encounter in using this library in a practical application. Current backlog is as follows:
64
+
65
+ - Fixing non-compliance with spec for recipe referencing by path
66
+ - Pantry parsing and basic functions (e.g. take pantry into account when creating a shopping list)
64
67
 
65
68
  ## Test coverage
66
69
 
package/dist/index.cjs CHANGED
@@ -316,13 +316,14 @@ var i = (() => {
316
316
 
317
317
  // src/regex.ts
318
318
  var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
319
- var nonWordChar = "\\s@#~\\[\\]{(.,;:!?";
320
- 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();
321
- 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();
322
323
  var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
323
- 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();
324
- 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();
325
- 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();
326
327
  var tokensRegex = new RegExp(
327
328
  [
328
329
  multiwordIngredient,
@@ -338,6 +339,7 @@ var blockCommentRegex = d().whitespace().zeroOrMore().literal("[-").anyCharacter
338
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();
339
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();
340
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();
341
343
 
342
344
  // src/units.ts
343
345
  var units = [
@@ -452,7 +454,7 @@ for (const unit of units) {
452
454
  unitMap.set(alias.toLowerCase(), unit);
453
455
  }
454
456
  }
455
- function normalizeUnit(unit) {
457
+ function normalizeUnit(unit = "") {
456
458
  return unitMap.get(unit.toLowerCase().trim());
457
459
  }
458
460
  var CannotAddTextValueError = class extends Error {
@@ -514,7 +516,10 @@ function addNumericValues(val1, val2) {
514
516
  num2 = val2.num;
515
517
  den2 = val2.den;
516
518
  }
517
- 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) {
518
523
  const commonDen = den1 * den2;
519
524
  const sumNum = num1 * den2 + num2 * den1;
520
525
  return simplifyFraction(sumNum, commonDen);
@@ -555,6 +560,32 @@ var convertQuantityValue = (value, def, targetDef) => {
555
560
  const factor = def.toBase / targetDef.toBase;
556
561
  return multiplyQuantityValue(value, factor);
557
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
+ }
558
589
  function addQuantities(q1, q2) {
559
590
  const v1 = q1.value;
560
591
  const v2 = q2.value;
@@ -563,33 +594,14 @@ function addQuantities(q1, q2) {
563
594
  }
564
595
  const unit1Def = normalizeUnit(q1.unit);
565
596
  const unit2Def = normalizeUnit(q2.unit);
566
- const addQuantityValuesAndSetUnit = (val1, val2, unit) => {
567
- if (val1.type === "fixed" && val2.type === "fixed") {
568
- const res = addNumericValues(
569
- val1.value,
570
- val2.value
571
- );
572
- return { value: { type: "fixed", value: res }, unit };
573
- }
574
- const r1 = val1.type === "range" ? val1 : { type: "range", min: val1.value, max: val1.value };
575
- const r2 = val2.type === "range" ? val2 : { type: "range", min: val2.value, max: val2.value };
576
- const newMin = addNumericValues(
577
- r1.min,
578
- r2.min
579
- );
580
- const newMax = addNumericValues(
581
- r1.max,
582
- r2.max
583
- );
584
- return { value: { type: "range", min: newMin, max: newMax }, unit };
585
- };
586
- 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) {
587
599
  return addQuantityValuesAndSetUnit(v1, v2, q2.unit);
588
600
  }
589
- if (q2.unit === "" && unit1Def) {
601
+ if ((q2.unit === "" || q2.unit === void 0) && q1.unit !== void 0) {
590
602
  return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
591
603
  }
592
- if (q1.unit.toLowerCase() === q2.unit.toLowerCase()) {
604
+ if (!q1.unit && !q2.unit || q1.unit && q2.unit && q1.unit.toLowerCase() === q2.unit.toLowerCase()) {
593
605
  return addQuantityValuesAndSetUnit(v1, v2, q1.unit);
594
606
  }
595
607
  if (unit1Def && unit2Def) {
@@ -619,6 +631,17 @@ function addQuantities(q1, q2) {
619
631
  throw new IncompatibleUnitsError(q1.unit, q2.unit);
620
632
  }
621
633
 
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";
642
+ }
643
+ };
644
+
622
645
  // src/parser_helpers.ts
623
646
  function flushPendingNote(section, note) {
624
647
  if (note.length > 0) {
@@ -638,21 +661,28 @@ function flushPendingItems(section, items) {
638
661
  function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
639
662
  const { name, quantity, unit } = newIngredient;
640
663
  if (isReference) {
641
- const index = ingredients.findIndex(
664
+ const indexFind = ingredients.findIndex(
642
665
  (i2) => i2.name.toLowerCase() === name.toLowerCase()
643
666
  );
644
- if (index === -1) {
667
+ if (indexFind === -1) {
645
668
  throw new Error(
646
669
  `Referenced ingredient "${name}" not found. A referenced ingredient must be declared before being referenced with '&'.`
647
670
  );
648
671
  }
649
- 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;
650
683
  if (quantity !== void 0) {
651
684
  const currentQuantity = {
652
- value: existingIngredient.quantity ?? {
653
- type: "fixed",
654
- value: { type: "decimal", value: 0 }
655
- },
685
+ value: existingIngredient.quantity ?? getDefaultQuantityValue(),
656
686
  unit: existingIngredient.unit ?? ""
657
687
  };
658
688
  const newQuantity = { value: quantity, unit: unit ?? "" };
@@ -660,18 +690,35 @@ function findAndUpsertIngredient(ingredients, newIngredient, isReference) {
660
690
  const total = addQuantities(currentQuantity, newQuantity);
661
691
  existingIngredient.quantity = total.value;
662
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;
663
701
  } catch (e2) {
664
702
  if (e2 instanceof IncompatibleUnitsError || e2 instanceof CannotAddTextValueError) {
665
- return ingredients.push(newIngredient) - 1;
703
+ return {
704
+ ingredientIndex: ingredients.push(newIngredient) - 1,
705
+ quantityPartIndex: 0
706
+ };
666
707
  }
667
708
  }
668
709
  }
669
- return index;
710
+ return {
711
+ ingredientIndex: indexFind,
712
+ quantityPartIndex
713
+ };
670
714
  }
671
- return ingredients.push(newIngredient) - 1;
715
+ return {
716
+ ingredientIndex: ingredients.push(newIngredient) - 1,
717
+ quantityPartIndex: 0
718
+ };
672
719
  }
673
720
  function findAndUpsertCookware(cookware, newCookware, isReference) {
674
- const { name } = newCookware;
721
+ const { name, quantity } = newCookware;
675
722
  if (isReference) {
676
723
  const index = cookware.findIndex(
677
724
  (i2) => i2.name.toLowerCase() === name.toLowerCase()
@@ -681,9 +728,55 @@ function findAndUpsertCookware(cookware, newCookware, isReference) {
681
728
  `Referenced cookware "${name}" not found. A referenced cookware must be declared before being referenced with '&'.`
682
729
  );
683
730
  }
684
- 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
+ };
685
775
  }
686
- return cookware.push(newCookware) - 1;
776
+ return {
777
+ cookwareIndex: cookware.push(newCookware) - 1,
778
+ quantityPartIndex: quantity ? 0 : void 0
779
+ };
687
780
  }
688
781
  var parseFixedValue = (input_str) => {
689
782
  if (!numberLikeRegex.test(input_str)) {
@@ -715,9 +808,7 @@ function parseSimpleMetaVar(content, varName) {
715
808
  return varMatch ? varMatch[1]?.trim().replace(/\s*\r?\n\s+/g, " ") : void 0;
716
809
  }
717
810
  function parseScalingMetaVar(content, varName) {
718
- const varMatch = content.match(
719
- new RegExp(`^${varName}:[\\t ]*(([^,\\n]*),? ?(?:.*)?)`, "m")
720
- );
811
+ const varMatch = content.match(scalingMetaValueRegex(varName));
721
812
  if (!varMatch) return void 0;
722
813
  if (isNaN(Number(varMatch[2]?.trim()))) {
723
814
  throw new Error("Scaling variables should be numbers");
@@ -773,7 +864,7 @@ function extractMetadata(content) {
773
864
  const stringMetaValue = parseSimpleMetaVar(metadataContent, metaVar);
774
865
  if (stringMetaValue) metadata[metaVar] = stringMetaValue;
775
866
  }
776
- for (const metaVar of ["servings", "yield", "serves"]) {
867
+ for (const metaVar of ["serves", "yield", "servings"]) {
777
868
  const scalingMetaValue = parseScalingMetaVar(metadataContent, metaVar);
778
869
  if (scalingMetaValue && scalingMetaValue[1]) {
779
870
  metadata[metaVar] = scalingMetaValue[1];
@@ -889,15 +980,28 @@ var Recipe = class _Recipe {
889
980
  }
890
981
  const groups = match.groups;
891
982
  if (groups.mIngredientName || groups.sIngredientName) {
892
- const name = groups.mIngredientName || groups.sIngredientName;
983
+ let name = groups.mIngredientName || groups.sIngredientName;
984
+ const scalableQuantity = (groups.mIngredientQuantityModifier || groups.sIngredientQuantityModifier) !== "=";
893
985
  const quantityRaw = groups.mIngredientQuantity || groups.sIngredientQuantity;
894
- const units2 = groups.mIngredientUnits || groups.sIngredientUnits;
986
+ const unit = groups.mIngredientUnit || groups.sIngredientUnit;
895
987
  const preparation = groups.mIngredientPreparation || groups.sIngredientPreparation;
896
- const modifier = groups.mIngredientModifier || groups.sIngredientModifier;
897
- const optional = modifier === "?";
898
- const hidden = modifier === "-";
899
- const reference = modifier === "&";
900
- 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
+ }
901
1005
  const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
902
1006
  const aliasMatch = name.match(ingredientAliasRegex);
903
1007
  let listName, displayName;
@@ -908,50 +1012,70 @@ var Recipe = class _Recipe {
908
1012
  listName = name;
909
1013
  displayName = name;
910
1014
  }
911
- 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(
912
1033
  this.ingredients,
913
- {
914
- name: listName,
915
- quantity,
916
- unit: units2,
917
- optional,
918
- hidden,
919
- preparation,
920
- isRecipe
921
- },
1034
+ newIngredient,
922
1035
  reference
923
1036
  );
924
1037
  const newItem = {
925
1038
  type: "ingredient",
926
- value: idxInList,
927
- itemQuantity: quantity,
928
- itemUnit: units2,
1039
+ index: idxsInList.ingredientIndex,
929
1040
  displayName
930
1041
  };
1042
+ if (idxsInList.quantityPartIndex !== void 0) {
1043
+ newItem.quantityPartIndex = idxsInList.quantityPartIndex;
1044
+ }
931
1045
  items.push(newItem);
932
1046
  } else if (groups.mCookwareName || groups.sCookwareName) {
933
1047
  const name = groups.mCookwareName || groups.sCookwareName;
934
- const modifier = groups.mCookwareModifier || groups.sCookwareModifier;
1048
+ const modifiers = groups.mCookwareModifiers || groups.sCookwareModifiers;
935
1049
  const quantityRaw = groups.mCookwareQuantity || groups.sCookwareQuantity;
936
- const optional = modifier === "?";
937
- const hidden = modifier === "-";
938
- 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
+ }
939
1058
  const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
940
- const idxInList = findAndUpsertCookware(
1059
+ const idxsInList = findAndUpsertCookware(
941
1060
  this.cookware,
942
- { name, quantity, optional, hidden },
1061
+ {
1062
+ name,
1063
+ quantity,
1064
+ quantityParts: quantity ? [quantity] : void 0,
1065
+ flags
1066
+ },
943
1067
  reference
944
1068
  );
945
1069
  items.push({
946
1070
  type: "cookware",
947
- value: idxInList,
948
- itemQuantity: quantity
1071
+ index: idxsInList.cookwareIndex,
1072
+ quantityPartIndex: idxsInList.quantityPartIndex
949
1073
  });
950
- } else if (groups.timerQuantity !== void 0) {
1074
+ } else {
951
1075
  const durationStr = groups.timerQuantity.trim();
952
- const unit = (groups.timerUnits || "").trim();
1076
+ const unit = (groups.timerUnit || "").trim();
953
1077
  if (!unit) {
954
- throw new Error("Timer missing units");
1078
+ throw new Error("Timer missing unit");
955
1079
  }
956
1080
  const name = groups.timerName || void 0;
957
1081
  const duration = parseQuantityInput(durationStr);
@@ -960,7 +1084,7 @@ var Recipe = class _Recipe {
960
1084
  duration,
961
1085
  unit
962
1086
  };
963
- items.push({ type: "timer", value: this.timers.push(timerObj) - 1 });
1087
+ items.push({ type: "timer", index: this.timers.push(timerObj) - 1 });
964
1088
  }
965
1089
  cursor = idx + match[0].length;
966
1090
  }
@@ -1003,30 +1127,57 @@ var Recipe = class _Recipe {
1003
1127
  throw new Error("Error scaling recipe: no initial servings value set");
1004
1128
  }
1005
1129
  newRecipe.ingredients = newRecipe.ingredients.map((ingredient) => {
1006
- if (ingredient.quantity && !(ingredient.quantity.type === "fixed" && ingredient.quantity.value.type === "text")) {
1007
- ingredient.quantity = multiplyQuantityValue(
1008
- ingredient.quantity,
1009
- 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
+ }
1010
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
+ }
1011
1156
  }
1012
1157
  return ingredient;
1013
1158
  }).filter((ingredient) => ingredient.quantity !== null);
1014
1159
  newRecipe.servings = originalServings * factor;
1015
1160
  if (newRecipe.metadata.servings && this.metadata.servings) {
1016
- const servingsValue = parseFloat(this.metadata.servings);
1017
- if (!isNaN(servingsValue)) {
1161
+ if (floatRegex.test(String(this.metadata.servings).replace(",", ".").trim())) {
1162
+ const servingsValue = parseFloat(
1163
+ String(this.metadata.servings).replace(",", ".")
1164
+ );
1018
1165
  newRecipe.metadata.servings = String(servingsValue * factor);
1019
1166
  }
1020
1167
  }
1021
1168
  if (newRecipe.metadata.yield && this.metadata.yield) {
1022
- const yieldValue = parseFloat(this.metadata.yield);
1023
- if (!isNaN(yieldValue)) {
1169
+ if (floatRegex.test(String(this.metadata.yield).replace(",", ".").trim())) {
1170
+ const yieldValue = parseFloat(
1171
+ String(this.metadata.yield).replace(",", ".")
1172
+ );
1024
1173
  newRecipe.metadata.yield = String(yieldValue * factor);
1025
1174
  }
1026
1175
  }
1027
1176
  if (newRecipe.metadata.serves && this.metadata.serves) {
1028
- const servesValue = parseFloat(this.metadata.serves);
1029
- if (!isNaN(servesValue)) {
1177
+ if (floatRegex.test(String(this.metadata.serves).replace(",", ".").trim())) {
1178
+ const servesValue = parseFloat(
1179
+ String(this.metadata.serves).replace(",", ".")
1180
+ );
1030
1181
  newRecipe.metadata.serves = String(servesValue * factor);
1031
1182
  }
1032
1183
  }
@@ -1095,7 +1246,7 @@ var ShoppingList = class {
1095
1246
  for (const { recipe, factor } of this.recipes) {
1096
1247
  const scaledRecipe = factor === 1 ? recipe : recipe.scaleBy(factor);
1097
1248
  for (const ingredient of scaledRecipe.ingredients) {
1098
- if (ingredient.hidden) {
1249
+ if (ingredient.flags && ingredient.flags.includes("hidden")) {
1099
1250
  continue;
1100
1251
  }
1101
1252
  const existingIngredient = this.ingredients.find(
@@ -1103,8 +1254,8 @@ var ShoppingList = class {
1103
1254
  );
1104
1255
  let addSeparate = false;
1105
1256
  try {
1106
- if (existingIngredient) {
1107
- if (existingIngredient.quantity && ingredient.quantity) {
1257
+ if (existingIngredient && ingredient.quantity) {
1258
+ if (existingIngredient.quantity) {
1108
1259
  const newQuantity = addQuantities(
1109
1260
  {
1110
1261
  value: existingIngredient.quantity,
@@ -1119,7 +1270,7 @@ var ShoppingList = class {
1119
1270
  if (newQuantity.unit) {
1120
1271
  existingIngredient.unit = newQuantity.unit;
1121
1272
  }
1122
- } else if (ingredient.quantity) {
1273
+ } else {
1123
1274
  existingIngredient.quantity = ingredient.quantity;
1124
1275
  if (ingredient.unit) {
1125
1276
  existingIngredient.unit = ingredient.unit;
@@ -1219,4 +1370,8 @@ var ShoppingList = class {
1219
1370
  Section,
1220
1371
  ShoppingList
1221
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 */
1222
1377
  //# sourceMappingURL=index.cjs.map