@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.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 };
@@ -2439,34 +2451,7 @@ function findCompatibleQuantityWithinList(list, quantity) {
2439
2451
  }
2440
2452
 
2441
2453
  // src/utils/general.ts
2442
- var legacyDeepClone = (v) => {
2443
- if (v === null || typeof v !== "object") {
2444
- return v;
2445
- }
2446
- if (v instanceof Map) {
2447
- return new Map(
2448
- Array.from(v.entries()).map(([k, val]) => [
2449
- legacyDeepClone(k),
2450
- legacyDeepClone(val)
2451
- ])
2452
- );
2453
- }
2454
- if (v instanceof Set) {
2455
- return new Set(Array.from(v).map((val) => legacyDeepClone(val)));
2456
- }
2457
- if (v instanceof Date) {
2458
- return new Date(v.getTime());
2459
- }
2460
- if (Array.isArray(v)) {
2461
- return v.map((item) => legacyDeepClone(item));
2462
- }
2463
- const cloned = {};
2464
- for (const key of Object.keys(v)) {
2465
- cloned[key] = legacyDeepClone(v[key]);
2466
- }
2467
- return cloned;
2468
- };
2469
- var deepClone = (v) => typeof structuredClone === "function" ? structuredClone(v) : legacyDeepClone(v);
2454
+ var deepClone = (v) => structuredClone(v);
2470
2455
 
2471
2456
  // src/quantities/alternatives.ts
2472
2457
  function getEquivalentUnitsLists(...quantities) {
@@ -2745,6 +2730,47 @@ function addEquivalentsAndSimplify(quantities, system) {
2745
2730
  return { and: regrouped.map(toPlainUnit) };
2746
2731
  }
2747
2732
  }
2733
+ function buildEquivalenceRatioMap(unitsLists) {
2734
+ const ratioMap = {};
2735
+ for (const list of unitsLists) {
2736
+ for (const equiv of list) {
2737
+ const equivValue = getAverageValue(equiv.quantity);
2738
+ for (const primary of list) {
2739
+ if (primary === equiv) continue;
2740
+ const primaryValue = getAverageValue(primary.quantity);
2741
+ const equivUnit = normalizeUnit(equiv.unit.name)?.name ?? equiv.unit.name;
2742
+ const primaryUnit = normalizeUnit(primary.unit.name)?.name ?? primary.unit.name;
2743
+ ratioMap[equivUnit] ?? (ratioMap[equivUnit] = {});
2744
+ ratioMap[equivUnit][primaryUnit] = equivValue / primaryValue;
2745
+ }
2746
+ }
2747
+ }
2748
+ return ratioMap;
2749
+ }
2750
+ function recomputeEquivalents(primaries, ratioMap, equivUnits) {
2751
+ const equivalents = [];
2752
+ for (const equivUnit of equivUnits) {
2753
+ const ratios = ratioMap[normalizeUnit(equivUnit)?.name ?? equivUnit];
2754
+ let total = 0;
2755
+ for (const primary of primaries) {
2756
+ const pUnit = normalizeUnit(primary.unit ?? NO_UNIT)?.name ?? primary.unit ?? NO_UNIT;
2757
+ const ratio = ratios[pUnit];
2758
+ if (ratio === void 0) continue;
2759
+ const pValue = getAverageValue(primary.quantity);
2760
+ total += pValue * ratio;
2761
+ }
2762
+ if (total > 0) {
2763
+ equivalents.push({
2764
+ quantity: {
2765
+ type: "fixed",
2766
+ value: { type: "decimal", decimal: total }
2767
+ },
2768
+ ...equivUnit !== "" && { unit: equivUnit }
2769
+ });
2770
+ }
2771
+ }
2772
+ return equivalents.length > 0 ? equivalents : void 0;
2773
+ }
2748
2774
 
2749
2775
  // src/classes/recipe.ts
2750
2776
  import Big4 from "big.js";
