@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.js CHANGED
@@ -276,7 +276,7 @@ var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
276
276
  var nonWordCharStrict = "\\s@#~\\[\\]{(,;:!?|";
277
277
  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();
278
278
  var inlineIngredientAlternativesRegex = new RegExp("\\|" + ingredientWithAlternativeRegex.source.slice(1));
279
- 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();
279
+ 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();
280
280
  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();
281
281
  var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
282
282
  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();
@@ -292,11 +292,18 @@ var tokensRegex = new RegExp(
292
292
  ].map((r2) => r2.source).join("|"),
293
293
  "gu"
294
294
  );
295
- 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();
296
- var servingsSuffixPart = d().anyOf(" ").zeroOrMore().startNamedGroup("servingsSuffix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().endAnchor().toRegExp();
297
- 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();
298
- var scalingMetaValueWithUnitRegex = (varName) => new RegExp(
299
- servingsPrefixPart(varName).source + arbitraryScalableRegex.source + servingsSuffixPart.source,
295
+ var yieldPrefixPart = d().startAnchor().literal("yield").literal(":").anyOf(" ").zeroOrMore().startNamedGroup("servingsPrefix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().lazy().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().toRegExp();
296
+ var yieldSuffixPart = d().anyOf(" ").zeroOrMore().startNamedGroup("servingsSuffix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().endAnchor().toRegExp();
297
+ var yieldMetaValueWithUnitRegex = new RegExp(
298
+ yieldPrefixPart.source + arbitraryScalableRegex.source + yieldSuffixPart.source,
299
+ "m"
300
+ );
301
+ 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();
302
+ var yieldMetaValueRegex = new RegExp(
303
+ [
304
+ yieldMetaValueWithUnitRegex.source,
305
+ yieldMetaValueAsQuantityRegex.source
306
+ ].join("|"),
300
307
  "m"
301
308
  );
302
309
  var commentRegex = d().literal("--").anyCharacter().zeroOrMore().global().toRegExp();
@@ -305,7 +312,7 @@ var shoppingListRegex = d().literal("[").startNamedGroup("name").anyCharacter().
305
312
  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();
306
313
  var numberLikeRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".,/").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
307
314
  var floatRegex = d().startAnchor().digit().oneOrMore().startGroup().anyOf(".").exactly(1).digit().oneOrMore().endGroup().optional().endAnchor().toRegExp();
308
- var variantTagRegex = d().startAnchor().literal("[").startNamedGroup("variantOptionalPrefix").literal("?").endGroup().optional().startNamedGroup("variantNames").notAnyOf("\\]").zeroOrMore().endGroup().literal("]").whitespace().zeroOrMore().toRegExp();
315
+ var variantTagRegex = d().startAnchor().literal("[").startNamedGroup("variantOptionalPrefix").literal("?").endGroup().optional().startNamedGroup("variantNames").notAnyOf("\\]").oneOrMore().endGroup().optional().literal("]").whitespace().zeroOrMore().toRegExp();
309
316
  var mdEscaped = d().literal("\\").startCaptureGroup().anyOf("*_`").endGroup();
310
317
  var mdInlineCode = d().literal("`").startCaptureGroup().notAnyOf("`").oneOrMore().lazy().endGroup().literal("`");
311
318
  var mdLink = d().literal("[").startCaptureGroup().notAnyOf("\\]").oneOrMore().lazy().endGroup().literal("](").startCaptureGroup().notAnyOf(")").oneOrMore().lazy().endGroup().literal(")");
@@ -881,7 +888,7 @@ var NoProductMatchError = class extends Error {
881
888
  constructor(item_name, code) {
882
889
  const messageMap = {
883
890
  incompatibleUnits: `The units of the products in the catalogue are incompatible with ingredient ${item_name} in the shopping list.`,
884
- noProduct: "No product was found linked to ingredient name ${item_name} in the shopping list",
891
+ noProduct: `No product was found linked to ingredient name ${item_name} in the shopping list`,
885
892
  textValue: `Ingredient ${item_name} has a text value as quantity and can therefore not be matched with any product in the catalogue.`,
886
893
  noQuantity: `Ingredient ${item_name} has no quantity and can therefore not be matched with any product in the catalogue.`,
887
894
  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`
@@ -1484,7 +1491,7 @@ function stringifyFixedValue(quantity) {
1484
1491
  return String(quantity.value.decimal);
1485
1492
  else return quantity.value.text;
1486
1493
  }
1487
- function parseQuantityInput(input_str) {
1494
+ function parseQuantityValue(input_str) {
1488
1495
  const clean_str = String(input_str).trim();
1489
1496
  if (rangeRegex.test(clean_str)) {
1490
1497
  const range_parts = clean_str.split("-");
@@ -1498,12 +1505,12 @@ function parseQuantityWithUnit(input) {
1498
1505
  const trimmed = input.trim();
1499
1506
  const separatorIndex = trimmed.indexOf("%");
1500
1507
  if (separatorIndex === -1) {
1501
- return { value: parseQuantityInput(trimmed) };
1508
+ return { value: parseQuantityValue(trimmed) };
1502
1509
  }
1503
1510
  const valuePart = trimmed.slice(0, separatorIndex).trim();
1504
1511
  const unitPart = trimmed.slice(separatorIndex + 1).trim();
1505
1512
  return {
1506
- value: parseQuantityInput(valuePart),
1513
+ value: parseQuantityValue(valuePart),
1507
1514
  unit: unitPart || void 0
1508
1515
  };
1509
1516
  }
@@ -1693,7 +1700,7 @@ function parseArbitraryQuantity(raw) {
1693
1700
  "Arbitrary quantities must have a numerical value"
1694
1701
  );
1695
1702
  }
1696
- const value = parseQuantityInput(quantityMatch.groups.quantity);
1703
+ const value = parseQuantityValue(quantityMatch.groups.quantity);
1697
1704
  const unit = quantityMatch.groups.unit;
1698
1705
  if (!value || value.type === "fixed" && value.value.type === "text") {
1699
1706
  throw new InvalidQuantityFormat(
@@ -1707,40 +1714,40 @@ function parseArbitraryQuantity(raw) {
1707
1714
  if (unit) arbitrary.unit = unit;
1708
1715
  return arbitrary;
1709
1716
  }
1710
- function parseScalingMetaVar(content, varName) {
1711
- const complexMatch = content.match(scalingMetaValueWithUnitRegex(varName));
1712
- if (complexMatch?.groups?.arbitraryQuantity) {
1713
- const parsed = parseArbitraryQuantity(
1714
- complexMatch.groups.arbitraryQuantity
1715
- );
1716
- const result2 = {
1717
+ function parseServingsMetaVar(content, varName) {
1718
+ const raw = parseSimpleMetaVar(content, varName);
1719
+ if (raw === void 0) return void 0;
1720
+ const num = Number(raw);
1721
+ if (isNaN(num)) {
1722
+ return { numericValue: 1, rawValue: raw };
1723
+ }
1724
+ return { numericValue: num, rawValue: num };
1725
+ }
1726
+ function parseYieldMetaVar(content) {
1727
+ const match = content.match(yieldMetaValueRegex);
1728
+ if (!match) return void 0;
1729
+ if (match.groups?.arbitraryQuantity) {
1730
+ const parsed = parseArbitraryQuantity(match.groups.arbitraryQuantity);
1731
+ const result = {
1717
1732
  quantity: parsed.quantity
1718
1733
  };
1719
- if (parsed.unit) result2.unit = parsed.unit;
1720
- if (complexMatch.groups.servingsPrefix) {
1721
- result2.textBefore = complexMatch.groups.servingsPrefix;
1734
+ if (parsed.unit) result.unit = parsed.unit;
1735
+ if (match.groups.servingsPrefix) {
1736
+ result.textBefore = match.groups.servingsPrefix;
1722
1737
  }
1723
- if (complexMatch.groups.servingsSuffix) {
1724
- result2.textAfter = complexMatch.groups.servingsSuffix;
1738
+ if (match.groups.servingsSuffix) {
1739
+ result.textAfter = match.groups.servingsSuffix;
1725
1740
  }
1726
- return result2;
1727
- }
1728
- const varMatch = content.match(scalingSimpleMetaValueRegex(varName));
1729
- if (!varMatch) return void 0;
1730
- if (isNaN(Number(varMatch[2]?.trim()))) {
1731
- throw new Error("Scaling variables should be numbers");
1741
+ return result;
1732
1742
  }
1733
- const numericValue = Number(varMatch[2]?.trim());
1734
- const result = {
1735
- quantity: {
1736
- type: "fixed",
1737
- value: { type: "decimal", decimal: numericValue }
1738
- }
1739
- };
1740
- if (varMatch[3]) {
1741
- result.text = `${varMatch[3].trim()}`;
1743
+ if (match.groups?.quantity) {
1744
+ const result = {
1745
+ quantity: parseQuantityValue(match.groups.quantity)
1746
+ };
1747
+ if (match.groups.unit) result.unit = match.groups.unit;
1748
+ return result;
1742
1749
  }
1743
- return result;
1750
+ return void 0;
1744
1751
  }
1745
1752
  function parseListMetaVar(content, varName) {
1746
1753
  const listMatch = content.match(
@@ -1858,12 +1865,12 @@ function parseAnyMetaVar(content, varName) {
1858
1865
  if (simple) return parseMetadataValue(simple);
1859
1866
  return void 0;
1860
1867
  }
1861
- function getNumericValueFromMetaVar(v) {
1868
+ function getNumericValueFromYield(v) {
1862
1869
  if (v.quantity.type === "fixed" && v.quantity.value.type !== "text") {
1863
1870
  return getNumericValue(v.quantity.value);
1864
1871
  }
1865
1872
  if (v.quantity.type === "range") return getNumericValue(v.quantity.min);
1866
- return 0;
1873
+ return 1;
1867
1874
  }
1868
1875
  function extractMetadata(content) {
1869
1876
  const metadata = {};
@@ -1988,11 +1995,16 @@ function extractMetadata(content) {
1988
1995
  };
1989
1996
  unitSystem = unitSystemMap[unitSystemRaw.toLowerCase()];
1990
1997
  }
1991
- for (const metaVar of ["servings", "yield", "serves"]) {
1992
- const scalingMetaValue = parseScalingMetaVar(metadataContent, metaVar);
1993
- if (scalingMetaValue) {
1994
- metadata[metaVar] = scalingMetaValue;
1995
- servings = getNumericValueFromMetaVar(scalingMetaValue);
1998
+ const yieldValue = parseYieldMetaVar(metadataContent);
1999
+ if (yieldValue) {
2000
+ metadata.yield = yieldValue;
2001
+ servings = getNumericValueFromYield(yieldValue);
2002
+ }
2003
+ for (const metaVar of ["serves", "servings"]) {
2004
+ const result = parseServingsMetaVar(metadataContent, metaVar);
2005
+ if (result !== void 0) {
2006
+ metadata[metaVar] = result.rawValue;
2007
+ servings = result.numericValue;
1996
2008
  }
1997
2009
  }
1998
2010
  const tags = parseListMetaVar(metadataContent, "tags");
@@ -2273,7 +2285,7 @@ var ProductCatalog = class {
2273
2285
  const sizeStrings = Array.isArray(size) ? size : [size];
2274
2286
  const sizes = sizeStrings.map((sizeStr) => {
2275
2287
  const sizeAndUnitRaw = sizeStr.split("%");
2276
- const sizeParsed = parseQuantityInput(
2288
+ const sizeParsed = parseQuantityValue(
2277
2289
  sizeAndUnitRaw[0]
2278
2290
  );
2279
2291
  const productSize = { size: sizeParsed };
@@ -2416,7 +2428,7 @@ var Section = class {
2416
2428
  };
2417
2429
 
2418
2430
  // src/quantities/alternatives.ts
2419
- import Big3 from "big.js";
2431
+ import Big4 from "big.js";
2420
2432
 
2421
2433
  // src/units/lookup.ts
2422
2434
  function findListWithCompatibleQuantity(list, quantity) {
@@ -2439,10 +2451,14 @@ function findCompatibleQuantityWithinList(list, quantity) {
2439
2451
  }
2440
2452
 
2441
2453
  // src/utils/general.ts
2454
+ import Big3 from "big.js";
2442
2455
  var legacyDeepClone = (v) => {
2443
2456
  if (v === null || typeof v !== "object") {
2444
2457
  return v;
2445
2458
  }
2459
+ if (v instanceof Big3) {
2460
+ return new Big3(v);
2461
+ }
2446
2462
  if (v instanceof Map) {
2447
2463
  return new Map(
2448
2464
  Array.from(v.entries()).map(([k, val]) => [
@@ -2452,7 +2468,9 @@ var legacyDeepClone = (v) => {
2452
2468
  );
2453
2469
  }
2454
2470
  if (v instanceof Set) {
2455
- return new Set(Array.from(v).map((val) => legacyDeepClone(val)));
2471
+ return new Set(
2472
+ Array.from(v).map((val) => legacyDeepClone(val))
2473
+ );
2456
2474
  }
2457
2475
  if (v instanceof Date) {
2458
2476
  return new Date(v.getTime());
@@ -2466,7 +2484,7 @@ var legacyDeepClone = (v) => {
2466
2484
  }
2467
2485
  return cloned;
2468
2486
  };
2469
- var deepClone = (v) => typeof structuredClone === "function" ? structuredClone(v) : legacyDeepClone(v);
2487
+ var deepClone = (v) => legacyDeepClone(v);
2470
2488
 
2471
2489
  // src/quantities/alternatives.ts
2472
2490
  function getEquivalentUnitsLists(...quantities) {
@@ -2692,7 +2710,7 @@ function regroupQuantitiesAndExpandEquivalents(sum, unitsLists, system) {
2692
2710
  return main.reduce((acc, v) => {
2693
2711
  const mainInList = findCompatibleQuantityWithinList(list, v);
2694
2712
  const conversionRatio = getBaseUnitRatio(v, mainInList);
2695
- const valueInOriginalUnit = Big3(getAverageValue(v.quantity)).times(
2713
+ const valueInOriginalUnit = Big4(getAverageValue(v.quantity)).times(
2696
2714
  conversionRatio
2697
2715
  );
2698
2716
  const newValue = {
@@ -2704,7 +2722,7 @@ function regroupQuantitiesAndExpandEquivalents(sum, unitsLists, system) {
2704
2722
  decimal: valueInOriginalUnit.toNumber()
2705
2723
  }
2706
2724
  },
2707
- Big3(getAverageValue(equiv.quantity)).div(
2725
+ Big4(getAverageValue(equiv.quantity)).div(
2708
2726
  getAverageValue(mainInList.quantity)
2709
2727
  )
2710
2728
  )
@@ -2745,9 +2763,50 @@ function addEquivalentsAndSimplify(quantities, system) {
2745
2763
  return { and: regrouped.map(toPlainUnit) };
2746
2764
  }
2747
2765
  }
2766
+ function buildEquivalenceRatioMap(unitsLists) {
2767
+ const ratioMap = {};
2768
+ for (const list of unitsLists) {
2769
+ for (const equiv of list) {
2770
+ const equivValue = getAverageValue(equiv.quantity);
2771
+ for (const primary of list) {
2772
+ if (primary === equiv) continue;
2773
+ const primaryValue = getAverageValue(primary.quantity);
2774
+ const equivUnit = normalizeUnit(equiv.unit.name)?.name ?? equiv.unit.name;
2775
+ const primaryUnit = normalizeUnit(primary.unit.name)?.name ?? primary.unit.name;
2776
+ ratioMap[equivUnit] ?? (ratioMap[equivUnit] = {});
2777
+ ratioMap[equivUnit][primaryUnit] = equivValue / primaryValue;
2778
+ }
2779
+ }
2780
+ }
2781
+ return ratioMap;
2782
+ }
2783
+ function recomputeEquivalents(primaries, ratioMap, equivUnits) {
2784
+ const equivalents = [];
2785
+ for (const equivUnit of equivUnits) {
2786
+ const ratios = ratioMap[normalizeUnit(equivUnit)?.name ?? equivUnit];
2787
+ let total = 0;
2788
+ for (const primary of primaries) {
2789
+ const pUnit = normalizeUnit(primary.unit ?? NO_UNIT)?.name ?? primary.unit ?? NO_UNIT;
2790
+ const ratio = ratios[pUnit];
2791
+ if (ratio === void 0) continue;
2792
+ const pValue = getAverageValue(primary.quantity);
2793
+ total += pValue * ratio;
2794
+ }
2795
+ if (total > 0) {
2796
+ equivalents.push({
2797
+ quantity: {
2798
+ type: "fixed",
2799
+ value: { type: "decimal", decimal: total }
2800
+ },
2801
+ ...equivUnit !== "" && { unit: equivUnit }
2802
+ });
2803
+ }
2804
+ }
2805
+ return equivalents.length > 0 ? equivalents : void 0;
2806
+ }
2748
2807
 
2749
2808
  // src/classes/recipe.ts
2750
- import Big4 from "big.js";
2809
+ import Big5 from "big.js";
2751
2810
  var _Recipe = class _Recipe {
2752
2811
  /**
2753
2812
  * Creates a new Recipe instance.
@@ -2869,7 +2928,7 @@ var _Recipe = class _Recipe {
2869
2928
  let quantityMatch = quantityRaw.match(quantityAlternativeRegex);
2870
2929
  const quantities = [];
2871
2930
  while (quantityMatch?.groups) {
2872
- const value = quantityMatch.groups.quantity ? parseQuantityInput(quantityMatch.groups.quantity) : void 0;
2931
+ const value = quantityMatch.groups.quantity ? parseQuantityValue(quantityMatch.groups.quantity) : void 0;
2873
2932
  const unit = quantityMatch.groups.unit;
2874
2933
  if (value) {
2875
2934
  const newQuantity = { quantity: value };
@@ -3237,7 +3296,9 @@ var _Recipe = class _Recipe {
3237
3296
  } else {
3238
3297
  const targetSubgroupIndex = 0;
3239
3298
  const selectedSubgroup = groupSubgroups?.[targetSubgroupIndex];
3240
- isSelected = selectedSubgroup?.some((alt) => alt.itemId === item.id) ?? false;
3299
+ isSelected = selectedSubgroup.some(
3300
+ (alt) => alt.itemId === item.id
3301
+ );
3241
3302
  }
3242
3303
  } else {
3243
3304
  const targetSubgroupIndex = groupChoice ?? 0;
@@ -3636,16 +3697,10 @@ var _Recipe = class _Recipe {
3636
3697
  }
3637
3698
  sectionName = sectionName.slice(sectionVarMatch[0].length).trim();
3638
3699
  }
3639
- if (this.sections.length === 0 && section.isBlank()) {
3640
- section.name = sectionName;
3641
- if (sectionVariants) section.variants = sectionVariants;
3642
- if (sectionOptional) section.optional = true;
3643
- } else {
3644
- if (!section.isBlank()) {
3645
- this.sections.push(section);
3646
- }
3647
- section = new Section(sectionName, sectionVariants, sectionOptional);
3700
+ if (!section.isBlank()) {
3701
+ this.sections.push(section);
3648
3702
  }
3703
+ section = new Section(sectionName, sectionVariants, sectionOptional);
3649
3704
  blankLineBefore = true;
3650
3705
  inNote = false;
3651
3706
  continue;
@@ -3711,7 +3766,7 @@ var _Recipe = class _Recipe {
3711
3766
  if (modifiers !== void 0 && modifiers.includes("-")) {
3712
3767
  flags.push("hidden");
3713
3768
  }
3714
- const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
3769
+ const quantity = quantityRaw ? parseQuantityValue(quantityRaw) : void 0;
3715
3770
  const newCookware = {
3716
3771
  name
3717
3772
  };
@@ -3743,7 +3798,7 @@ var _Recipe = class _Recipe {
3743
3798
  throw new Error("Timer missing unit");
3744
3799
  }
3745
3800
  const name = groups.timerName || void 0;
3746
- const duration = parseQuantityInput(durationStr);
3801
+ const duration = parseQuantityValue(durationStr);
3747
3802
  const timerObj = {
3748
3803
  name,
3749
3804
  duration,
@@ -3783,7 +3838,7 @@ var _Recipe = class _Recipe {
3783
3838
  if (originalServings === void 0 || originalServings === 0) {
3784
3839
  originalServings = 1;
3785
3840
  }
3786
- const factor = Big4(newServings).div(originalServings);
3841
+ const factor = Big5(newServings).div(originalServings);
3787
3842
  return this.scaleBy(factor);
3788
3843
  }
3789
3844
  /**
@@ -3802,7 +3857,7 @@ var _Recipe = class _Recipe {
3802
3857
  function scaleAlternativesBy(alternatives, factor2) {
3803
3858
  for (const alternative of alternatives) {
3804
3859
  if (alternative.quantity) {
3805
- const scaleFactor = alternative.scalable ? Big4(factor2) : 1;
3860
+ const scaleFactor = alternative.scalable ? Big5(factor2) : 1;
3806
3861
  if (alternative.quantity.type !== "fixed" || alternative.quantity.value.type !== "text") {
3807
3862
  alternative.quantity = multiplyQuantityValue(
3808
3863
  alternative.quantity,
@@ -3875,10 +3930,16 @@ var _Recipe = class _Recipe {
3875
3930
  arbitrary.unit = optimized.unit;
3876
3931
  }
3877
3932
  newRecipe._populateIngredientQuantities();
3878
- newRecipe.servings = Big4(originalServings).times(factor).toNumber();
3879
- for (const metaVar of ["servings", "yield", "serves"]) {
3880
- if (newRecipe.metadata[metaVar] && this.metadata[metaVar]) {
3881
- const original = this.metadata[metaVar];
3933
+ newRecipe.servings = Big5(originalServings).times(factor).toNumber();
3934
+ for (const metaVar of ["servings", "serves"]) {
3935
+ if (typeof newRecipe.metadata[metaVar] === "number") {
3936
+ newRecipe.metadata[metaVar] = Big5(newRecipe.metadata[metaVar]).times(factor).toNumber();
3937
+ }
3938
+ }
3939
+ if (newRecipe.metadata.yield && this.metadata.yield) {
3940
+ const original = this.metadata.yield;
3941
+ if (original.quantity.type === "fixed" && original.quantity.value.type === "text") {
3942
+ } else {
3882
3943
  const scaledQuantity = multiplyQuantityValue(
3883
3944
  original.quantity,
3884
3945
  factor
@@ -3893,8 +3954,7 @@ var _Recipe = class _Recipe {
3893
3954
  if (optimized.unit) scaled.unit = optimized.unit;
3894
3955
  if (original.textBefore) scaled.textBefore = original.textBefore;
3895
3956
  if (original.textAfter) scaled.textAfter = original.textAfter;
3896
- if (original.text) scaled.text = original.text;
3897
- newRecipe.metadata[metaVar] = scaled;
3957
+ newRecipe.metadata.yield = scaled;
3898
3958
  }
3899
3959
  }
3900
3960
  return newRecipe;
@@ -4111,7 +4171,7 @@ __publicField(_Recipe, "subgroupIndices", /* @__PURE__ */ new WeakMap());
4111
4171
  var Recipe = _Recipe;
4112
4172
 
4113
4173
  // src/classes/shopping_list.ts
4114
- var ShoppingList = class _ShoppingList {
4174
+ var ShoppingList = class {
4115
4175
  /**
4116
4176
  * Creates a new ShoppingList instance
4117
4177
  * @param categoryConfigStr - The category configuration to parse.
@@ -4206,7 +4266,7 @@ var ShoppingList = class _ShoppingList {
4206
4266
  }
4207
4267
  }
4208
4268
  if (numericEntries.length > 1) {
4209
- const ratioMap = _ShoppingList.buildEquivalenceRatioMap(
4269
+ const ratioMap = buildEquivalenceRatioMap(
4210
4270
  getEquivalentUnitsLists(...numericEntries)
4211
4271
  );
4212
4272
  if (Object.keys(ratioMap).length > 0) {
@@ -4263,10 +4323,12 @@ var ShoppingList = class _ShoppingList {
4263
4323
  const pantryHasUnit = pantryExtended.unit !== void 0 && pantryExtended.unit.name !== "";
4264
4324
  const ratioMap = this.equivalenceRatios.get(ingredient.name);
4265
4325
  const unitMismatch = leafHasUnit !== pantryHasUnit && ratioMap !== void 0;
4326
+ const leafDef = normalizeUnit(leaf.unit);
4327
+ const pantryDef = normalizeUnit(pantryExtended.unit?.name);
4266
4328
  if (unitMismatch) {
4267
4329
  const leafUnit = leaf.unit ?? NO_UNIT;
4268
4330
  const pantryUnit = pantryExtended.unit?.name ?? NO_UNIT;
4269
- const ratioFromPantry = ratioMap[leafUnit]?.[pantryUnit];
4331
+ const ratioFromPantry = ratioMap[normalizeUnit(leafUnit)?.name ?? leafUnit]?.[normalizeUnit(pantryUnit)?.name ?? pantryUnit];
4270
4332
  if (ratioFromPantry !== void 0) {
4271
4333
  const pantryValue = getAverageValue(pantryExtended.quantity);
4272
4334
  const leafValue = getAverageValue(ingredientExtended.quantity);
@@ -4281,7 +4343,7 @@ var ShoppingList = class _ShoppingList {
4281
4343
  type: "fixed",
4282
4344
  value: { type: "decimal", decimal: remainingLeafValue }
4283
4345
  };
4284
- const consumedInPantryUnits = ratioFromPantry !== 0 ? subtracted / ratioFromPantry : pantryValue;
4346
+ const consumedInPantryUnits = subtracted / ratioFromPantry;
4285
4347
  const remainingPantryValue = Math.max(
4286
4348
  pantryValue - consumedInPantryUnits,
4287
4349
  0
@@ -4298,9 +4360,10 @@ var ShoppingList = class _ShoppingList {
4298
4360
  };
4299
4361
  continue;
4300
4362
  }
4363
+ } else {
4364
+ continue;
4301
4365
  }
4302
- }
4303
- try {
4366
+ } else if (leafDef && pantryDef && areUnitsConvertible(leafDef, pantryDef) || (leaf.unit ?? "").toLowerCase() === (pantryExtended.unit?.name ?? "").toLowerCase()) {
4304
4367
  const remaining = subtractQuantities(
4305
4368
  ingredientExtended,
4306
4369
  pantryExtended,
@@ -4315,7 +4378,47 @@ var ShoppingList = class _ShoppingList {
4315
4378
  const updated = toPlainUnit(remaining);
4316
4379
  leaf.quantity = updated.quantity;
4317
4380
  leaf.unit = updated.unit;
4318
- } catch {
4381
+ } else if (ratioMap) {
4382
+ const canonicalLeaf = normalizeUnit(leaf.unit)?.name ?? leaf.unit;
4383
+ const leafValue = getAverageValue(ingredientExtended.quantity);
4384
+ const pantryValue = getAverageValue(pantryExtended.quantity);
4385
+ if (typeof leafValue === "number" && typeof pantryValue === "number" && pantryDef) {
4386
+ for (const [equivUnit, ratios] of Object.entries(ratioMap)) {
4387
+ const ratio = ratios[canonicalLeaf];
4388
+ if (ratio === void 0) continue;
4389
+ const equivDef = normalizeUnit(equivUnit);
4390
+ if (!equivDef || !areUnitsConvertible(equivDef, pantryDef))
4391
+ continue;
4392
+ const pantryInEquiv = pantryValue * getToBase(pantryDef) / getToBase(equivDef);
4393
+ const pantryInLeafUnits = pantryInEquiv / ratio;
4394
+ const subtracted = Math.min(pantryInLeafUnits, leafValue);
4395
+ const remainingLeafValue = Math.max(
4396
+ leafValue - pantryInLeafUnits,
4397
+ 0
4398
+ );
4399
+ leaf.quantity = {
4400
+ type: "fixed",
4401
+ value: { type: "decimal", decimal: remainingLeafValue }
4402
+ };
4403
+ const consumedInEquiv = subtracted * ratio;
4404
+ const consumedInPantryUnits = consumedInEquiv * getToBase(equivDef) / getToBase(pantryDef);
4405
+ const remainingPantryValue = Math.max(
4406
+ pantryValue - consumedInPantryUnits,
4407
+ 0
4408
+ );
4409
+ pantryExtended = {
4410
+ quantity: {
4411
+ type: "fixed",
4412
+ value: {
4413
+ type: "decimal",
4414
+ decimal: remainingPantryValue
4415
+ }
4416
+ },
4417
+ ...pantryExtended.unit && { unit: pantryExtended.unit }
4418
+ };
4419
+ break;
4420
+ }
4421
+ }
4319
4422
  }
4320
4423
  }
4321
4424
  if ("and" in entry) {
@@ -4326,8 +4429,8 @@ var ShoppingList = class _ShoppingList {
4326
4429
  entry.and.push(...nonZero);
4327
4430
  const ratioMap = this.equivalenceRatios.get(ingredient.name);
4328
4431
  if (entry.equivalents && ratioMap) {
4329
- const equivUnits = entry.equivalents.map((e2) => e2.unit ?? NO_UNIT);
4330
- entry.equivalents = _ShoppingList.recomputeEquivalents(
4432
+ const equivUnits = entry.equivalents.map((e2) => e2.unit);
4433
+ entry.equivalents = recomputeEquivalents(
4331
4434
  entry.and,
4332
4435
  ratioMap,
4333
4436
  equivUnits
@@ -4338,17 +4441,17 @@ var ShoppingList = class _ShoppingList {
4338
4441
  ingredient.quantities[i2] = {
4339
4442
  quantity: single.quantity,
4340
4443
  ...single.unit && { unit: single.unit },
4341
- ...entry.equivalents && { equivalents: entry.equivalents },
4342
- ...entry.alternatives && { alternatives: entry.alternatives }
4444
+ ...entry.equivalents && { equivalents: entry.equivalents }
4343
4445
  };
4344
4446
  }
4345
4447
  } else if ("equivalents" in entry && entry.equivalents) {
4346
4448
  const ratioMap = this.equivalenceRatios.get(ingredient.name);
4347
4449
  if (ratioMap) {
4348
4450
  const equivUnits = entry.equivalents.map(
4349
- (e2) => e2.unit ?? NO_UNIT
4451
+ (e2) => e2.unit
4452
+ // equivalents always have units
4350
4453
  );
4351
- const recomputed = _ShoppingList.recomputeEquivalents(
4454
+ const recomputed = recomputeEquivalents(
4352
4455
  [entry],
4353
4456
  ratioMap,
4354
4457
  equivUnits
@@ -4371,57 +4474,6 @@ var ShoppingList = class _ShoppingList {
4371
4474
  }
4372
4475
  this.resultingPantry = clonedPantry;
4373
4476
  }
4374
- /**
4375
- * Builds a ratio map from equivalence lists.
4376
- * For each equivalence list, stores ratio = equiv_value / primary_value
4377
- * for every pair of units, so equivalents can be recomputed after
4378
- * pantry subtraction modifies primary quantities.
4379
- */
4380
- static buildEquivalenceRatioMap(unitsLists) {
4381
- const ratioMap = {};
4382
- for (const list of unitsLists) {
4383
- for (const equiv of list) {
4384
- const equivValue = getAverageValue(equiv.quantity);
4385
- for (const primary of list) {
4386
- if (primary === equiv) continue;
4387
- const primaryValue = getAverageValue(primary.quantity);
4388
- const equivUnit = equiv.unit.name;
4389
- const primaryUnit = primary.unit.name;
4390
- ratioMap[equivUnit] ?? (ratioMap[equivUnit] = {});
4391
- ratioMap[equivUnit][primaryUnit] = equivValue / primaryValue;
4392
- }
4393
- }
4394
- }
4395
- return ratioMap;
4396
- }
4397
- /**
4398
- * Recomputes equivalent quantities from current primary values and stored ratios.
4399
- * For each equivalent unit in equivUnits, new_value = Σ (primary_value × ratio[equivUnit][primaryUnit]).
4400
- * Returns undefined if all equivalents compute to zero.
4401
- */
4402
- static recomputeEquivalents(primaries, ratioMap, equivUnits) {
4403
- const equivalents = [];
4404
- for (const equivUnit of equivUnits) {
4405
- const ratios = ratioMap[equivUnit];
4406
- let total = 0;
4407
- for (const primary of primaries) {
4408
- const pUnit = primary.unit ?? NO_UNIT;
4409
- const ratio = ratios[pUnit];
4410
- const pValue = getAverageValue(primary.quantity);
4411
- total += pValue * ratio;
4412
- }
4413
- if (total > 0) {
4414
- equivalents.push({
4415
- quantity: {
4416
- type: "fixed",
4417
- value: { type: "decimal", decimal: total }
4418
- },
4419
- ...equivUnit !== "" && { unit: equivUnit }
4420
- });
4421
- }
4422
- }
4423
- return equivalents.length > 0 ? equivalents : void 0;
4424
- }
4425
4477
  /**
4426
4478
  * Adds a recipe to the shopping list, then automatically
4427
4479
  * recalculates the quantities and recategorize the ingredients.
@@ -5035,6 +5087,9 @@ export {
5035
5087
  // v8 ignore if -- @preserve: defensive type guard
5036
5088
  /* v8 ignore if -- @preserve */
5037
5089
  // v8 ignore next -- @preserve
5090
+ // v8 ignore else -- @preserve: text quantities never reach the equivalence path
5038
5091
  // v8 ignore else --@preserve: defensive type guard
5039
5092
  // v8 ignore else -- @preserve: detection if
5093
+ /* v8 ignore else -- @preserve: only act when there are matches */
5094
+ /* v8 ignore else -- @preserve: initialization pattern */
5040
5095
  //# sourceMappingURL=index.js.map