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

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 };
@@ -2498,34 +2510,7 @@ function findCompatibleQuantityWithinList(list, quantity) {
2498
2510
  }
2499
2511
 
2500
2512
  // src/utils/general.ts
2501
- var legacyDeepClone = (v) => {
2502
- if (v === null || typeof v !== "object") {
2503
- return v;
2504
- }
2505
- if (v instanceof Map) {
2506
- return new Map(
2507
- Array.from(v.entries()).map(([k, val]) => [
2508
- legacyDeepClone(k),
2509
- legacyDeepClone(val)
2510
- ])
2511
- );
2512
- }
2513
- if (v instanceof Set) {
2514
- return new Set(Array.from(v).map((val) => legacyDeepClone(val)));
2515
- }
2516
- if (v instanceof Date) {
2517
- return new Date(v.getTime());
2518
- }
2519
- if (Array.isArray(v)) {
2520
- return v.map((item) => legacyDeepClone(item));
2521
- }
2522
- const cloned = {};
2523
- for (const key of Object.keys(v)) {
2524
- cloned[key] = legacyDeepClone(v[key]);
2525
- }
2526
- return cloned;
2527
- };
2528
- var deepClone = (v) => typeof structuredClone === "function" ? structuredClone(v) : legacyDeepClone(v);
2513
+ var deepClone = (v) => structuredClone(v);
2529
2514
 
2530
2515
  // src/quantities/alternatives.ts
2531
2516
  function getEquivalentUnitsLists(...quantities) {
@@ -2804,6 +2789,47 @@ function addEquivalentsAndSimplify(quantities, system) {
2804
2789
  return { and: regrouped.map(toPlainUnit) };
2805
2790
  }
2806
2791
  }
2792
+ function buildEquivalenceRatioMap(unitsLists) {
2793
+ const ratioMap = {};
2794
+ for (const list of unitsLists) {
2795
+ for (const equiv of list) {
2796
+ const equivValue = getAverageValue(equiv.quantity);
2797
+ for (const primary of list) {
2798
+ if (primary === equiv) continue;
2799
+ const primaryValue = getAverageValue(primary.quantity);
2800
+ const equivUnit = normalizeUnit(equiv.unit.name)?.name ?? equiv.unit.name;
2801
+ const primaryUnit = normalizeUnit(primary.unit.name)?.name ?? primary.unit.name;
2802
+ ratioMap[equivUnit] ?? (ratioMap[equivUnit] = {});
2803
+ ratioMap[equivUnit][primaryUnit] = equivValue / primaryValue;
2804
+ }
2805
+ }
2806
+ }
2807
+ return ratioMap;
2808
+ }
2809
+ function recomputeEquivalents(primaries, ratioMap, equivUnits) {
2810
+ const equivalents = [];
2811
+ for (const equivUnit of equivUnits) {
2812
+ const ratios = ratioMap[normalizeUnit(equivUnit)?.name ?? equivUnit];
2813
+ let total = 0;
2814
+ for (const primary of primaries) {
2815
+ const pUnit = normalizeUnit(primary.unit ?? NO_UNIT)?.name ?? primary.unit ?? NO_UNIT;
2816
+ const ratio = ratios[pUnit];
2817
+ if (ratio === void 0) continue;
2818
+ const pValue = getAverageValue(primary.quantity);
2819
+ total += pValue * ratio;
2820
+ }
2821
+ if (total > 0) {
2822
+ equivalents.push({
2823
+ quantity: {
2824
+ type: "fixed",
2825
+ value: { type: "decimal", decimal: total }
2826
+ },
2827
+ ...equivUnit !== "" && { unit: equivUnit }
2828
+ });
2829
+ }
2830
+ }
2831
+ return equivalents.length > 0 ? equivalents : void 0;
2832
+ }
2807
2833
 
2808
2834
  // src/classes/recipe.ts
2809
2835
  var import_big4 = __toESM(require("big.js"), 1);