@@ -2869,7 +2895,7 @@ var _Recipe = class _Recipe {
2869
2895
  let quantityMatch = quantityRaw.match(quantityAlternativeRegex);
2870
2896
  const quantities = [];
2871
2897
  while (quantityMatch?.groups) {
2872
- const value = quantityMatch.groups.quantity ? parseQuantityInput(quantityMatch.groups.quantity) : void 0;
2898
+ const value = quantityMatch.groups.quantity ? parseQuantityValue(quantityMatch.groups.quantity) : void 0;
2873
2899
  const unit = quantityMatch.groups.unit;
2874
2900
  if (value) {
2875
2901
  const newQuantity = { quantity: value };
@@ -3237,7 +3263,9 @@ var _Recipe = class _Recipe {
3237
3263
  } else {
3238
3264
  const targetSubgroupIndex = 0;
3239
3265
  const selectedSubgroup = groupSubgroups?.[targetSubgroupIndex];
3240
- isSelected = selectedSubgroup?.some((alt) => alt.itemId === item.id) ?? false;
3266
+ isSelected = selectedSubgroup.some(
3267
+ (alt) => alt.itemId === item.id
3268
+ );
3241
3269
  }
3242
3270
  } else {
3243
3271
  const targetSubgroupIndex = groupChoice ?? 0;
@@ -3636,16 +3664,10 @@ var _Recipe = class _Recipe {
3636
3664
  }
3637
3665
  sectionName = sectionName.slice(sectionVarMatch[0].length).trim();
3638
3666
  }
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);
3667
+ if (!section.isBlank()) {
3668
+ this.sections.push(section);
3648
3669
  }
3670
+ section = new Section(sectionName, sectionVariants, sectionOptional);
3649
3671
  blankLineBefore = true;
3650
3672
  inNote = false;
3651
3673
  continue;
@@ -3711,7 +3733,7 @@ var _Recipe = class _Recipe {
3711
3733
  if (modifiers !== void 0 && modifiers.includes("-")) {
3712
3734
  flags.push("hidden");
3713
3735
  }
3714
- const quantity = quantityRaw ? parseQuantityInput(quantityRaw) : void 0;
3736
+ const quantity = quantityRaw ? parseQuantityValue(quantityRaw) : void 0;
3715
3737
  const newCookware = {
3716
3738
  name
3717
3739
  };
@@ -3743,7 +3765,7 @@ var _Recipe = class _Recipe {
3743
3765
  throw new Error("Timer missing unit");
3744
3766
  }
3745
3767
  const name = groups.timerName || void 0;
3746
- const duration = parseQuantityInput(durationStr);
3768
+ const duration = parseQuantityValue(durationStr);
3747
3769
  const timerObj = {
3748
3770
  name,
3749
3771
  duration,
@@ -3876,9 +3898,15 @@ var _Recipe = class _Recipe {
3876
3898
  }
3877
3899
  newRecipe._populateIngredientQuantities();
3878
3900
  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];
3901
+ for (const metaVar of ["servings", "serves"]) {
3902
+ if (typeof newRecipe.metadata[metaVar] === "number") {
3903
+ newRecipe.metadata[metaVar] = Big4(newRecipe.metadata[metaVar]).times(factor).toNumber();
3904
+ }
3905
+ }
3906
+ if (newRecipe.metadata.yield && this.metadata.yield) {
3907
+ const original = this.metadata.yield;
3908
+ if (original.quantity.type === "fixed" && original.quantity.value.type === "text") {
3909
+ } else {
3882
3910
  const scaledQuantity = multiplyQuantityValue(
3883
3911
  original.quantity,
3884
3912
  factor
@@ -3893,8 +3921,7 @@ var _Recipe = class _Recipe {
3893
3921
  if (optimized.unit) scaled.unit = optimized.unit;
3894
3922
  if (original.textBefore) scaled.textBefore = original.textBefore;
3895
3923
  if (original.textAfter) scaled.textAfter = original.textAfter;
3896
- if (original.text) scaled.text = original.text;
3897
- newRecipe.metadata[metaVar] = scaled;
3924
+ newRecipe.metadata.yield = scaled;
3898
3925
  }
3899
3926
  }
3900
3927
  return newRecipe;
@@ -4111,7 +4138,7 @@ __publicField(_Recipe, "subgroupIndices", /* @__PURE__ */ new WeakMap());
4111
4138
  var Recipe = _Recipe;
4112
4139
 
4113
4140
  // src/classes/shopping_list.ts
4114
- var ShoppingList = class _ShoppingList {
4141
+ var ShoppingList = class {
4115
4142
  /**
4116
4143
  * Creates a new ShoppingList instance
4117
4144
  * @param categoryConfigStr - The category configuration to parse.
@@ -4206,7 +4233,7 @@ var ShoppingList = class _ShoppingList {
4206
4233
  }
4207
4234
  }
4208
4235
  if (numericEntries.length > 1) {
4209
- const ratioMap = _ShoppingList.buildEquivalenceRatioMap(
4236
+ const ratioMap = buildEquivalenceRatioMap(
4210
4237
  getEquivalentUnitsLists(...numericEntries)
4211
4238
  );
4212
4239
  if (Object.keys(ratioMap).length > 0) {
@@ -4263,10 +4290,12 @@ var ShoppingList = class _ShoppingList {
4263
4290
  const pantryHasUnit = pantryExtended.unit !== void 0 && pantryExtended.unit.name !== "";
4264
4291
  const ratioMap = this.equivalenceRatios.get(ingredient.name);
4265
4292
  const unitMismatch = leafHasUnit !== pantryHasUnit && ratioMap !== void 0;
4293
+ const leafDef = normalizeUnit(leaf.unit);
4294
+ const pantryDef = normalizeUnit(pantryExtended.unit?.name);
4266
4295
  if (unitMismatch) {
4267
4296
  const leafUnit = leaf.unit ?? NO_UNIT;
4268
4297
  const pantryUnit = pantryExtended.unit?.name ?? NO_UNIT;
4269
- const ratioFromPantry = ratioMap[leafUnit]?.[pantryUnit];
4298
+ const ratioFromPantry = ratioMap[normalizeUnit(leafUnit)?.name ?? leafUnit]?.[normalizeUnit(pantryUnit)?.name ?? pantryUnit];
4270
4299
  if (ratioFromPantry !== void 0) {
4271
4300
  const pantryValue = getAverageValue(pantryExtended.quantity);
4272
4301
  const leafValue = getAverageValue(ingredientExtended.quantity);
@@ -4281,7 +4310,7 @@ var ShoppingList = class _ShoppingList {
4281
4310
  type: "fixed",
4282
4311
  value: { type: "decimal", decimal: remainingLeafValue }
4283
4312
  };
4284
- const consumedInPantryUnits = ratioFromPantry !== 0 ? subtracted / ratioFromPantry : pantryValue;
4313
+ const consumedInPantryUnits = subtracted / ratioFromPantry;
4285
4314
  const remainingPantryValue = Math.max(
4286
4315
  pantryValue - consumedInPantryUnits,
4287
4316
  0
@@ -4298,9 +4327,10 @@ var ShoppingList = class _ShoppingList {
4298
4327
  };
4299
4328
  continue;
4300
4329
  }
4330
+ } else {
4331
+ continue;
4301
4332
  }
4302
- }
4303
- try {
4333
+ } else if (leafDef && pantryDef && areUnitsConvertible(leafDef, pantryDef) || (leaf.unit ?? "").toLowerCase() === (pantryExtended.unit?.name ?? "").toLowerCase()) {
4304
4334
  const remaining = subtractQuantities(
4305
4335
  ingredientExtended,
4306
4336
  pantryExtended,
@@ -4315,7 +4345,47 @@ var ShoppingList = class _ShoppingList {
4315
4345
  const updated = toPlainUnit(remaining);
4316
4346
  leaf.quantity = updated.quantity;
4317
4347
  leaf.unit = updated.unit;
4318
- } catch {
4348
+ } else if (ratioMap) {
4349
+ const canonicalLeaf = normalizeUnit(leaf.unit)?.name ?? leaf.unit;
4350
+ const leafValue = getAverageValue(ingredientExtended.quantity);
4351
+ const pantryValue = getAverageValue(pantryExtended.quantity);
4352
+ if (typeof leafValue === "number" && typeof pantryValue === "number" && pantryDef) {
4353
+ for (const [equivUnit, ratios] of Object.entries(ratioMap)) {
4354
+ const ratio = ratios[canonicalLeaf];
4355
+ if (ratio === void 0) continue;
4356
+ const equivDef = normalizeUnit(equivUnit);
4357
+ if (!equivDef || !areUnitsConvertible(equivDef, pantryDef))
4358
+ continue;
4359
+ const pantryInEquiv = pantryValue * getToBase(pantryDef) / getToBase(equivDef);
4360
+ const pantryInLeafUnits = pantryInEquiv / ratio;
4361
+ const subtracted = Math.min(pantryInLeafUnits, leafValue);
4362
+ const remainingLeafValue = Math.max(
4363
+ leafValue - pantryInLeafUnits,
4364
+ 0
4365
+ );
4366
+ leaf.quantity = {
4367
+ type: "fixed",
4368
+ value: { type: "decimal", decimal: remainingLeafValue }
4369
+ };
4370
+ const consumedInEquiv = subtracted * ratio;
4371
+ const consumedInPantryUnits = consumedInEquiv * getToBase(equivDef) / getToBase(pantryDef);
4372
+ const remainingPantryValue = Math.max(
4373
+ pantryValue - consumedInPantryUnits,
4374
+ 0
4375
+ );
4376
+ pantryExtended = {
4377
+ quantity: {
4378
+ type: "fixed",
4379
+ value: {
4380
+ type: "decimal",
4381
+ decimal: remainingPantryValue
4382
+ }
4383
+ },
4384
+ ...pantryExtended.unit && { unit: pantryExtended.unit }
4385
+ };
4386
+ break;
4387
+ }
4388
+ }
4319
4389
  }
4320
4390
  }
4321
4391
  if ("and" in entry) {
@@ -4326,8 +4396,8 @@ var ShoppingList = class _ShoppingList {
4326
4396
  entry.and.push(...nonZero);
4327
4397
  const ratioMap = this.equivalenceRatios.get(ingredient.name);
4328
4398
  if (entry.equivalents && ratioMap) {
4329
- const equivUnits = entry.equivalents.map((e2) => e2.unit ?? NO_UNIT);
4330
- entry.equivalents = _ShoppingList.recomputeEquivalents(
4399
+ const equivUnits = entry.equivalents.map((e2) => e2.unit);
4400
+ entry.equivalents = recomputeEquivalents(
4331
4401
  entry.and,
4332
4402
  ratioMap,
4333
4403
  equivUnits
@@ -4338,17 +4408,17 @@ var ShoppingList = class _ShoppingList {
4338
4408
  ingredient.quantities[i2] = {
4339
4409
  quantity: single.quantity,
4340
4410
  ...single.unit && { unit: single.unit },
4341
- ...entry.equivalents && { equivalents: entry.equivalents },
4342
- ...entry.alternatives && { alternatives: entry.alternatives }
4411
+ ...entry.equivalents && { equivalents: entry.equivalents }
4343
4412
  };
4344
4413
  }
4345
4414
  } else if ("equivalents" in entry && entry.equivalents) {
4346
4415
  const ratioMap = this.equivalenceRatios.get(ingredient.name);
4347
4416
  if (ratioMap) {
4348
4417
  const equivUnits = entry.equivalents.map(
4349
- (e2) => e2.unit ?? NO_UNIT
4418
+ (e2) => e2.unit
4419
+ // equivalents always have units
4350
4420
  );
4351
- const recomputed = _ShoppingList.recomputeEquivalents(
4421
+ const recomputed = recomputeEquivalents(
4352
4422
  [entry],
4353
4423
  ratioMap,
4354
4424
  equivUnits
@@ -4371,57 +4441,6 @@ var ShoppingList = class _ShoppingList {
4371
4441
  }
4372
4442
  this.resultingPantry = clonedPantry;
4373
4443
  }
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
4444
  /**
4426
4445
  * Adds a recipe to the shopping list, then automatically
4427
4446
  * recalculates the quantities and recategorize the ingredients.
@@ -5035,6 +5054,9 @@ export {
5035
5054
  // v8 ignore if -- @preserve: defensive type guard
5036
5055
  /* v8 ignore if -- @preserve */
5037
5056
  // v8 ignore next -- @preserve
5057
+ // v8 ignore else -- @preserve: text quantities never reach the equivalence path
5038
5058
  // v8 ignore else --@preserve: defensive type guard
5039
5059
  // v8 ignore else -- @preserve: detection if
5060
+ /* v8 ignore else -- @preserve: only act when there are matches */
5061
+ /* v8 ignore else -- @preserve: initialization pattern */
5040
5062
  //# sourceMappingURL=index.js.map