@tmlmt/cooklang-parser 3.0.0-alpha.7 → 3.0.0-alpha.9

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
@@ -39,7 +39,8 @@ __export(index_exports, {
39
39
  Recipe: () => Recipe,
40
40
  Section: () => Section,
41
41
  ShoppingCart: () => ShoppingCart,
42
- ShoppingList: () => ShoppingList
42
+ ShoppingList: () => ShoppingList,
43
+ isAlternativeSelected: () => isAlternativeSelected
43
44
  });
44
45
  module.exports = __toCommonJS(index_exports);
45
46
 
@@ -654,13 +655,13 @@ var InvalidQuantityFormat = class extends Error {
654
655
 
655
656
  // src/utils/type_guards.ts
656
657
  function isGroup(x) {
657
- return x && "type" in x;
658
+ return "and" in x || "or" in x;
658
659
  }
659
660
  function isOrGroup(x) {
660
- return isGroup(x) && x.type === "or";
661
+ return isGroup(x) && "or" in x;
661
662
  }
662
663
  function isAndGroup(x) {
663
- return isGroup(x) && x.type === "and";
664
+ return isGroup(x) && "and" in x;
664
665
  }
665
666
  function isQuantity(x) {
666
667
  return x && typeof x === "object" && "quantity" in x;
@@ -679,8 +680,10 @@ function isValueIntegerLike(q) {
679
680
 
680
681
  // src/quantities/mutations.ts
681
682
  function extendAllUnits(q) {
682
- if (isGroup(q)) {
683
- return { ...q, entries: q.entries.map(extendAllUnits) };
683
+ if (isAndGroup(q)) {
684
+ return { and: q.and.map(extendAllUnits) };
685
+ } else if (isOrGroup(q)) {
686
+ return { or: q.or.map(extendAllUnits) };
684
687
  } else {
685
688
  const newQ = {
686
689
  quantity: q.quantity
@@ -692,13 +695,23 @@ function extendAllUnits(q) {
692
695
  }
693
696
  }
694
697
  function normalizeAllUnits(q) {
695
- if (isGroup(q)) {
696
- return { ...q, entries: q.entries.map(normalizeAllUnits) };
698
+ if (isAndGroup(q)) {
699
+ return { and: q.and.map(normalizeAllUnits) };
700
+ } else if (isOrGroup(q)) {
701
+ return { or: q.or.map(normalizeAllUnits) };
697
702
  } else {
698
703
  const newQ = {
699
704
  quantity: q.quantity,
700
705
  unit: resolveUnit(q.unit)
701
706
  };
707
+ if (q.equivalents && q.equivalents.length > 0) {
708
+ const equivalentsNormalized = q.equivalents.map(
709
+ (eq) => normalizeAllUnits(eq)
710
+ );
711
+ return {
712
+ or: [newQ, ...equivalentsNormalized]
713
+ };
714
+ }
702
715
  return newQ;
703
716
  }
704
717
  }
@@ -783,23 +796,23 @@ function addQuantities(q1, q2) {
783
796
  function toPlainUnit(quantity) {
784
797
  if (isQuantity(quantity))
785
798
  return quantity.unit ? { ...quantity, unit: quantity.unit.name } : quantity;
786
- else {
799
+ else if (isOrGroup(quantity)) {
787
800
  return {
788
- ...quantity,
789
- entries: quantity.entries.map(toPlainUnit)
801
+ or: quantity.or.map(toPlainUnit)
802
+ };
803
+ } else {
804
+ return {
805
+ and: quantity.and.map(toPlainUnit)
790
806
  };
791
807
  }
792
808
  }
793
809
  function toExtendedUnit(q) {
794
810
  if (isQuantity(q)) {
795
811
  return q.unit ? { ...q, unit: { name: q.unit } } : q;
812
+ } else if (isOrGroup(q)) {
813
+ return { or: q.or.map(toExtendedUnit) };
796
814
  } else {
797
- return {
798
- ...q,
799
- entries: q.entries.map(
800
- (entry) => isQuantity(entry) ? toExtendedUnit(entry) : toExtendedUnit(entry)
801
- )
802
- };
815
+ return { and: q.and.map(toExtendedUnit) };
803
816
  }
804
817
  }
805
818
  function deNormalizeQuantity(q) {
@@ -813,26 +826,24 @@ function deNormalizeQuantity(q) {
813
826
  }
814
827
  var flattenPlainUnitGroup = (summed) => {
815
828
  if (isOrGroup(summed)) {
816
- const entries = summed.entries;
829
+ const entries = summed.or;
817
830
  const andGroupEntry = entries.find(
818
- (e2) => isGroup(e2) && e2.type === "and"
831
+ (e2) => isAndGroup(e2)
819
832
  );
820
833
  if (andGroupEntry) {
821
834
  const andEntries = [];
822
- for (const entry of andGroupEntry.entries) {
823
- if (isQuantity(entry)) {
824
- andEntries.push({
825
- quantity: entry.quantity,
826
- unit: entry.unit
827
- });
828
- }
835
+ const addGroupEntryContent = andGroupEntry.and;
836
+ for (const entry of addGroupEntryContent) {
837
+ andEntries.push({
838
+ quantity: entry.quantity,
839
+ ...entry.unit && { unit: entry.unit }
840
+ });
829
841
  }
830
842
  const equivalentsList = entries.filter((e2) => isQuantity(e2)).map((e2) => ({ quantity: e2.quantity, unit: e2.unit }));
831
843
  if (equivalentsList.length > 0) {
832
844
  return [
833
845
  {
834
- type: "and",
835
- entries: andEntries,
846
+ and: andEntries,
836
847
  equivalents: equivalentsList
837
848
  }
838
849
  ];
@@ -856,25 +867,21 @@ var flattenPlainUnitGroup = (summed) => {
856
867
  const first = entries[0];
857
868
  return [{ quantity: first.quantity, unit: first.unit }];
858
869
  }
859
- } else if (isGroup(summed)) {
870
+ } else if (isAndGroup(summed)) {
860
871
  const andEntries = [];
861
872
  const equivalentsList = [];
862
- for (const entry of summed.entries) {
873
+ for (const entry of summed.and) {
863
874
  if (isOrGroup(entry)) {
864
- const orEntries = entry.entries.filter(
865
- (e2) => isQuantity(e2)
866
- );
867
- if (orEntries.length > 0) {
868
- andEntries.push({
869
- quantity: orEntries[0].quantity,
870
- unit: orEntries[0].unit
871
- });
872
- equivalentsList.push(...orEntries.slice(1));
873
- }
875
+ const orEntries = entry.or;
876
+ andEntries.push({
877
+ quantity: orEntries[0].quantity,
878
+ ...orEntries[0].unit && { unit: orEntries[0].unit }
879
+ });
880
+ equivalentsList.push(...orEntries.slice(1));
874
881
  } else if (isQuantity(entry)) {
875
882
  andEntries.push({
876
883
  quantity: entry.quantity,
877
- unit: entry.unit
884
+ ...entry.unit && { unit: entry.unit }
878
885
  });
879
886
  }
880
887
  }
@@ -882,13 +889,14 @@ var flattenPlainUnitGroup = (summed) => {
882
889
  return andEntries;
883
890
  }
884
891
  const result = {
885
- type: "and",
886
- entries: andEntries,
892
+ and: andEntries,
887
893
  equivalents: equivalentsList
888
894
  };
889
895
  return [result];
890
896
  } else {
891
- return [{ quantity: summed.quantity, unit: summed.unit }];
897
+ return [
898
+ { quantity: summed.quantity, ...summed.unit && { unit: summed.unit } }
899
+ ];
892
900
  }
893
901
  };
894
902
 
@@ -1371,11 +1379,11 @@ var deepClone = (v) => typeof structuredClone === "function" ? structuredClone(v
1371
1379
  // src/quantities/alternatives.ts
1372
1380
  function getEquivalentUnitsLists(...quantities) {
1373
1381
  const quantitiesCopy = deepClone(quantities);
1374
- const OrGroups = quantitiesCopy.filter(isOrGroup).filter((q) => q.entries.length > 1);
1382
+ const OrGroups = quantitiesCopy.filter(isOrGroup).filter((q) => q.or.length > 1);
1375
1383
  const unitLists = [];
1376
1384
  const normalizeOrGroup = (og) => ({
1377
1385
  ...og,
1378
- entries: og.entries.map((q) => ({
1386
+ or: og.or.map((q) => ({
1379
1387
  ...q,
1380
1388
  unit: resolveUnit(q.unit?.name, q.unit?.integerProtected)
1381
1389
  }))
@@ -1397,7 +1405,7 @@ function getEquivalentUnitsLists(...quantities) {
1397
1405
  ...v,
1398
1406
  unit: resolveUnit(v.unit?.name, v.unit?.integerProtected)
1399
1407
  };
1400
- const commonQuantity = og.entries.find(
1408
+ const commonQuantity = og.or.find(
1401
1409
  (q) => isQuantity(q) && areUnitsCompatible(q.unit, normalizedV.unit)
1402
1410
  );
1403
1411
  if (commonQuantity) {
@@ -1406,7 +1414,7 @@ function getEquivalentUnitsLists(...quantities) {
1406
1414
  }
1407
1415
  return acc;
1408
1416
  }, []);
1409
- for (const newQ of og.entries) {
1417
+ for (const newQ of og.or) {
1410
1418
  if (commonUnitList.some((q) => areUnitsCompatible(q.unit, newQ.unit))) {
1411
1419
  continue;
1412
1420
  } else {
@@ -1417,10 +1425,10 @@ function getEquivalentUnitsLists(...quantities) {
1417
1425
  }
1418
1426
  for (const orGroup of OrGroups) {
1419
1427
  const orGroupModified = normalizeOrGroup(orGroup);
1420
- const units2 = orGroupModified.entries.map((q) => q.unit);
1428
+ const units2 = orGroupModified.or.map((q) => q.unit);
1421
1429
  const linkIndex = findLinkIndexForUnits(unitLists, units2);
1422
1430
  if (linkIndex === -1) {
1423
- unitLists.push(orGroupModified.entries);
1431
+ unitLists.push(orGroupModified.or);
1424
1432
  } else {
1425
1433
  mergeOrGroupIntoList(unitLists, linkIndex, orGroupModified);
1426
1434
  }
@@ -1516,7 +1524,7 @@ function reduceOrsToFirstEquivalent(unitList, quantities) {
1516
1524
  return quantities.map((q) => {
1517
1525
  if (isQuantity(q)) return reduceToQuantity(q);
1518
1526
  const qListModified = sortUnitList(
1519
- q.entries.map((qq) => ({
1527
+ q.or.map((qq) => ({
1520
1528
  ...qq,
1521
1529
  unit: resolveUnit(qq.unit?.name, qq.unit?.integerProtected)
1522
1530
  }))
@@ -1562,10 +1570,10 @@ function addQuantitiesOrGroups(...quantities) {
1562
1570
  if (sum.length === 1) {
1563
1571
  return { sum: sum[0], unitsLists };
1564
1572
  }
1565
- return { sum: { type: "and", entries: sum }, unitsLists };
1573
+ return { sum: { and: sum }, unitsLists };
1566
1574
  }
1567
1575
  function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
1568
- const sumQuantities = isGroup(sum) ? sum.entries : [sum];
1576
+ const sumQuantities = isAndGroup(sum) ? sum.and : [sum];
1569
1577
  const result = [];
1570
1578
  const processedQuantities = /* @__PURE__ */ new Set();
1571
1579
  for (const list of unitsLists) {
@@ -1608,12 +1616,10 @@ function regroupQuantitiesAndExpandEquivalents(sum, unitsLists) {
1608
1616
  });
1609
1617
  if (main.length + equivalents.length > 1) {
1610
1618
  const resultMain = main.length > 1 ? {
1611
- type: "and",
1612
- entries: main.map(deNormalizeQuantity)
1619
+ and: main.map(deNormalizeQuantity)
1613
1620
  } : deNormalizeQuantity(main[0]);
1614
1621
  result.push({
1615
- type: "or",
1616
- entries: [resultMain, ...equivalents]
1622
+ or: [resultMain, ...equivalents]
1617
1623
  });
1618
1624
  } else {
1619
1625
  result.push(deNormalizeQuantity(main[0]));
@@ -1631,7 +1637,7 @@ function addEquivalentsAndSimplify(...quantities) {
1631
1637
  if (regrouped.length === 1) {
1632
1638
  return toPlainUnit(regrouped[0]);
1633
1639
  } else {
1634
- return { type: "and", entries: regrouped.map(toPlainUnit) };
1640
+ return { and: regrouped.map(toPlainUnit) };
1635
1641
  }
1636
1642
  }
1637
1643
 
@@ -1648,8 +1654,7 @@ var _Recipe = class _Recipe {
1648
1654
  */
1649
1655
  __publicField(this, "metadata", {});
1650
1656
  /**
1651
- * The default or manual choice of alternative ingredients.
1652
- * Contains the full context including alternatives list and active selection index.
1657
+ * The possible choices of alternative ingredients for this recipe.
1653
1658
  */
1654
1659
  __publicField(this, "choices", {
1655
1660
  ingredientItems: /* @__PURE__ */ new Map(),
@@ -1712,7 +1717,7 @@ var _Recipe = class _Recipe {
1712
1717
  if (!regexMatchGroups || !regexMatchGroups.arbitraryQuantity) return;
1713
1718
  const quantityMatch = regexMatchGroups.arbitraryQuantity?.trim().match(quantityAlternativeRegex);
1714
1719
  if (quantityMatch?.groups) {
1715
- const value = quantityMatch.groups.quantity ? parseQuantityInput(quantityMatch.groups.quantity) : void 0;
1720
+ const value = parseQuantityInput(quantityMatch.groups.quantity);
1716
1721
  const unit = quantityMatch.groups.unit;
1717
1722
  const name = regexMatchGroups.arbitraryName || void 0;
1718
1723
  if (!value || value.type === "fixed" && value.value.type === "text") {
@@ -2015,286 +2020,229 @@ var _Recipe = class _Recipe {
2015
2020
  * @internal
2016
2021
  */
2017
2022
  _populate_ingredient_quantities() {
2018
- this.ingredients = this.ingredients.map((ing) => {
2019
- if (ing.quantities) {
2020
- delete ing.quantities;
2023
+ for (const ing of this.ingredients) {
2024
+ delete ing.quantities;
2025
+ delete ing.usedAsPrimary;
2026
+ }
2027
+ const ingredientsWithQuantities = this.getIngredientQuantities();
2028
+ const matchedIndices = /* @__PURE__ */ new Set();
2029
+ for (const computed of ingredientsWithQuantities) {
2030
+ const idx = this.ingredients.findIndex(
2031
+ (ing2, i2) => ing2.name === computed.name && !matchedIndices.has(i2)
2032
+ );
2033
+ matchedIndices.add(idx);
2034
+ const ing = this.ingredients[idx];
2035
+ if (computed.quantities) {
2036
+ ing.quantities = computed.quantities;
2021
2037
  }
2022
- if (ing.usedAsPrimary) {
2023
- delete ing.usedAsPrimary;
2038
+ if (computed.usedAsPrimary) {
2039
+ ing.usedAsPrimary = true;
2024
2040
  }
2025
- return ing;
2026
- });
2027
- const seenGroups = /* @__PURE__ */ new Set();
2041
+ }
2042
+ }
2043
+ /**
2044
+ * Gets ingredients with their quantities populated, optionally filtered by section/step
2045
+ * and respecting user choices for alternatives.
2046
+ *
2047
+ * When no options are provided, returns all recipe ingredients with quantities
2048
+ * calculated using primary alternatives (same as after parsing).
2049
+ *
2050
+ * @param options - Options for filtering and choice selection:
2051
+ * - `section`: Filter to a specific section (Section object or 0-based index)
2052
+ * - `step`: Filter to a specific step (Step object or 0-based index)
2053
+ * - `choices`: Choices for alternative ingredients (defaults to primary)
2054
+ * @returns Array of Ingredient objects with quantities populated
2055
+ *
2056
+ * @example
2057
+ * ```typescript
2058
+ * // Get all ingredients with primary alternatives
2059
+ * const ingredients = recipe.getIngredientQuantities();
2060
+ *
2061
+ * // Get ingredients for a specific section
2062
+ * const sectionIngredients = recipe.getIngredientQuantities({ section: 0 });
2063
+ *
2064
+ * // Get ingredients with specific choices applied
2065
+ * const withChoices = recipe.getIngredientQuantities({
2066
+ * choices: { ingredientItems: new Map([['ingredient-item-2', 1]]) }
2067
+ * });
2068
+ * ```
2069
+ */
2070
+ getIngredientQuantities(options) {
2071
+ const { section, step, choices } = options || {};
2072
+ const sectionsToProcess = section !== void 0 ? (() => {
2073
+ const idx = typeof section === "number" ? section : this.sections.indexOf(section);
2074
+ return idx >= 0 && idx < this.sections.length ? [this.sections[idx]] : [];
2075
+ })() : this.sections;
2028
2076
  const ingredientGroups = /* @__PURE__ */ new Map();
2029
- for (const section of this.sections) {
2030
- for (const step of section.content.filter(
2077
+ const selectedIndices = /* @__PURE__ */ new Set();
2078
+ const referencedIndices = /* @__PURE__ */ new Set();
2079
+ for (const currentSection of sectionsToProcess) {
2080
+ const allSteps = currentSection.content.filter(
2031
2081
  (item) => item.type === "step"
2032
- )) {
2033
- for (const item of step.items.filter(
2082
+ );
2083
+ const stepsToProcess = step === void 0 ? allSteps : typeof step === "number" ? step >= 0 && step < allSteps.length ? [allSteps[step]] : [] : allSteps.includes(step) ? [step] : [];
2084
+ for (const currentStep of stepsToProcess) {
2085
+ for (const item of currentStep.items.filter(
2034
2086
  (item2) => item2.type === "ingredient"
2035
2087
  )) {
2036
- const isGroupedItem = "group" in item && item.group !== void 0;
2037
- const isFirstInGroup = isGroupedItem && !seenGroups.has(item.group);
2038
- if (isGroupedItem) {
2039
- seenGroups.add(item.group);
2088
+ const isGrouped = "group" in item && item.group !== void 0;
2089
+ const groupAlternatives = isGrouped ? this.choices.ingredientGroups.get(item.group) : void 0;
2090
+ let selectedAltIndex = 0;
2091
+ let isSelected = false;
2092
+ let hasExplicitChoice = false;
2093
+ if (isGrouped) {
2094
+ const groupChoice = choices?.ingredientGroups?.get(item.group);
2095
+ hasExplicitChoice = groupChoice !== void 0;
2096
+ const targetIndex = groupChoice ?? 0;
2097
+ isSelected = groupAlternatives?.[targetIndex]?.itemId === item.id;
2098
+ } else {
2099
+ const itemChoice = choices?.ingredientItems?.get(item.id);
2100
+ hasExplicitChoice = itemChoice !== void 0;
2101
+ selectedAltIndex = itemChoice ?? 0;
2102
+ isSelected = true;
2040
2103
  }
2041
- const isPrimary = !isGroupedItem || isFirstInGroup;
2042
- const alternative = item.alternatives[0];
2043
- if (isPrimary) {
2044
- const primaryIngredient = this.ingredients[alternative.index];
2045
- if (primaryIngredient) {
2046
- primaryIngredient.usedAsPrimary = true;
2047
- }
2104
+ const alternative = item.alternatives[selectedAltIndex];
2105
+ if (!alternative || !isSelected) continue;
2106
+ selectedIndices.add(alternative.index);
2107
+ const allAlts = isGrouped ? groupAlternatives : item.alternatives;
2108
+ for (const alt of allAlts) {
2109
+ referencedIndices.add(alt.index);
2048
2110
  }
2049
- if (!isPrimary || !alternative.itemQuantity) continue;
2050
- const allQuantities = [
2051
- {
2052
- quantity: alternative.itemQuantity.quantity,
2111
+ if (!alternative.itemQuantity) continue;
2112
+ const baseQty = {
2113
+ quantity: alternative.itemQuantity.quantity,
2114
+ ...alternative.itemQuantity.unit && {
2053
2115
  unit: alternative.itemQuantity.unit
2054
2116
  }
2055
- ];
2056
- if (alternative.itemQuantity.equivalents) {
2057
- allQuantities.push(...alternative.itemQuantity.equivalents);
2058
- }
2059
- const quantityEntry = allQuantities.length === 1 ? allQuantities[0] : { type: "or", entries: allQuantities };
2060
- const hasInlineAlternatives = item.alternatives.length > 1;
2061
- const hasGroupedAlternatives = isGroupedItem && this.choices.ingredientGroups.has(item.group);
2117
+ };
2118
+ const quantityEntry = alternative.itemQuantity.equivalents?.length ? { or: [baseQty, ...alternative.itemQuantity.equivalents] } : baseQty;
2062
2119
  let alternativeRefs;
2063
- if (hasInlineAlternatives) {
2064
- alternativeRefs = [];
2065
- for (let j = 1; j < item.alternatives.length; j++) {
2066
- const otherAlt = item.alternatives[j];
2067
- const newRef = {
2068
- index: otherAlt.index
2069
- };
2120
+ if (!hasExplicitChoice && allAlts.length > 1) {
2121
+ alternativeRefs = allAlts.filter(
2122
+ (alt) => isGrouped ? alt.itemId !== item.id : alt.index !== alternative.index
2123
+ ).map((otherAlt) => {
2124
+ const ref = { index: otherAlt.index };
2070
2125
  if (otherAlt.itemQuantity) {
2071
2126
  const altQty = {
2072
- quantity: otherAlt.itemQuantity.quantity
2127
+ quantity: otherAlt.itemQuantity.quantity,
2128
+ ...otherAlt.itemQuantity.unit && {
2129
+ unit: otherAlt.itemQuantity.unit.name
2130
+ },
2131
+ ...otherAlt.itemQuantity.equivalents && {
2132
+ equivalents: otherAlt.itemQuantity.equivalents.map(
2133
+ (eq) => toPlainUnit(eq)
2134
+ )
2135
+ }
2073
2136
  };
2074
- if (otherAlt.itemQuantity.unit) {
2075
- altQty.unit = otherAlt.itemQuantity.unit.name;
2076
- }
2077
- if (otherAlt.itemQuantity.equivalents) {
2078
- altQty.equivalents = otherAlt.itemQuantity.equivalents.map(
2079
- (eq) => toPlainUnit(eq)
2080
- );
2081
- }
2082
- newRef.quantities = [altQty];
2137
+ ref.quantities = [altQty];
2083
2138
  }
2084
- alternativeRefs.push(newRef);
2085
- }
2086
- } else if (hasGroupedAlternatives) {
2087
- const groupAlternatives = this.choices.ingredientGroups.get(
2088
- item.group
2139
+ return ref;
2140
+ });
2141
+ }
2142
+ const altIndices = getAlternativeSignature(alternativeRefs) ?? "";
2143
+ let signature;
2144
+ if (isGrouped) {
2145
+ const resolvedUnit = resolveUnit(
2146
+ alternative.itemQuantity.unit?.name
2089
2147
  );
2090
- alternativeRefs = [];
2091
- for (let j = 1; j < groupAlternatives.length; j++) {
2092
- const otherAlt = groupAlternatives[j];
2093
- if (otherAlt.itemQuantity) {
2094
- const altQty = {
2095
- quantity: otherAlt.itemQuantity.quantity
2096
- };
2097
- if (otherAlt.itemQuantity.unit) {
2098
- altQty.unit = otherAlt.itemQuantity.unit.name;
2099
- }
2100
- if (otherAlt.itemQuantity.equivalents) {
2101
- altQty.equivalents = otherAlt.itemQuantity.equivalents.map(
2102
- (eq) => toPlainUnit(eq)
2103
- );
2104
- }
2105
- alternativeRefs.push({
2106
- index: otherAlt.index,
2107
- quantities: [altQty]
2108
- });
2109
- }
2110
- }
2111
- if (alternativeRefs.length === 0) {
2112
- alternativeRefs = void 0;
2113
- }
2148
+ signature = `group:${item.group}|${altIndices}|${resolvedUnit.type}`;
2149
+ } else if (altIndices) {
2150
+ const resolvedUnit = resolveUnit(
2151
+ alternative.itemQuantity.unit?.name
2152
+ );
2153
+ signature = `${altIndices}|${resolvedUnit.type}}`;
2154
+ } else {
2155
+ signature = null;
2114
2156
  }
2115
2157
  if (!ingredientGroups.has(alternative.index)) {
2116
2158
  ingredientGroups.set(alternative.index, /* @__PURE__ */ new Map());
2117
2159
  }
2118
- const groupsForIngredient = ingredientGroups.get(alternative.index);
2119
- const baseSignature = getAlternativeSignature(alternativeRefs);
2120
- const signature = isGroupedItem ? `group:${item.group}|${baseSignature ?? ""}` : baseSignature;
2121
- if (!groupsForIngredient.has(signature)) {
2122
- groupsForIngredient.set(signature, {
2123
- alternativeQuantities: /* @__PURE__ */ new Map(),
2124
- quantities: []
2160
+ const groupsForIng = ingredientGroups.get(alternative.index);
2161
+ if (!groupsForIng.has(signature)) {
2162
+ groupsForIng.set(signature, {
2163
+ quantities: [],
2164
+ alternativeQuantities: /* @__PURE__ */ new Map()
2125
2165
  });
2126
2166
  }
2127
- const group = groupsForIngredient.get(signature);
2167
+ const group = groupsForIng.get(signature);
2128
2168
  group.quantities.push(quantityEntry);
2129
- if (alternativeRefs) {
2130
- for (const ref of alternativeRefs) {
2131
- if (!group.alternativeQuantities.has(ref.index)) {
2132
- group.alternativeQuantities.set(ref.index, []);
2133
- }
2134
- if (ref.quantities && ref.quantities.length > 0) {
2135
- for (const altQty of ref.quantities) {
2136
- if (altQty.equivalents && altQty.equivalents.length > 0) {
2137
- const entries = [
2138
- toExtendedUnit({
2139
- quantity: altQty.quantity,
2140
- unit: altQty.unit
2141
- }),
2142
- ...altQty.equivalents.map((eq) => toExtendedUnit(eq))
2143
- ];
2144
- group.alternativeQuantities.get(ref.index).push({ type: "or", entries });
2145
- } else {
2146
- group.alternativeQuantities.get(ref.index).push(
2147
- toExtendedUnit({
2148
- quantity: altQty.quantity,
2149
- unit: altQty.unit
2150
- })
2151
- );
2152
- }
2153
- }
2154
- }
2169
+ for (const ref of alternativeRefs ?? []) {
2170
+ if (!group.alternativeQuantities.has(ref.index)) {
2171
+ group.alternativeQuantities.set(ref.index, []);
2155
2172
  }
2156
- }
2157
- }
2158
- }
2159
- }
2160
- for (const [index, groupsForIngredient] of ingredientGroups) {
2161
- const ingredient = this.ingredients[index];
2162
- const quantityGroups = [];
2163
- for (const [, group] of groupsForIngredient) {
2164
- const summedGroupQuantity = addEquivalentsAndSimplify(
2165
- ...group.quantities
2166
- );
2167
- const groupQuantities = flattenPlainUnitGroup(summedGroupQuantity);
2168
- let alternatives;
2169
- if (group.alternativeQuantities.size > 0) {
2170
- alternatives = [];
2171
- for (const [altIndex, altQuantities] of group.alternativeQuantities) {
2172
- const ref = { index: altIndex };
2173
- if (altQuantities.length > 0) {
2174
- const summedAltQuantity = addEquivalentsAndSimplify(
2175
- ...altQuantities
2176
- );
2177
- const flattenedAlt = flattenPlainUnitGroup(summedAltQuantity);
2178
- ref.quantities = flattenedAlt.flatMap((item) => {
2179
- if ("quantity" in item) {
2180
- return [item];
2181
- } else {
2182
- return item.entries;
2183
- }
2173
+ for (const altQty of ref.quantities ?? []) {
2174
+ const extended = toExtendedUnit({
2175
+ quantity: altQty.quantity,
2176
+ unit: altQty.unit
2184
2177
  });
2185
- }
2186
- alternatives.push(ref);
2187
- }
2188
- }
2189
- for (const gq of groupQuantities) {
2190
- if ("type" in gq && gq.type === "and") {
2191
- const andGroup = {
2192
- type: "and",
2193
- entries: gq.entries
2194
- };
2195
- if (gq.equivalents && gq.equivalents.length > 0) {
2196
- andGroup.equivalents = gq.equivalents;
2197
- }
2198
- if (alternatives && alternatives.length > 0) {
2199
- andGroup.alternatives = alternatives;
2200
- }
2201
- quantityGroups.push(andGroup);
2202
- } else {
2203
- const quantityGroup = gq;
2204
- if (alternatives && alternatives.length > 0) {
2205
- quantityGroup.alternatives = alternatives;
2206
- }
2207
- quantityGroups.push(quantityGroup);
2208
- }
2209
- }
2210
- }
2211
- if (quantityGroups.length > 0) {
2212
- ingredient.quantities = quantityGroups;
2213
- }
2214
- }
2215
- }
2216
- /**
2217
- * Calculates ingredient quantities based on the provided choices.
2218
- * Returns a list of computed ingredients with their total quantities.
2219
- *
2220
- * @param choices - The recipe choices to apply when computing quantities.
2221
- * If not provided, uses the default choices (first alternative for each item).
2222
- * @returns An array of ComputedIngredient with quantityTotal calculated based on choices.
2223
- */
2224
- calc_ingredient_quantities(choices) {
2225
- const effectiveChoices = choices || {
2226
- ingredientItems: new Map(
2227
- Array.from(this.choices.ingredientItems.keys()).map((k) => [k, 0])
2228
- ),
2229
- ingredientGroups: new Map(
2230
- Array.from(this.choices.ingredientGroups.keys()).map((k) => [k, 0])
2231
- )
2232
- };
2233
- const ingredientQuantities = /* @__PURE__ */ new Map();
2234
- const selectedIngredientIndices = /* @__PURE__ */ new Set();
2235
- for (const section of this.sections) {
2236
- for (const step of section.content.filter(
2237
- (item) => item.type === "step"
2238
- )) {
2239
- for (const item of step.items.filter(
2240
- (item2) => item2.type === "ingredient"
2241
- )) {
2242
- for (let i2 = 0; i2 < item.alternatives.length; i2++) {
2243
- const alternative = item.alternatives[i2];
2244
- const isAlternativeChoiceItem = effectiveChoices.ingredientItems?.get(item.id) === i2;
2245
- const alternativeChoiceGroupIdx = item.group ? effectiveChoices.ingredientGroups?.get(item.group) : void 0;
2246
- const alternativeChoiceGroup = item.group ? this.choices.ingredientGroups.get(item.group) : void 0;
2247
- const isAlternativeChoiceGroup = alternativeChoiceGroup && alternativeChoiceGroupIdx !== void 0 ? alternativeChoiceGroup[alternativeChoiceGroupIdx]?.itemId === item.id : false;
2248
- const isSelected = !("group" in item) && (item.alternatives.length === 1 || isAlternativeChoiceItem) || isAlternativeChoiceGroup;
2249
- if (isSelected) {
2250
- selectedIngredientIndices.add(alternative.index);
2251
- if (alternative.itemQuantity) {
2252
- const allQuantities = [
2253
- {
2254
- quantity: alternative.itemQuantity.quantity,
2255
- unit: alternative.itemQuantity.unit
2256
- }
2178
+ if (altQty.equivalents?.length) {
2179
+ const eqEntries = [
2180
+ extended,
2181
+ ...altQty.equivalents.map((eq) => toExtendedUnit(eq))
2257
2182
  ];
2258
- if (alternative.itemQuantity.equivalents) {
2259
- allQuantities.push(...alternative.itemQuantity.equivalents);
2260
- }
2261
- const equivalents = allQuantities.length === 1 ? allQuantities[0] : {
2262
- type: "or",
2263
- entries: allQuantities
2264
- };
2265
- ingredientQuantities.set(alternative.index, [
2266
- ...ingredientQuantities.get(alternative.index) || [],
2267
- equivalents
2268
- ]);
2183
+ group.alternativeQuantities.get(ref.index).push({ or: eqEntries });
2184
+ } else {
2185
+ group.alternativeQuantities.get(ref.index).push(extended);
2269
2186
  }
2270
2187
  }
2271
2188
  }
2272
2189
  }
2273
2190
  }
2274
2191
  }
2275
- const computedIngredients = [];
2192
+ const result = [];
2276
2193
  for (let index = 0; index < this.ingredients.length; index++) {
2277
- if (!selectedIngredientIndices.has(index)) continue;
2278
- const ing = this.ingredients[index];
2279
- const computed = {
2280
- name: ing.name
2194
+ if (!referencedIndices.has(index)) continue;
2195
+ const orig = this.ingredients[index];
2196
+ const ing = {
2197
+ name: orig.name,
2198
+ ...orig.preparation && { preparation: orig.preparation },
2199
+ ...orig.flags && { flags: orig.flags },
2200
+ ...orig.extras && { extras: orig.extras }
2281
2201
  };
2282
- if (ing.preparation) {
2283
- computed.preparation = ing.preparation;
2284
- }
2285
- if (ing.flags) {
2286
- computed.flags = ing.flags;
2287
- }
2288
- if (ing.extras) {
2289
- computed.extras = ing.extras;
2290
- }
2291
- const quantities = ingredientQuantities.get(index);
2292
- if (quantities && quantities.length > 0) {
2293
- computed.quantityTotal = addEquivalentsAndSimplify(...quantities);
2202
+ if (selectedIndices.has(index)) {
2203
+ ing.usedAsPrimary = true;
2204
+ const groupsForIng = ingredientGroups.get(index);
2205
+ if (groupsForIng) {
2206
+ const quantityGroups = [];
2207
+ for (const [, group] of groupsForIng) {
2208
+ const summed = addEquivalentsAndSimplify(...group.quantities);
2209
+ const flattened = flattenPlainUnitGroup(summed);
2210
+ const alternatives = group.alternativeQuantities.size > 0 ? [...group.alternativeQuantities].map(([altIdx, altQtys]) => ({
2211
+ index: altIdx,
2212
+ ...altQtys.length > 0 && {
2213
+ quantities: flattenPlainUnitGroup(
2214
+ addEquivalentsAndSimplify(...altQtys)
2215
+ ).flatMap(
2216
+ /* v8 ignore next -- item.and branch requires complex nested AND-with-equivalents structure */
2217
+ (item) => "quantity" in item ? [item] : item.and
2218
+ )
2219
+ }
2220
+ })) : void 0;
2221
+ for (const gq of flattened) {
2222
+ if ("and" in gq) {
2223
+ quantityGroups.push({
2224
+ and: gq.and,
2225
+ ...gq.equivalents?.length && {
2226
+ equivalents: gq.equivalents
2227
+ },
2228
+ ...alternatives?.length && { alternatives }
2229
+ });
2230
+ } else {
2231
+ quantityGroups.push({
2232
+ ...gq,
2233
+ ...alternatives?.length && { alternatives }
2234
+ });
2235
+ }
2236
+ }
2237
+ }
2238
+ if (quantityGroups.length > 0) {
2239
+ ing.quantities = quantityGroups;
2240
+ }
2241
+ }
2294
2242
  }
2295
- computedIngredients.push(computed);
2243
+ result.push(ing);
2296
2244
  }
2297
- return computedIngredients;
2245
+ return result;
2298
2246
  }
2299
2247
  /**
2300
2248
  * Parses a recipe from a string.
@@ -2343,10 +2291,6 @@ var _Recipe = class _Recipe {
2343
2291
  }
2344
2292
  if (blankLineBefore && line.startsWith(">")) {
2345
2293
  flushPendingItems(section, items);
2346
- flushPendingNote(
2347
- section,
2348
- noteText ? this._parseNoteText(noteText) : []
2349
- );
2350
2294
  noteText = line.substring(1).trim();
2351
2295
  inNote = true;
2352
2296
  blankLineBefore = false;
@@ -2361,8 +2305,6 @@ var _Recipe = class _Recipe {
2361
2305
  blankLineBefore = false;
2362
2306
  continue;
2363
2307
  }
2364
- flushPendingNote(section, noteText ? this._parseNoteText(noteText) : []);
2365
- noteText = "";
2366
2308
  let cursor = 0;
2367
2309
  for (const match of line.matchAll(tokensRegex)) {
2368
2310
  const idx = match.index;
@@ -2627,7 +2569,7 @@ var ShoppingList = class {
2627
2569
  this.ingredients = [];
2628
2570
  const addIngredientQuantity = (name, quantityTotal) => {
2629
2571
  const quantityTotalExtended = extendAllUnits(quantityTotal);
2630
- const newQuantities = isAndGroup(quantityTotalExtended) ? quantityTotalExtended.entries : [quantityTotalExtended];
2572
+ const newQuantities = isAndGroup(quantityTotalExtended) ? quantityTotalExtended.and : [quantityTotalExtended];
2631
2573
  const existing = this.ingredients.find((i2) => i2.name === name);
2632
2574
  if (existing) {
2633
2575
  if (!existing.quantityTotal) {
@@ -2638,7 +2580,7 @@ var ShoppingList = class {
2638
2580
  const existingQuantityTotalExtended = extendAllUnits(
2639
2581
  existing.quantityTotal
2640
2582
  );
2641
- const existingQuantities = isAndGroup(existingQuantityTotalExtended) ? existingQuantityTotalExtended.entries : [existingQuantityTotalExtended];
2583
+ const existingQuantities = isAndGroup(existingQuantityTotalExtended) ? existingQuantityTotalExtended.and : [existingQuantityTotalExtended];
2642
2584
  existing.quantityTotal = addEquivalentsAndSimplify(
2643
2585
  ...existingQuantities,
2644
2586
  ...newQuantities
@@ -2660,15 +2602,43 @@ var ShoppingList = class {
2660
2602
  } else {
2661
2603
  scaledRecipe = addedRecipe.recipe.scaleTo(addedRecipe.servings);
2662
2604
  }
2663
- const computedIngredients = scaledRecipe.calc_ingredient_quantities(
2664
- addedRecipe.choices
2665
- );
2666
- for (const ingredient of computedIngredients) {
2605
+ const ingredients = scaledRecipe.getIngredientQuantities({
2606
+ choices: addedRecipe.choices
2607
+ });
2608
+ for (const ingredient of ingredients) {
2667
2609
  if (ingredient.flags && ingredient.flags.includes("hidden")) {
2668
2610
  continue;
2669
2611
  }
2670
- if (ingredient.quantityTotal) {
2671
- addIngredientQuantity(ingredient.name, ingredient.quantityTotal);
2612
+ if (!ingredient.usedAsPrimary) {
2613
+ continue;
2614
+ }
2615
+ if (ingredient.quantities && ingredient.quantities.length > 0) {
2616
+ const allQuantities = [];
2617
+ for (const qGroup of ingredient.quantities) {
2618
+ if ("and" in qGroup) {
2619
+ for (const qty of qGroup.and) {
2620
+ allQuantities.push(qty);
2621
+ }
2622
+ } else {
2623
+ const plainQty = {
2624
+ quantity: qGroup.quantity
2625
+ };
2626
+ if (qGroup.unit) plainQty.unit = qGroup.unit;
2627
+ if (qGroup.equivalents) plainQty.equivalents = qGroup.equivalents;
2628
+ allQuantities.push(plainQty);
2629
+ }
2630
+ }
2631
+ if (allQuantities.length === 1) {
2632
+ addIngredientQuantity(ingredient.name, allQuantities[0]);
2633
+ } else {
2634
+ const extendedQuantities = allQuantities.map(
2635
+ (q) => extendAllUnits(q)
2636
+ );
2637
+ const totalQuantity = addEquivalentsAndSimplify(
2638
+ ...extendedQuantities
2639
+ );
2640
+ addIngredientQuantity(ingredient.name, totalQuantity);
2641
+ }
2672
2642
  } else if (!this.ingredients.some((i2) => i2.name === ingredient.name)) {
2673
2643
  this.ingredients.push({ name: ingredient.name });
2674
2644
  }
@@ -2680,8 +2650,16 @@ var ShoppingList = class {
2680
2650
  * recalculates the quantities and recategorize the ingredients.
2681
2651
  * @param recipe - The recipe to add.
2682
2652
  * @param options - Options for adding the recipe.
2653
+ * @throws Error if the recipe has alternatives without corresponding choices.
2683
2654
  */
2684
2655
  add_recipe(recipe, options = {}) {
2656
+ const errorMessage = this.getUnresolvedAlternativesError(
2657
+ recipe,
2658
+ options.choices
2659
+ );
2660
+ if (errorMessage) {
2661
+ throw new Error(errorMessage);
2662
+ }
2685
2663
  if (!options.scaling) {
2686
2664
  this.recipes.push({
2687
2665
  recipe,
@@ -2706,6 +2684,41 @@ var ShoppingList = class {
2706
2684
  this.calculate_ingredients();
2707
2685
  this.categorize();
2708
2686
  }
2687
+ /**
2688
+ * Checks if a recipe has unresolved alternatives (alternatives without provided choices).
2689
+ * @param recipe - The recipe to check.
2690
+ * @param choices - The choices provided for the recipe.
2691
+ * @returns An error message if there are unresolved alternatives, undefined otherwise.
2692
+ */
2693
+ getUnresolvedAlternativesError(recipe, choices) {
2694
+ const missingItems = [];
2695
+ const missingGroups = [];
2696
+ for (const itemId of recipe.choices.ingredientItems.keys()) {
2697
+ if (!choices?.ingredientItems?.has(itemId)) {
2698
+ missingItems.push(itemId);
2699
+ }
2700
+ }
2701
+ for (const groupId of recipe.choices.ingredientGroups.keys()) {
2702
+ if (!choices?.ingredientGroups?.has(groupId)) {
2703
+ missingGroups.push(groupId);
2704
+ }
2705
+ }
2706
+ if (missingItems.length === 0 && missingGroups.length === 0) {
2707
+ return void 0;
2708
+ }
2709
+ const parts = [];
2710
+ if (missingItems.length > 0) {
2711
+ parts.push(
2712
+ `ingredientItems: [${missingItems.map((i2) => `'${i2}'`).join(", ")}]`
2713
+ );
2714
+ }
2715
+ if (missingGroups.length > 0) {
2716
+ parts.push(
2717
+ `ingredientGroups: [${missingGroups.map((g) => `'${g}'`).join(", ")}]`
2718
+ );
2719
+ }
2720
+ return `Recipe has unresolved alternatives. Missing choices for: ${parts.join(", ")}`;
2721
+ }
2709
2722
  /**
2710
2723
  * Removes a recipe from the shopping list, then automatically
2711
2724
  * recalculates the quantities and recategorize the ingredients.s
@@ -2894,7 +2907,7 @@ var ShoppingCart = class {
2894
2907
  const normalizedQuantityTotal = normalizeAllUnits(ingredient.quantityTotal);
2895
2908
  function getOptimumMatchForQuantityParts(normalizedQuantities, normalizedOptions2, selection = []) {
2896
2909
  if (isAndGroup(normalizedQuantities)) {
2897
- for (const q of normalizedQuantities.entries) {
2910
+ for (const q of normalizedQuantities.and) {
2898
2911
  const result = getOptimumMatchForQuantityParts(
2899
2912
  q,
2900
2913
  normalizedOptions2,
@@ -2903,7 +2916,7 @@ var ShoppingCart = class {
2903
2916
  selection.push(...result);
2904
2917
  }
2905
2918
  } else {
2906
- const alternativeUnitsOfQuantity = isOrGroup(normalizedQuantities) ? normalizedQuantities.entries : [normalizedQuantities];
2919
+ const alternativeUnitsOfQuantity = isOrGroup(normalizedQuantities) ? normalizedQuantities.or : [normalizedQuantities];
2907
2920
  const solutions = [];
2908
2921
  const errors = /* @__PURE__ */ new Set();
2909
2922
  for (const alternative of alternativeUnitsOfQuantity) {
@@ -3023,6 +3036,21 @@ var ShoppingCart = class {
3023
3036
  return this.summary;
3024
3037
  }
3025
3038
  };
3039
+
3040
+ // src/utils/render_helpers.ts
3041
+ function isAlternativeSelected(recipe, choices, item, alternativeIndex) {
3042
+ if (item.group) {
3043
+ const selectedIndex2 = choices?.ingredientGroups?.get(item.group);
3044
+ const groupAlternatives = recipe.choices.ingredientGroups.get(item.group);
3045
+ if (groupAlternatives && selectedIndex2 && selectedIndex2 < groupAlternatives.length) {
3046
+ const selectedItemId = groupAlternatives[selectedIndex2]?.itemId;
3047
+ return selectedItemId === item.id;
3048
+ }
3049
+ return false;
3050
+ }
3051
+ const selectedIndex = choices?.ingredientItems?.get(item.id);
3052
+ return alternativeIndex === selectedIndex;
3053
+ }
3026
3054
  // Annotate the CommonJS export names for ESM import in node:
3027
3055
  0 && (module.exports = {
3028
3056
  CategoryConfig,
@@ -3032,9 +3060,12 @@ var ShoppingCart = class {
3032
3060
  Recipe,
3033
3061
  Section,
3034
3062
  ShoppingCart,
3035
- ShoppingList
3063
+ ShoppingList,
3064
+ isAlternativeSelected
3036
3065
  });
3037
3066
  /* v8 ignore else -- @preserve */
3067
+ // v8 ignore else -- @preserve
3038
3068
  /* v8 ignore else -- expliciting error type -- @preserve */
3069
+ // v8 ignore if -- @preserve
3039
3070
  /* v8 ignore if -- @preserve */
3040
3071
  //# sourceMappingURL=index.cjs.map