@@ -2928,7 +2954,7 @@ var _Recipe = class _Recipe {
2928
2954
  let quantityMatch = quantityRaw.match(quantityAlternativeRegex);
2929
2955
  const quantities = [];
2930
2956
  while (quantityMatch?.groups) {
2931
- const value = quantityMatch.groups.quantity ? parseQuantityInput(quantityMatch.groups.quantity) : void 0;
2957
+ const value = quantityMatch.groups.quantity ? parseQuantityValue(quantityMatch.groups.quantity) : void 0;
2932
2958
  const unit = quantityMatch.groups.unit;
2933
2959
  if (value) {
2934
2960
  const newQuantity = { quantity: value };
@@ -3296,7 +3322,9 @@ var _Recipe = class _Recipe {
3296
3322
  } else {
3297
3323
  const targetSubgroupIndex = 0;
3298
3324
  const selectedSubgroup = groupSubgroups?.[targetSubgroupIndex];
3299
- isSelected = selectedSubgroup?.some((alt) => alt.itemId === item.id) ?? false;
3325
+ isSelected = selectedSubgroup.some(
3326
+ (alt) => alt.itemId === item.id
3327
+ );
3300
3328
  }
3301
3329
  } else {
3302
3330
  const targetSubgroupIndex = groupChoice ?? 0;
@@ -3695,16 +3723,10 @@ var _Recipe = class _Recipe {
3695
3723
  }
3696
3724
  sectionName = sectionName.slice(sectionVarMatch[0].length).trim();
3697
3725
  }
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);
3726
+ if (!section.isBlank()) {
3727
+ this.sections.push(section);
3707
3728
  }
3729
+ section = new Section(sectionName, sectionVariants, sectionOptional);
3708
3730
  blankLineBefore = true;
3709
3731
  inNote = false;
3710
3732
  continue;
@@ -3770,7 +3792,7 @@ var _Recipe = class _Recipe {
3770
3792
  if (modifiers !== void 0 && modifiers.includes("-")) {
3771
3793
  flags.push("hidden");
3772
3794
  }
3773
- const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
3795
+ const quantity = quantityRaw ? parseQuantityValue(quantityRaw) : void 0;
3774
3796
  const newCookware = {
3775
3797
  name
3776
3798
  };
@@ -3802,7 +3824,7 @@ var _Recipe = class _Recipe {
3802
3824
  throw new Error("Timer missing unit");
3803
3825
  }
3804
3826
  const name = groups.timerName || void 0;
3805
- const duration = parseQuantityInput(durationStr);
3827
+ const duration = parseQuantityValue(durationStr);
3806
3828
  const timerObj = {
3807
3829
  name,
3808
3830
  duration,
@@ -3935,9 +3957,15 @@ var _Recipe = class _Recipe {
3935
3957
  }
3936
3958
  newRecipe._populateIngredientQuantities();
3937
3959
  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];
3960
+ for (const metaVar of ["servings", "serves"]) {
3961
+ if (typeof newRecipe.metadata[metaVar] === "number") {
3962
+ newRecipe.metadata[metaVar] = (0, import_big4.default)(newRecipe.metadata[metaVar]).times(factor).toNumber();
3963
+ }
3964
+ }
3965
+ if (newRecipe.metadata.yield && this.metadata.yield) {
3966
+ const original = this.metadata.yield;
3967
+ if (original.quantity.type === "fixed" && original.quantity.value.type === "text") {
3968
+ } else {
3941
3969
  const scaledQuantity = multiplyQuantityValue(
3942
3970
  original.quantity,
3943
3971
  factor
@@ -3952,8 +3980,7 @@ var _Recipe = class _Recipe {
3952
3980
  if (optimized.unit) scaled.unit = optimized.unit;
3953
3981
  if (original.textBefore) scaled.textBefore = original.textBefore;
3954
3982
  if (original.textAfter) scaled.textAfter = original.textAfter;
3955
- if (original.text) scaled.text = original.text;
3956
- newRecipe.metadata[metaVar] = scaled;
3983
+ newRecipe.metadata.yield = scaled;
3957
3984
  }
3958
3985
  }
3959
3986
  return newRecipe;
@@ -4170,7 +4197,7 @@ __publicField(_Recipe, "subgroupIndices", /* @__PURE__ */ new WeakMap());
4170
4197
  var Recipe = _Recipe;
4171
4198
 
4172
4199
  // src/classes/shopping_list.ts
4173
- var ShoppingList = class _ShoppingList {
4200
+ var ShoppingList = class {
4174
4201
  /**
4175
4202
  * Creates a new ShoppingList instance
4176
4203
  * @param categoryConfigStr - The category configuration to parse.
@@ -4265,7 +4292,7 @@ var ShoppingList = class _ShoppingList {
4265
4292
  }
4266
4293
  }
4267
4294
  if (numericEntries.length > 1) {
4268
- const ratioMap = _ShoppingList.buildEquivalenceRatioMap(
4295
+ const ratioMap = buildEquivalenceRatioMap(
4269
4296
  getEquivalentUnitsLists(...numericEntries)
4270
4297
  );
4271
4298
  if (Object.keys(ratioMap).length > 0) {
@@ -4322,10 +4349,12 @@ var ShoppingList = class _ShoppingList {
4322
4349
  const pantryHasUnit = pantryExtended.unit !== void 0 && pantryExtended.unit.name !== "";
4323
4350
  const ratioMap = this.equivalenceRatios.get(ingredient.name);
4324
4351
  const unitMismatch = leafHasUnit !== pantryHasUnit && ratioMap !== void 0;
4352
+ const leafDef = normalizeUnit(leaf.unit);
4353
+ const pantryDef = normalizeUnit(pantryExtended.unit?.name);
4325
4354
  if (unitMismatch) {
4326
4355
  const leafUnit = leaf.unit ?? NO_UNIT;
4327
4356
  const pantryUnit = pantryExtended.unit?.name ?? NO_UNIT;
4328
- const ratioFromPantry = ratioMap[leafUnit]?.[pantryUnit];
4357
+ const ratioFromPantry = ratioMap[normalizeUnit(leafUnit)?.name ?? leafUnit]?.[normalizeUnit(pantryUnit)?.name ?? pantryUnit];
4329
4358
  if (ratioFromPantry !== void 0) {
4330
4359
  const pantryValue = getAverageValue(pantryExtended.quantity);
4331
4360
  const leafValue = getAverageValue(ingredientExtended.quantity);
@@ -4340,7 +4369,7 @@ var ShoppingList = class _ShoppingList {
4340
4369
  type: "fixed",
4341
4370
  value: { type: "decimal", decimal: remainingLeafValue }
4342
4371
  };
4343
- const consumedInPantryUnits = ratioFromPantry !== 0 ? subtracted / ratioFromPantry : pantryValue;
4372
+ const consumedInPantryUnits = subtracted / ratioFromPantry;
4344
4373
  const remainingPantryValue = Math.max(
4345
4374
  pantryValue - consumedInPantryUnits,
4346
4375
  0
@@ -4357,9 +4386,10 @@ var ShoppingList = class _ShoppingList {
4357
4386
  };
4358
4387
  continue;
4359
4388
  }
4389
+ } else {
4390
+ continue;
4360
4391
  }
4361
- }
4362
- try {
4392
+ } else if (leafDef && pantryDef && areUnitsConvertible(leafDef, pantryDef) || (leaf.unit ?? "").toLowerCase() === (pantryExtended.unit?.name ?? "").toLowerCase()) {
4363
4393
  const remaining = subtractQuantities(
4364
4394
  ingredientExtended,
4365
4395
  pantryExtended,
@@ -4374,7 +4404,47 @@ var ShoppingList = class _ShoppingList {
4374
4404
  const updated = toPlainUnit(remaining);
4375
4405
  leaf.quantity = updated.quantity;
4376
4406
  leaf.unit = updated.unit;
4377
- } catch {
4407
+ } else if (ratioMap) {
4408
+ const canonicalLeaf = normalizeUnit(leaf.unit)?.name ?? leaf.unit;
4409
+ const leafValue = getAverageValue(ingredientExtended.quantity);
4410
+ const pantryValue = getAverageValue(pantryExtended.quantity);
4411
+ if (typeof leafValue === "number" && typeof pantryValue === "number" && pantryDef) {
4412
+ for (const [equivUnit, ratios] of Object.entries(ratioMap)) {
4413
+ const ratio = ratios[canonicalLeaf];
4414
+ if (ratio === void 0) continue;
4415
+ const equivDef = normalizeUnit(equivUnit);
4416
+ if (!equivDef || !areUnitsConvertible(equivDef, pantryDef))
4417
+ continue;
4418
+ const pantryInEquiv = pantryValue * getToBase(pantryDef) / getToBase(equivDef);
4419
+ const pantryInLeafUnits = pantryInEquiv / ratio;
4420
+ const subtracted = Math.min(pantryInLeafUnits, leafValue);
4421
+ const remainingLeafValue = Math.max(
4422
+ leafValue - pantryInLeafUnits,
4423
+ 0
4424
+ );
4425
+ leaf.quantity = {
4426
+ type: "fixed",
4427
+ value: { type: "decimal", decimal: remainingLeafValue }
4428
+ };
4429
+ const consumedInEquiv = subtracted * ratio;
4430
+ const consumedInPantryUnits = consumedInEquiv * getToBase(equivDef) / getToBase(pantryDef);
4431
+ const remainingPantryValue = Math.max(
4432
+ pantryValue - consumedInPantryUnits,
4433
+ 0
4434
+ );
4435
+ pantryExtended = {
4436
+ quantity: {
4437
+ type: "fixed",
4438
+ value: {
4439
+ type: "decimal",
4440
+ decimal: remainingPantryValue
4441
+ }
4442
+ },
4443
+ ...pantryExtended.unit && { unit: pantryExtended.unit }
4444
+ };
4445
+ break;
4446
+ }
4447
+ }
4378
4448
  }
4379
4449
  }
4380
4450
  if ("and" in entry) {
@@ -4385,8 +4455,8 @@ var ShoppingList = class _ShoppingList {
4385
4455
  entry.and.push(...nonZero);
4386
4456
  const ratioMap = this.equivalenceRatios.get(ingredient.name);
4387
4457
  if (entry.equivalents && ratioMap) {
4388
- const equivUnits = entry.equivalents.map((e2) => e2.unit ?? NO_UNIT);
4389
- entry.equivalents = _ShoppingList.recomputeEquivalents(
4458
+ const equivUnits = entry.equivalents.map((e2) => e2.unit);
4459
+ entry.equivalents = recomputeEquivalents(
4390
4460
  entry.and,
4391
4461
  ratioMap,
4392
4462
  equivUnits
@@ -4397,17 +4467,17 @@ var ShoppingList = class _ShoppingList {
4397
4467
  ingredient.quantities[i2] = {
4398
4468
  quantity: single.quantity,
4399
4469
  ...single.unit && { unit: single.unit },
4400
- ...entry.equivalents && { equivalents: entry.equivalents },
4401
- ...entry.alternatives && { alternatives: entry.alternatives }
4470
+ ...entry.equivalents && { equivalents: entry.equivalents }
4402
4471
  };
4403
4472
  }
4404
4473
  } else if ("equivalents" in entry && entry.equivalents) {
4405
4474
  const ratioMap = this.equivalenceRatios.get(ingredient.name);
4406
4475
  if (ratioMap) {
4407
4476
  const equivUnits = entry.equivalents.map(
4408
- (e2) => e2.unit ?? NO_UNIT
4477
+ (e2) => e2.unit
4478
+ // equivalents always have units
4409
4479
  );
4410
- const recomputed = _ShoppingList.recomputeEquivalents(
4480
+ const recomputed = recomputeEquivalents(
4411
4481
  [entry],
4412
4482
  ratioMap,
4413
4483
  equivUnits
@@ -4430,57 +4500,6 @@ var ShoppingList = class _ShoppingList {
4430
4500
  }
4431
4501
  this.resultingPantry = clonedPantry;
4432
4502
  }
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
4503
  /**
4485
4504
  * Adds a recipe to the shopping list, then automatically
4486
4505
  * recalculates the quantities and recategorize the ingredients.
@@ -5095,6 +5114,9 @@ function getEffectiveChoices(recipe, variant) {
5095
5114
  // v8 ignore if -- @preserve: defensive type guard
5096
5115
  /* v8 ignore if -- @preserve */
5097
5116
  // v8 ignore next -- @preserve
5117
+ // v8 ignore else -- @preserve: text quantities never reach the equivalence path
5098
5118
  // v8 ignore else --@preserve: defensive type guard
5099
5119
  // v8 ignore else -- @preserve: detection if
5120
+ /* v8 ignore else -- @preserve: only act when there are matches */
5121
+ /* v8 ignore else -- @preserve: initialization pattern */
5100
5122
  //# sourceMappingURL=index.cjs.map