@tmlmt/cooklang-parser 3.0.0-alpha.19 → 3.0.0-alpha.21

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
@@ -335,7 +335,7 @@ var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
335
335
  var nonWordCharStrict = "\\s@#~\\[\\]{(,;:!?|";
336
336
  var ingredientWithAlternativeRegex = d().literal("@").startNamedGroup("ingredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("ingredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("mIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("sIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("ingredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("ingredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("ingredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startNamedGroup("ingredientNote").notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().startNamedGroup("ingredientAlternative").startGroup().literal("|").startGroup().anyOf("@\\-&?").zeroOrMore().endGroup().optional().startGroup().literal("./").endGroup().optional().startGroup().startGroup().startGroup().notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startGroup().notAnyOf(nonWordChar).oneOrMore().endGroup().endGroup().startGroup().literal("{").startGroup().literal("=").exactly(1).endGroup().optional().startGroup().notAnyOf("}%").oneOrMore().endGroup().optional().startGroup().literal("%").startGroup().notAnyOf("}").oneOrMore().lazy().endGroup().endGroup().optional().literal("}").endGroup().optional().startGroup().literal("(").startGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().endGroup().zeroOrMore().endGroup().toRegExp();
337
337
  var inlineIngredientAlternativesRegex = new RegExp("\\|" + ingredientWithAlternativeRegex.source.slice(1));
338
- var quantityAlternativeRegex = d().startNamedGroup("quantity").notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("unit").notAnyOf("|}").oneOrMore().endGroup().endGroup().optional().startGroup().literal("|").startNamedGroup("alternative").startGroup().notAnyOf("}").oneOrMore().endGroup().zeroOrMore().endGroup().endGroup().optional().toRegExp();
338
+ var quantityAlternativeRegex = d().startNamedGroup("quantity").notAnyOf("{}|%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("unit").notAnyOf("|}").oneOrMore().endGroup().endGroup().optional().startGroup().literal("|").startNamedGroup("alternative").startGroup().notAnyOf("}").oneOrMore().endGroup().zeroOrMore().endGroup().endGroup().optional().toRegExp();
339
339
  var ingredientWithGroupKeyRegex = d().literal("@|").startNamedGroup("gIngredientGroupKey").notAnyOf(nonWordCharStrict + "/").oneOrMore().endGroup().startGroup().literal("/").startNamedGroup("gIngredientSubgroupKey").notAnyOf(nonWordCharStrict).oneOrMore().endGroup().endGroup().optional().literal("|").startNamedGroup("gIngredientModifiers").anyOf("@\\-&?").zeroOrMore().endGroup().optional().startNamedGroup("gIngredientRecipeAnchor").literal("./").endGroup().optional().startGroup().startGroup().startNamedGroup("gmIngredientName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\}|\\([^)]*\\))").endGroup().or().startNamedGroup("gsIngredientName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("gIngredientQuantityModifier").literal("=").exactly(1).endGroup().optional().startNamedGroup("gIngredientQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}").endGroup().optional().startGroup().literal("(").startNamedGroup("gIngredientPreparation").notAnyOf(")").oneOrMore().lazy().endGroup().literal(")").endGroup().optional().startGroup().literal("[").startNamedGroup("gIngredientNote").notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("]").endGroup().optional().toRegExp();
340
340
  var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
341
341
  var cookwareRegex = d().literal("#").startNamedGroup("cookwareModifiers").anyOf("\\-&?").zeroOrMore().endGroup().startGroup().startGroup().startNamedGroup("mCookwareName").notAnyOf(nonWordChar).oneOrMore().startGroup().whitespace().oneOrMore().notAnyOf(nonWordChar).oneOrMore().endGroup().oneOrMore().endGroup().positiveLookahead("\\s*(?:\\{[^\\}]*\\})").endGroup().or().startNamedGroup("sCookwareName").notAnyOf(nonWordChar).zeroOrMore().notAnyOf("\\." + nonWordChar).endGroup().endGroup().startGroup().literal("{").startNamedGroup("cookwareQuantity").anyCharacter().zeroOrMore().lazy().endGroup().literal("}").endGroup().optional().toRegExp();
@@ -351,11 +351,18 @@ var tokensRegex = new RegExp(
351
351
  ].map((r2) => r2.source).join("|"),
352
352
  "gu"
353
353
  );
354
- var servingsPrefixPart = (varName) => d().startAnchor().literal(varName).literal(":").anyOf(" ").zeroOrMore().startNamedGroup("servingsPrefix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().lazy().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().toRegExp();
355
- var servingsSuffixPart = d().anyOf(" ").zeroOrMore().startNamedGroup("servingsSuffix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().endAnchor().toRegExp();
356
- var scalingSimpleMetaValueRegex = (varName) => d().startAnchor().literal(varName).literal(":").anyOf("\\t ").zeroOrMore().startCaptureGroup().startCaptureGroup().notAnyOf(",\\n").oneOrMore().endGroup().startGroup().literal(",").anyOf("\\t ").zeroOrMore().startCaptureGroup().anyCharacter().oneOrMore().endGroup().optional().endGroup().optional().endGroup().endAnchor().multiline().toRegExp();
357
- var scalingMetaValueWithUnitRegex = (varName) => new RegExp(
358
- servingsPrefixPart(varName).source + arbitraryScalableRegex.source + servingsSuffixPart.source,
354
+ var yieldPrefixPart = d().startAnchor().literal("yield").literal(":").anyOf(" ").zeroOrMore().startNamedGroup("servingsPrefix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().lazy().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().toRegExp();
355
+ var yieldSuffixPart = d().anyOf(" ").zeroOrMore().startNamedGroup("servingsSuffix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().endAnchor().toRegExp();
356
+ var yieldMetaValueWithUnitRegex = new RegExp(
357
+ yieldPrefixPart.source + arbitraryScalableRegex.source + yieldSuffixPart.source,
358
+ "m"
359
+ );
360
+ var yieldMetaValueAsQuantityRegex = d().startAnchor().literal("yield:").anyOf(" ").zeroOrMore().startNamedGroup("quantity").notAnyOf("{}|%\\n\\r").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("unit").notAnyOf("\\n\\r|}").oneOrMore().endGroup().endGroup().optional().anyOf(" ").zeroOrMore().endAnchor().toRegExp();
361
+ var yieldMetaValueRegex = new RegExp(
362
+ [
363
+ yieldMetaValueWithUnitRegex.source,
364
+ yieldMetaValueAsQuantityRegex.source
365
+ ].join("|"),
359
366
  "m"
360
367
  );
361
368
  var commentRegex = d().literal("--").anyCharacter().zeroOrMore().global().toRegExp();
@@ -364,7 +371,7 @@ var shoppingListRegex = d().literal("[").startNamedGroup("name").anyCharacter().
364
371
  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();
365
372
  var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
366
373
  var floatRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
367
- var variantTagRegex = d().startAnchor().literal("[").startNamedGroup("variantOptionalPrefix").literal("?").endGroup().optional().startNamedGroup("variantNames").notAnyOf("\\]").zeroOrMore().endGroup().literal("]").whitespace().zeroOrMore().toRegExp();
374
+ var variantTagRegex = d().startAnchor().literal("[").startNamedGroup("variantOptionalPrefix").literal("?").endGroup().optional().startNamedGroup("variantNames").notAnyOf("\\]").oneOrMore().endGroup().optional().literal("]").whitespace().zeroOrMore().toRegExp();
368
375
  var mdEscaped = d().literal("\\").startCaptureGroup().anyOf("*_`").endGroup();
369
376
  var mdInlineCode = d().literal("`").startCaptureGroup().notAnyOf("`").oneOrMore().lazy().endGroup().literal("`");
370
377
  var mdLink = d().literal("[").startCaptureGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("](").startCaptureGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")");
@@ -940,7 +947,7 @@ var NoProductMatchError = class extends Error {
940
947
  constructor(item_name, code) {
941
948
  const messageMap = {
942
949
  incompatibleUnits: `The units of the products in the catalogue are incompatible with ingredient ${item_name} in the shopping list.`,
943
- noProduct: "No product was found linked to ingredient name ${item_name} in the shopping list",
950
+ noProduct: `No product was found linked to ingredient name ${item_name} in the shopping list`,
944
951
  textValue: `Ingredient ${item_name} has a text value as quantity and can therefore not be matched with any product in the catalogue.`,
945
952
  noQuantity: `Ingredient ${item_name} has no quantity and can therefore not be matched with any product in the catalogue.`,
946
953
  textValue_incompatibleUnits: `Multiple alternative quantities were provided for ingredient ${item_name} in the shopping list but they were either text values or no product in catalog were found to have compatible units`
@@ -1543,7 +1550,7 @@ function stringifyFixedValue(quantity) {
1543
1550
  return String(quantity.value.decimal);
1544
1551
  else return quantity.value.text;
1545
1552
  }
1546
- function parseQuantityInput(input_str) {
1553
+ function parseQuantityValue(input_str) {
1547
1554
  const clean_str = String(input_str).trim();
1548
1555
  if (rangeRegex.test(clean_str)) {
1549
1556
  const range_parts = clean_str.split("-");
@@ -1557,12 +1564,12 @@ function parseQuantityWithUnit(input) {
1557
1564
  const trimmed = input.trim();
1558
1565
  const separatorIndex = trimmed.indexOf("%");
1559
1566
  if (separatorIndex === -1) {
1560
- return { value: parseQuantityInput(trimmed) };
1567
+ return { value: parseQuantityValue(trimmed) };
1561
1568
  }
1562
1569
  const valuePart = trimmed.slice(0, separatorIndex).trim();
1563
1570
  const unitPart = trimmed.slice(separatorIndex + 1).trim();
1564
1571
  return {
1565
- value: parseQuantityInput(valuePart),
1572
+ value: parseQuantityValue(valuePart),
1566
1573
  unit: unitPart || void 0
1567
1574
  };
1568
1575
  }
@@ -1752,7 +1759,7 @@ function parseArbitraryQuantity(raw) {
1752
1759
  "Arbitrary quantities must have a numerical value"
1753
1760
  );
1754
1761
  }
1755
- const value = parseQuantityInput(quantityMatch.groups.quantity);
1762
+ const value = parseQuantityValue(quantityMatch.groups.quantity);
1756
1763
  const unit = quantityMatch.groups.unit;
1757
1764
  if (!value || value.type === "fixed" && value.value.type === "text") {
1758
1765
  throw new InvalidQuantityFormat(
@@ -1766,40 +1773,40 @@ function parseArbitraryQuantity(raw) {
1766
1773
  if (unit) arbitrary.unit = unit;
1767
1774
  return arbitrary;
1768
1775
  }
1769
- function parseScalingMetaVar(content, varName) {
1770
- const complexMatch = content.match(scalingMetaValueWithUnitRegex(varName));
1771
- if (complexMatch?.groups?.arbitraryQuantity) {
1772
- const parsed = parseArbitraryQuantity(
1773
- complexMatch.groups.arbitraryQuantity
1774
- );
1775
- const result2 = {
1776
+ function parseServingsMetaVar(content, varName) {
1777
+ const raw = parseSimpleMetaVar(content, varName);
1778
+ if (raw === void 0) return void 0;
1779
+ const num = Number(raw);
1780
+ if (isNaN(num)) {
1781
+ return { numericValue: 1, rawValue: raw };
1782
+ }
1783
+ return { numericValue: num, rawValue: num };
1784
+ }
1785
+ function parseYieldMetaVar(content) {
1786
+ const match = content.match(yieldMetaValueRegex);
1787
+ if (!match) return void 0;
1788
+ if (match.groups?.arbitraryQuantity) {
1789
+ const parsed = parseArbitraryQuantity(match.groups.arbitraryQuantity);
1790
+ const result = {
1776
1791
  quantity: parsed.quantity
1777
1792
  };
1778
- if (parsed.unit) result2.unit = parsed.unit;
1779
- if (complexMatch.groups.servingsPrefix) {
1780
- result2.textBefore = complexMatch.groups.servingsPrefix;
1793
+ if (parsed.unit) result.unit = parsed.unit;
1794
+ if (match.groups.servingsPrefix) {
1795
+ result.textBefore = match.groups.servingsPrefix;
1781
1796
  }
1782
- if (complexMatch.groups.servingsSuffix) {
1783
- result2.textAfter = complexMatch.groups.servingsSuffix;
1797
+ if (match.groups.servingsSuffix) {
1798
+ result.textAfter = match.groups.servingsSuffix;
1784
1799
  }
1785
- return result2;
1786
- }
1787
- const varMatch = content.match(scalingSimpleMetaValueRegex(varName));
1788
- if (!varMatch) return void 0;
1789
- if (isNaN(Number(varMatch[2]?.trim()))) {
1790
- throw new Error("Scaling variables should be numbers");
1800
+ return result;
1791
1801
  }
1792
- const numericValue = Number(varMatch[2]?.trim());
1793
- const result = {
1794
- quantity: {
1795
- type: "fixed",
1796
- value: { type: "decimal", decimal: numericValue }
1797
- }
1798
- };
1799
- if (varMatch[3]) {
1800
- result.text = `${varMatch[3].trim()}`;
1802
+ if (match.groups?.quantity) {
1803
+ const result = {
1804
+ quantity: parseQuantityValue(match.groups.quantity)
1805
+ };
1806
+ if (match.groups.unit) result.unit = match.groups.unit;
1807
+ return result;
1801
1808
  }
1802
- return result;
1809
+ return void 0;
1803
1810
  }
1804
1811
  function parseListMetaVar(content, varName) {
1805
1812
  const listMatch = content.match(
@@ -1917,12 +1924,12 @@ function parseAnyMetaVar(content, varName) {
1917
1924
  if (simple) return parseMetadataValue(simple);
1918
1925
  return void 0;
1919
1926
  }
1920
- function getNumericValueFromMetaVar(v) {
1927
+ function getNumericValueFromYield(v) {
1921
1928
  if (v.quantity.type === "fixed" && v.quantity.value.type !== "text") {
1922
1929
  return getNumericValue(v.quantity.value);
1923
1930
  }
1924
1931
  if (v.quantity.type === "range") return getNumericValue(v.quantity.min);
1925
- return 0;
1932
+ return 1;
1926
1933
  }
1927
1934
  function extractMetadata(content) {
1928
1935
  const metadata = {};
@@ -2047,11 +2054,16 @@ function extractMetadata(content) {
2047
2054
  };
2048
2055
  unitSystem = unitSystemMap[unitSystemRaw.toLowerCase()];
2049
2056
  }
2050
- for (const metaVar of ["servings", "yield", "serves"]) {
2051
- const scalingMetaValue = parseScalingMetaVar(metadataContent, metaVar);
2052
- if (scalingMetaValue) {
2053
- metadata[metaVar] = scalingMetaValue;
2054
- servings = getNumericValueFromMetaVar(scalingMetaValue);
2057
+ const yieldValue = parseYieldMetaVar(metadataContent);
2058
+ if (yieldValue) {
2059
+ metadata.yield = yieldValue;
2060
+ servings = getNumericValueFromYield(yieldValue);
2061
+ }
2062
+ for (const metaVar of ["serves", "servings"]) {
2063
+ const result = parseServingsMetaVar(metadataContent, metaVar);
2064
+ if (result !== void 0) {
2065
+ metadata[metaVar] = result.rawValue;
2066
+ servings = result.numericValue;
2055
2067
  }
2056
2068
  }
2057
2069
  const tags = parseListMetaVar(metadataContent, "tags");
@@ -2332,7 +2344,7 @@ var ProductCatalog = class {
2332
2344
  const sizeStrings = Array.isArray(size) ? size : [size];
2333
2345
  const sizes = sizeStrings.map((sizeStr) => {
2334
2346
  const sizeAndUnitRaw = sizeStr.split("%");
2335
- const sizeParsed = parseQuantityInput(
2347
+ const sizeParsed = parseQuantityValue(
2336
2348
  sizeAndUnitRaw[0]
2337
2349
  );
2338
2350
  const productSize = { size: sizeParsed };
@@ -2475,7 +2487,7 @@ var Section = class {
2475
2487
  };
2476
2488
 
2477
2489
  // src/quantities/alternatives.ts
2478
- var import_big3 = __toESM(require("big.js"), 1);
2490
+ var import_big4 = __toESM(require("big.js"), 1);
2479
2491
 
2480
2492
  // src/units/lookup.ts
2481
2493
  function findListWithCompatibleQuantity(list, quantity) {
@@ -2498,10 +2510,14 @@ function findCompatibleQuantityWithinList(list, quantity) {
2498
2510
  }
2499
2511
 
2500
2512
  // src/utils/general.ts
2513
+ var import_big3 = __toESM(require("big.js"), 1);
2501
2514
  var legacyDeepClone = (v) => {
2502
2515
  if (v === null || typeof v !== "object") {
2503
2516
  return v;
2504
2517
  }
2518
+ if (v instanceof import_big3.default) {
2519
+ return new import_big3.default(v);
2520
+ }
2505
2521
  if (v instanceof Map) {
2506
2522
  return new Map(
2507
2523
  Array.from(v.entries()).map(([k, val]) => [
@@ -2511,7 +2527,9 @@ var legacyDeepClone = (v) => {
2511
2527
  );
2512
2528
  }
2513
2529
  if (v instanceof Set) {
2514
- return new Set(Array.from(v).map((val) => legacyDeepClone(val)));
2530
+ return new Set(
2531
+ Array.from(v).map((val) => legacyDeepClone(val))
2532
+ );
2515
2533
  }
2516
2534
  if (v instanceof Date) {
2517
2535
  return new Date(v.getTime());
@@ -2525,7 +2543,7 @@ var legacyDeepClone = (v) => {
2525
2543
  }
2526
2544
  return cloned;
2527
2545
  };
2528
- var deepClone = (v) => typeof structuredClone === "function" ? structuredClone(v) : legacyDeepClone(v);
2546
+ var deepClone = (v) => legacyDeepClone(v);
2529
2547
 
2530
2548
  // src/quantities/alternatives.ts
2531
2549
  function getEquivalentUnitsLists(...quantities) {
@@ -2751,7 +2769,7 @@ function regroupQuantitiesAndExpandEquivalents(sum, unitsLists, system) {
2751
2769
  return main.reduce((acc, v) => {
2752
2770
  const mainInList = findCompatibleQuantityWithinList(list, v);
2753
2771
  const conversionRatio = getBaseUnitRatio(v, mainInList);
2754
- const valueInOriginalUnit = (0, import_big3.default)(getAverageValue(v.quantity)).times(
2772
+ const valueInOriginalUnit = (0, import_big4.default)(getAverageValue(v.quantity)).times(
2755
2773
  conversionRatio
2756
2774
  );
2757
2775
  const newValue = {
@@ -2763,7 +2781,7 @@ function regroupQuantitiesAndExpandEquivalents(sum, unitsLists, system) {
2763
2781
  decimal: valueInOriginalUnit.toNumber()
2764
2782
  }
2765
2783
  },
2766
- (0, import_big3.default)(getAverageValue(equiv.quantity)).div(
2784
+ (0, import_big4.default)(getAverageValue(equiv.quantity)).div(
2767
2785
  getAverageValue(mainInList.quantity)
2768
2786
  )
2769
2787
  )
@@ -2804,9 +2822,50 @@ function addEquivalentsAndSimplify(quantities, system) {
2804
2822
  return { and: regrouped.map(toPlainUnit) };
2805
2823
  }
2806
2824
  }
2825
+ function buildEquivalenceRatioMap(unitsLists) {
2826
+ const ratioMap = {};
2827
+ for (const list of unitsLists) {
2828
+ for (const equiv of list) {
2829
+ const equivValue = getAverageValue(equiv.quantity);
2830
+ for (const primary of list) {
2831
+ if (primary === equiv) continue;
2832
+ const primaryValue = getAverageValue(primary.quantity);
2833
+ const equivUnit = normalizeUnit(equiv.unit.name)?.name ?? equiv.unit.name;
2834
+ const primaryUnit = normalizeUnit(primary.unit.name)?.name ?? primary.unit.name;
2835
+ ratioMap[equivUnit] ?? (ratioMap[equivUnit] = {});
2836
+ ratioMap[equivUnit][primaryUnit] = equivValue / primaryValue;
2837
+ }
2838
+ }
2839
+ }
2840
+ return ratioMap;
2841
+ }
2842
+ function recomputeEquivalents(primaries, ratioMap, equivUnits) {
2843
+ const equivalents = [];
2844
+ for (const equivUnit of equivUnits) {
2845
+ const ratios = ratioMap[normalizeUnit(equivUnit)?.name ?? equivUnit];
2846
+ let total = 0;
2847
+ for (const primary of primaries) {
2848
+ const pUnit = normalizeUnit(primary.unit ?? NO_UNIT)?.name ?? primary.unit ?? NO_UNIT;
2849
+ const ratio = ratios[pUnit];
2850
+ if (ratio === void 0) continue;
2851
+ const pValue = getAverageValue(primary.quantity);
2852
+ total += pValue * ratio;
2853
+ }
2854
+ if (total > 0) {
2855
+ equivalents.push({
2856
+ quantity: {
2857
+ type: "fixed",
2858
+ value: { type: "decimal", decimal: total }
2859
+ },
2860
+ ...equivUnit !== "" && { unit: equivUnit }
2861
+ });
2862
+ }
2863
+ }
2864
+ return equivalents.length > 0 ? equivalents : void 0;
2865
+ }
2807
2866
 
2808
2867
  // src/classes/recipe.ts
2809
- var import_big4 = __toESM(require("big.js"), 1);
2868
+ var import_big5 = __toESM(require("big.js"), 1);
2810
2869
  var _Recipe = class _Recipe {
2811
2870
  /**
2812
2871
  * Creates a new Recipe instance.
@@ -2928,7 +2987,7 @@ var _Recipe = class _Recipe {
2928
2987
  let quantityMatch = quantityRaw.match(quantityAlternativeRegex);
2929
2988
  const quantities = [];
2930
2989
  while (quantityMatch?.groups) {
2931
- const value = quantityMatch.groups.quantity ? parseQuantityInput(quantityMatch.groups.quantity) : void 0;
2990
+ const value = quantityMatch.groups.quantity ? parseQuantityValue(quantityMatch.groups.quantity) : void 0;
2932
2991
  const unit = quantityMatch.groups.unit;
2933
2992
  if (value) {
2934
2993
  const newQuantity = { quantity: value };
@@ -3296,7 +3355,9 @@ var _Recipe = class _Recipe {
3296
3355
  } else {
3297
3356
  const targetSubgroupIndex = 0;
3298
3357
  const selectedSubgroup = groupSubgroups?.[targetSubgroupIndex];
3299
- isSelected = selectedSubgroup?.some((alt) => alt.itemId === item.id) ?? false;
3358
+ isSelected = selectedSubgroup.some(
3359
+ (alt) => alt.itemId === item.id
3360
+ );
3300
3361
  }
3301
3362
  } else {
3302
3363
  const targetSubgroupIndex = groupChoice ?? 0;
@@ -3695,16 +3756,10 @@ var _Recipe = class _Recipe {
3695
3756
  }
3696
3757
  sectionName = sectionName.slice(sectionVarMatch[0].length).trim();
3697
3758
  }
3698
- if (this.sections.length === 0 && section.isBlank()) {
3699
- section.name = sectionName;
3700
- if (sectionVariants) section.variants = sectionVariants;
3701
- if (sectionOptional) section.optional = true;
3702
- } else {
3703
- if (!section.isBlank()) {
3704
- this.sections.push(section);
3705
- }
3706
- section = new Section(sectionName, sectionVariants, sectionOptional);
3759
+ if (!section.isBlank()) {
3760
+ this.sections.push(section);
3707
3761
  }
3762
+ section = new Section(sectionName, sectionVariants, sectionOptional);
3708
3763
  blankLineBefore = true;
3709
3764
  inNote = false;
3710
3765
  continue;
@@ -3770,7 +3825,7 @@ var _Recipe = class _Recipe {
3770
3825
  if (modifiers !== void 0 && modifiers.includes("-")) {
3771
3826
  flags.push("hidden");
3772
3827
  }
3773
- const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
3828
+ const quantity = quantityRaw ? parseQuantityValue(quantityRaw) : void 0;
3774
3829
  const newCookware = {
3775
3830
  name
3776
3831
  };
@@ -3802,7 +3857,7 @@ var _Recipe = class _Recipe {
3802
3857
  throw new Error("Timer missing unit");
3803
3858
  }
3804
3859
  const name = groups.timerName || void 0;
3805
- const duration = parseQuantityInput(durationStr);
3860
+ const duration = parseQuantityValue(durationStr);
3806
3861
  const timerObj = {
3807
3862
  name,
3808
3863
  duration,
@@ -3842,7 +3897,7 @@ var _Recipe = class _Recipe {
3842
3897
  if (originalServings === void 0 || originalServings === 0) {
3843
3898
  originalServings = 1;
3844
3899
  }
3845
- const factor = (0, import_big4.default)(newServings).div(originalServings);
3900
+ const factor = (0, import_big5.default)(newServings).div(originalServings);
3846
3901
  return this.scaleBy(factor);
3847
3902
  }
3848
3903
  /**
@@ -3861,7 +3916,7 @@ var _Recipe = class _Recipe {
3861
3916
  function scaleAlternativesBy(alternatives, factor2) {
3862
3917
  for (const alternative of alternatives) {
3863
3918
  if (alternative.quantity) {
3864
- const scaleFactor = alternative.scalable ? (0, import_big4.default)(factor2) : 1;
3919
+ const scaleFactor = alternative.scalable ? (0, import_big5.default)(factor2) : 1;
3865
3920
  if (alternative.quantity.type !== "fixed" || alternative.quantity.value.type !== "text") {
3866
3921
  alternative.quantity = multiplyQuantityValue(
3867
3922
  alternative.quantity,
@@ -3934,10 +3989,16 @@ var _Recipe = class _Recipe {
3934
3989
  arbitrary.unit = optimized.unit;
3935
3990
  }
3936
3991
  newRecipe._populateIngredientQuantities();
3937
- newRecipe.servings = (0, import_big4.default)(originalServings).times(factor).toNumber();
3938
- for (const metaVar of ["servings", "yield", "serves"]) {
3939
- if (newRecipe.metadata[metaVar] && this.metadata[metaVar]) {
3940
- const original = this.metadata[metaVar];
3992
+ newRecipe.servings = (0, import_big5.default)(originalServings).times(factor).toNumber();
3993
+ for (const metaVar of ["servings", "serves"]) {
3994
+ if (typeof newRecipe.metadata[metaVar] === "number") {
3995
+ newRecipe.metadata[metaVar] = (0, import_big5.default)(newRecipe.metadata[metaVar]).times(factor).toNumber();
3996
+ }
3997
+ }
3998
+ if (newRecipe.metadata.yield && this.metadata.yield) {
3999
+ const original = this.metadata.yield;
4000
+ if (original.quantity.type === "fixed" && original.quantity.value.type === "text") {
4001
+ } else {
3941
4002
  const scaledQuantity = multiplyQuantityValue(
3942
4003
  original.quantity,
3943
4004
  factor
@@ -3952,8 +4013,7 @@ var _Recipe = class _Recipe {
3952
4013
  if (optimized.unit) scaled.unit = optimized.unit;
3953
4014
  if (original.textBefore) scaled.textBefore = original.textBefore;
3954
4015
  if (original.textAfter) scaled.textAfter = original.textAfter;
3955
- if (original.text) scaled.text = original.text;
3956
- newRecipe.metadata[metaVar] = scaled;
4016
+ newRecipe.metadata.yield = scaled;
3957
4017
  }
3958
4018
  }
3959
4019
  return newRecipe;
@@ -4170,7 +4230,7 @@ __publicField(_Recipe, "subgroupIndices", /* @__PURE__ */ new WeakMap());
4170
4230
  var Recipe = _Recipe;
4171
4231
 
4172
4232
  // src/classes/shopping_list.ts
4173
- var ShoppingList = class _ShoppingList {
4233
+ var ShoppingList = class {
4174
4234
  /**
4175
4235
  * Creates a new ShoppingList instance
4176
4236
  * @param categoryConfigStr - The category configuration to parse.
@@ -4265,7 +4325,7 @@ var ShoppingList = class _ShoppingList {
4265
4325
  }
4266
4326
  }
4267
4327
  if (numericEntries.length > 1) {
4268
- const ratioMap = _ShoppingList.buildEquivalenceRatioMap(
4328
+ const ratioMap = buildEquivalenceRatioMap(
4269
4329
  getEquivalentUnitsLists(...numericEntries)
4270
4330
  );
4271
4331
  if (Object.keys(ratioMap).length > 0) {
@@ -4322,10 +4382,12 @@ var ShoppingList = class _ShoppingList {
4322
4382
  const pantryHasUnit = pantryExtended.unit !== void 0 && pantryExtended.unit.name !== "";
4323
4383
  const ratioMap = this.equivalenceRatios.get(ingredient.name);
4324
4384
  const unitMismatch = leafHasUnit !== pantryHasUnit && ratioMap !== void 0;
4385
+ const leafDef = normalizeUnit(leaf.unit);
4386
+ const pantryDef = normalizeUnit(pantryExtended.unit?.name);
4325
4387
  if (unitMismatch) {
4326
4388
  const leafUnit = leaf.unit ?? NO_UNIT;
4327
4389
  const pantryUnit = pantryExtended.unit?.name ?? NO_UNIT;
4328
- const ratioFromPantry = ratioMap[leafUnit]?.[pantryUnit];
4390
+ const ratioFromPantry = ratioMap[normalizeUnit(leafUnit)?.name ?? leafUnit]?.[normalizeUnit(pantryUnit)?.name ?? pantryUnit];
4329
4391
  if (ratioFromPantry !== void 0) {
4330
4392
  const pantryValue = getAverageValue(pantryExtended.quantity);
4331
4393
  const leafValue = getAverageValue(ingredientExtended.quantity);
@@ -4340,7 +4402,7 @@ var ShoppingList = class _ShoppingList {
4340
4402
  type: "fixed",
4341
4403
  value: { type: "decimal", decimal: remainingLeafValue }
4342
4404
  };
4343
- const consumedInPantryUnits = ratioFromPantry !== 0 ? subtracted / ratioFromPantry : pantryValue;
4405
+ const consumedInPantryUnits = subtracted / ratioFromPantry;
4344
4406
  const remainingPantryValue = Math.max(
4345
4407
  pantryValue - consumedInPantryUnits,
4346
4408
  0
@@ -4357,9 +4419,10 @@ var ShoppingList = class _ShoppingList {
4357
4419
  };
4358
4420
  continue;
4359
4421
  }
4422
+ } else {
4423
+ continue;
4360
4424
  }
4361
- }
4362
- try {
4425
+ } else if (leafDef && pantryDef && areUnitsConvertible(leafDef, pantryDef) || (leaf.unit ?? "").toLowerCase() === (pantryExtended.unit?.name ?? "").toLowerCase()) {
4363
4426
  const remaining = subtractQuantities(
4364
4427
  ingredientExtended,
4365
4428
  pantryExtended,
@@ -4374,7 +4437,47 @@ var ShoppingList = class _ShoppingList {
4374
4437
  const updated = toPlainUnit(remaining);
4375
4438
  leaf.quantity = updated.quantity;
4376
4439
  leaf.unit = updated.unit;
4377
- } catch {
4440
+ } else if (ratioMap) {
4441
+ const canonicalLeaf = normalizeUnit(leaf.unit)?.name ?? leaf.unit;
4442
+ const leafValue = getAverageValue(ingredientExtended.quantity);
4443
+ const pantryValue = getAverageValue(pantryExtended.quantity);
4444
+ if (typeof leafValue === "number" && typeof pantryValue === "number" && pantryDef) {
4445
+ for (const [equivUnit, ratios] of Object.entries(ratioMap)) {
4446
+ const ratio = ratios[canonicalLeaf];
4447
+ if (ratio === void 0) continue;
4448
+ const equivDef = normalizeUnit(equivUnit);
4449
+ if (!equivDef || !areUnitsConvertible(equivDef, pantryDef))
4450
+ continue;
4451
+ const pantryInEquiv = pantryValue * getToBase(pantryDef) / getToBase(equivDef);
4452
+ const pantryInLeafUnits = pantryInEquiv / ratio;
4453
+ const subtracted = Math.min(pantryInLeafUnits, leafValue);
4454
+ const remainingLeafValue = Math.max(
4455
+ leafValue - pantryInLeafUnits,
4456
+ 0
4457
+ );
4458
+ leaf.quantity = {
4459
+ type: "fixed",
4460
+ value: { type: "decimal", decimal: remainingLeafValue }
4461
+ };
4462
+ const consumedInEquiv = subtracted * ratio;
4463
+ const consumedInPantryUnits = consumedInEquiv * getToBase(equivDef) / getToBase(pantryDef);
4464
+ const remainingPantryValue = Math.max(
4465
+ pantryValue - consumedInPantryUnits,
4466
+ 0
4467
+ );
4468
+ pantryExtended = {
4469
+ quantity: {
4470
+ type: "fixed",
4471
+ value: {
4472
+ type: "decimal",
4473
+ decimal: remainingPantryValue
4474
+ }
4475
+ },
4476
+ ...pantryExtended.unit && { unit: pantryExtended.unit }
4477
+ };
4478
+ break;
4479
+ }
4480
+ }
4378
4481
  }
4379
4482
  }
4380
4483
  if ("and" in entry) {
@@ -4385,8 +4488,8 @@ var ShoppingList = class _ShoppingList {
4385
4488
  entry.and.push(...nonZero);
4386
4489
  const ratioMap = this.equivalenceRatios.get(ingredient.name);
4387
4490
  if (entry.equivalents && ratioMap) {
4388
- const equivUnits = entry.equivalents.map((e2) => e2.unit ?? NO_UNIT);
4389
- entry.equivalents = _ShoppingList.recomputeEquivalents(
4491
+ const equivUnits = entry.equivalents.map((e2) => e2.unit);
4492
+ entry.equivalents = recomputeEquivalents(
4390
4493
  entry.and,
4391
4494
  ratioMap,
4392
4495
  equivUnits
@@ -4397,17 +4500,17 @@ var ShoppingList = class _ShoppingList {
4397
4500
  ingredient.quantities[i2] = {
4398
4501
  quantity: single.quantity,
4399
4502
  ...single.unit && { unit: single.unit },
4400
- ...entry.equivalents && { equivalents: entry.equivalents },
4401
- ...entry.alternatives && { alternatives: entry.alternatives }
4503
+ ...entry.equivalents && { equivalents: entry.equivalents }
4402
4504
  };
4403
4505
  }
4404
4506
  } else if ("equivalents" in entry && entry.equivalents) {
4405
4507
  const ratioMap = this.equivalenceRatios.get(ingredient.name);
4406
4508
  if (ratioMap) {
4407
4509
  const equivUnits = entry.equivalents.map(
4408
- (e2) => e2.unit ?? NO_UNIT
4510
+ (e2) => e2.unit
4511
+ // equivalents always have units
4409
4512
  );
4410
- const recomputed = _ShoppingList.recomputeEquivalents(
4513
+ const recomputed = recomputeEquivalents(
4411
4514
  [entry],
4412
4515
  ratioMap,
4413
4516
  equivUnits
@@ -4430,57 +4533,6 @@ var ShoppingList = class _ShoppingList {
4430
4533
  }
4431
4534
  this.resultingPantry = clonedPantry;
4432
4535
  }
4433
- /**
4434
- * Builds a ratio map from equivalence lists.
4435
- * For each equivalence list, stores ratio = equiv_value / primary_value
4436
- * for every pair of units, so equivalents can be recomputed after
4437
- * pantry subtraction modifies primary quantities.
4438
- */
4439
- static buildEquivalenceRatioMap(unitsLists) {
4440
- const ratioMap = {};
4441
- for (const list of unitsLists) {
4442
- for (const equiv of list) {
4443
- const equivValue = getAverageValue(equiv.quantity);
4444
- for (const primary of list) {
4445
- if (primary === equiv) continue;
4446
- const primaryValue = getAverageValue(primary.quantity);
4447
- const equivUnit = equiv.unit.name;
4448
- const primaryUnit = primary.unit.name;
4449
- ratioMap[equivUnit] ?? (ratioMap[equivUnit] = {});
4450
- ratioMap[equivUnit][primaryUnit] = equivValue / primaryValue;
4451
- }
4452
- }
4453
- }
4454
- return ratioMap;
4455
- }
4456
- /**
4457
- * Recomputes equivalent quantities from current primary values and stored ratios.
4458
- * For each equivalent unit in equivUnits, new_value = Σ (primary_value × ratio[equivUnit][primaryUnit]).
4459
- * Returns undefined if all equivalents compute to zero.
4460
- */
4461
- static recomputeEquivalents(primaries, ratioMap, equivUnits) {
4462
- const equivalents = [];
4463
- for (const equivUnit of equivUnits) {
4464
- const ratios = ratioMap[equivUnit];
4465
- let total = 0;
4466
- for (const primary of primaries) {
4467
- const pUnit = primary.unit ?? NO_UNIT;
4468
- const ratio = ratios[pUnit];
4469
- const pValue = getAverageValue(primary.quantity);
4470
- total += pValue * ratio;
4471
- }
4472
- if (total > 0) {
4473
- equivalents.push({
4474
- quantity: {
4475
- type: "fixed",
4476
- value: { type: "decimal", decimal: total }
4477
- },
4478
- ...equivUnit !== "" && { unit: equivUnit }
4479
- });
4480
- }
4481
- }
4482
- return equivalents.length > 0 ? equivalents : void 0;
4483
- }
4484
4536
  /**
4485
4537
  * Adds a recipe to the shopping list, then automatically
4486
4538
  * recalculates the quantities and recategorize the ingredients.
@@ -5095,6 +5147,9 @@ function getEffectiveChoices(recipe, variant) {
5095
5147
  // v8 ignore if -- @preserve: defensive type guard
5096
5148
  /* v8 ignore if -- @preserve */
5097
5149
  // v8 ignore next -- @preserve
5150
+ // v8 ignore else -- @preserve: text quantities never reach the equivalence path
5098
5151
  // v8 ignore else --@preserve: defensive type guard
5099
5152
  // v8 ignore else -- @preserve: detection if
5153
+ /* v8 ignore else -- @preserve: only act when there are matches */
5154
+ /* v8 ignore else -- @preserve: initialization pattern */
5100
5155
  //# sourceMappingURL=index.cjs.map