@tmlmt/cooklang-parser 3.0.0-alpha.15 → 3.0.0-alpha.16

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
@@ -330,7 +330,6 @@ var nestedMetaVarRegex = (varName) => new RegExp(
330
330
  "m"
331
331
  );
332
332
  var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
333
- var scalingMetaValueRegex = (varName) => d().startAnchor().literal(varName).literal(":").anyOf("\\t ").zeroOrMore().startCaptureGroup().startCaptureGroup().notAnyOf(",\\n").oneOrMore().endGroup().startGroup().literal(",").whitespace().zeroOrMore().startCaptureGroup().anyCharacter().oneOrMore().endGroup().endGroup().optional().endGroup().endAnchor().multiline().toRegExp();
334
333
  var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
335
334
  var nonWordCharStrict = "\\s@#~\\[\\]{(,;:!?|";
336
335
  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();
@@ -351,6 +350,13 @@ var tokensRegex = new RegExp(
351
350
  ].map((r2) => r2.source).join("|"),
352
351
  "gu"
353
352
  );
353
+ 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();
354
+ var servingsSuffixPart = d().anyOf(" ").zeroOrMore().startNamedGroup("servingsSuffix").nonWhitespace().startGroup().anyCharacter().zeroOrMore().nonWhitespace().endGroup().optional().endGroup().optional().anyOf(" ").zeroOrMore().endAnchor().toRegExp();
355
+ 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();
356
+ var scalingMetaValueWithUnitRegex = (varName) => new RegExp(
357
+ servingsPrefixPart(varName).source + arbitraryScalableRegex.source + servingsSuffixPart.source,
358
+ "m"
359
+ );
354
360
  var commentRegex = d().literal("--").anyCharacter().zeroOrMore().global().toRegExp();
355
361
  var blockCommentRegex = d().literal("[-").anyCharacter().zeroOrMore().lazy().literal("-]").whitespace().zeroOrMore().global().toRegExp();
356
362
  var shoppingListRegex = d().literal("[").startNamedGroup("name").anyCharacter().oneOrMore().endGroup().literal("]").newline().startNamedGroup("items").anyCharacter().zeroOrMore().lazy().endGroup().startGroup().newline().newline().or().endAnchor().endGroup().global().toRegExp();
@@ -1334,17 +1340,21 @@ var flattenPlainUnitGroup = (summed) => {
1334
1340
  }
1335
1341
  };
1336
1342
  function applyBestUnit(q, system) {
1337
- if (!q.unit?.name) {
1343
+ const extended = { quantity: q.quantity };
1344
+ if (q.unit) {
1345
+ extended.unit = typeof q.unit === "string" ? { name: q.unit } : q.unit;
1346
+ }
1347
+ if (!extended.unit?.name) {
1338
1348
  return q;
1339
1349
  }
1340
- const unitDef = resolveUnit(q.unit.name);
1350
+ const unitDef = resolveUnit(extended.unit.name);
1341
1351
  if (unitDef.type === "other") {
1342
1352
  return q;
1343
1353
  }
1344
- if (q.quantity.type === "fixed" && q.quantity.value.type === "text") {
1354
+ if (extended.quantity.type === "fixed" && extended.quantity.value.type === "text") {
1345
1355
  return q;
1346
1356
  }
1347
- const avgValue = getAverageValue(q.quantity);
1357
+ const avgValue = getAverageValue(extended.quantity);
1348
1358
  const effectiveSystem = system ?? (["metric", "JP"].includes(unitDef.system) ? unitDef.system : "US");
1349
1359
  const toBase = getToBase(unitDef, effectiveSystem);
1350
1360
  const valueInBase = avgValue * toBase;
@@ -1354,22 +1364,22 @@ function applyBestUnit(q, system) {
1354
1364
  effectiveSystem,
1355
1365
  [unitDef]
1356
1366
  );
1357
- const originalCanonicalName = normalizeUnit(q.unit.name)?.name;
1367
+ const originalCanonicalName = normalizeUnit(extended.unit.name)?.name;
1358
1368
  if (bestUnit.name === originalCanonicalName) {
1359
1369
  return q;
1360
1370
  }
1361
1371
  const formattedValue = formatOutputValue(bestValue, bestUnit);
1362
- if (q.quantity.type === "range") {
1372
+ if (extended.quantity.type === "range") {
1363
1373
  const bestToBase = getToBase(bestUnit, effectiveSystem);
1364
- const minValue = getNumericValue(q.quantity.min) * toBase / bestToBase;
1365
- const maxValue = getNumericValue(q.quantity.max) * toBase / bestToBase;
1374
+ const minValue = getNumericValue(extended.quantity.min) * toBase / bestToBase;
1375
+ const maxValue = getNumericValue(extended.quantity.max) * toBase / bestToBase;
1366
1376
  return {
1367
1377
  quantity: {
1368
1378
  type: "range",
1369
1379
  min: formatOutputValue(minValue, bestUnit),
1370
1380
  max: formatOutputValue(maxValue, bestUnit)
1371
1381
  },
1372
- unit: { name: bestUnit.name }
1382
+ unit: typeof q.unit === "string" ? bestUnit.name : { name: bestUnit.name }
1373
1383
  };
1374
1384
  }
1375
1385
  return {
@@ -1377,7 +1387,7 @@ function applyBestUnit(q, system) {
1377
1387
  type: "fixed",
1378
1388
  value: formattedValue
1379
1389
  },
1380
- unit: { name: bestUnit.name }
1390
+ unit: typeof q.unit === "string" ? bestUnit.name : { name: bestUnit.name }
1381
1391
  };
1382
1392
  }
1383
1393
  function subtractQuantities(q1, q2, options = {}) {
@@ -1729,13 +1739,62 @@ function parseBlockScalarMetaVar(content, varName) {
1729
1739
  }
1730
1740
  return stripped.replace(/\n\n/g, "\0").replace(/\n/g, " ").replace(/\0/g, "\n");
1731
1741
  }
1742
+ function parseArbitraryQuantity(raw) {
1743
+ const quantityMatch = raw.trim().match(quantityAlternativeRegex);
1744
+ if (!quantityMatch?.groups) {
1745
+ throw new InvalidQuantityFormat(
1746
+ raw,
1747
+ "Arbitrary quantities must have a numerical value"
1748
+ );
1749
+ }
1750
+ const value = parseQuantityInput(quantityMatch.groups.quantity);
1751
+ const unit = quantityMatch.groups.unit;
1752
+ if (!value || value.type === "fixed" && value.value.type === "text") {
1753
+ throw new InvalidQuantityFormat(
1754
+ raw,
1755
+ "Arbitrary quantities must have a numerical value"
1756
+ );
1757
+ }
1758
+ const arbitrary = {
1759
+ quantity: value
1760
+ };
1761
+ if (unit) arbitrary.unit = unit;
1762
+ return arbitrary;
1763
+ }
1732
1764
  function parseScalingMetaVar(content, varName) {
1733
- const varMatch = content.match(scalingMetaValueRegex(varName));
1765
+ const complexMatch = content.match(scalingMetaValueWithUnitRegex(varName));
1766
+ if (complexMatch?.groups?.arbitraryQuantity) {
1767
+ const parsed = parseArbitraryQuantity(
1768
+ complexMatch.groups.arbitraryQuantity
1769
+ );
1770
+ const result2 = {
1771
+ quantity: parsed.quantity
1772
+ };
1773
+ if (parsed.unit) result2.unit = parsed.unit;
1774
+ if (complexMatch.groups.servingsPrefix) {
1775
+ result2.textBefore = complexMatch.groups.servingsPrefix;
1776
+ }
1777
+ if (complexMatch.groups.servingsSuffix) {
1778
+ result2.textAfter = complexMatch.groups.servingsSuffix;
1779
+ }
1780
+ return result2;
1781
+ }
1782
+ const varMatch = content.match(scalingSimpleMetaValueRegex(varName));
1734
1783
  if (!varMatch) return void 0;
1735
1784
  if (isNaN(Number(varMatch[2]?.trim()))) {
1736
1785
  throw new Error("Scaling variables should be numbers");
1737
1786
  }
1738
- return [Number(varMatch[2]?.trim()), varMatch[1].trim()];
1787
+ const numericValue = Number(varMatch[2]?.trim());
1788
+ const result = {
1789
+ quantity: {
1790
+ type: "fixed",
1791
+ value: { type: "decimal", decimal: numericValue }
1792
+ }
1793
+ };
1794
+ if (varMatch[3]) {
1795
+ result.text = `${varMatch[3].trim()}`;
1796
+ }
1797
+ return result;
1739
1798
  }
1740
1799
  function parseListMetaVar(content, varName) {
1741
1800
  const listMatch = content.match(
@@ -1853,6 +1912,13 @@ function parseAnyMetaVar(content, varName) {
1853
1912
  if (simple) return parseMetadataValue(simple);
1854
1913
  return void 0;
1855
1914
  }
1915
+ function getNumericValueFromMetaVar(v) {
1916
+ if (v.quantity.type === "fixed" && v.quantity.value.type !== "text") {
1917
+ return getNumericValue(v.quantity.value);
1918
+ }
1919
+ if (v.quantity.type === "range") return getNumericValue(v.quantity.min);
1920
+ return 0;
1921
+ }
1856
1922
  function extractMetadata(content) {
1857
1923
  const metadata = {};
1858
1924
  let servings = void 0;
@@ -1977,9 +2043,9 @@ function extractMetadata(content) {
1977
2043
  }
1978
2044
  for (const metaVar of ["servings", "yield", "serves"]) {
1979
2045
  const scalingMetaValue = parseScalingMetaVar(metadataContent, metaVar);
1980
- if (scalingMetaValue && scalingMetaValue[1]) {
1981
- metadata[metaVar] = scalingMetaValue[1];
1982
- servings = scalingMetaValue[0];
2046
+ if (scalingMetaValue) {
2047
+ metadata[metaVar] = scalingMetaValue;
2048
+ servings = getNumericValueFromMetaVar(scalingMetaValue);
1983
2049
  }
1984
2050
  }
1985
2051
  const tags = parseListMetaVar(metadataContent, "tags");
@@ -2806,27 +2872,17 @@ var _Recipe = class _Recipe {
2806
2872
  */
2807
2873
  _parseArbitraryScalable(regexMatchGroups, intoArray) {
2808
2874
  if (!regexMatchGroups || !regexMatchGroups.arbitraryQuantity) return;
2809
- const quantityMatch = regexMatchGroups.arbitraryQuantity?.trim().match(quantityAlternativeRegex);
2810
- if (quantityMatch?.groups) {
2811
- const value = parseQuantityInput(quantityMatch.groups.quantity);
2812
- const unit = quantityMatch.groups.unit;
2813
- const name = regexMatchGroups.arbitraryName || void 0;
2814
- if (!value || value.type === "fixed" && value.value.type === "text") {
2815
- throw new InvalidQuantityFormat(
2816
- regexMatchGroups.arbitraryQuantity?.trim(),
2817
- "Arbitrary quantities must have a numerical value"
2818
- );
2819
- }
2820
- const arbitrary = {
2821
- quantity: value
2822
- };
2823
- if (name) arbitrary.name = name;
2824
- if (unit) arbitrary.unit = unit;
2825
- intoArray.push({
2826
- type: "arbitrary",
2827
- index: this.arbitraries.push(arbitrary) - 1
2828
- });
2829
- }
2875
+ const parsed = parseArbitraryQuantity(regexMatchGroups.arbitraryQuantity);
2876
+ const name = regexMatchGroups.arbitraryName || void 0;
2877
+ const arbitrary = {
2878
+ quantity: parsed.quantity
2879
+ };
2880
+ if (name) arbitrary.name = name;
2881
+ if (parsed.unit) arbitrary.unit = parsed.unit;
2882
+ intoArray.push({
2883
+ type: "arbitrary",
2884
+ index: this.arbitraries.push(arbitrary) - 1
2885
+ });
2830
2886
  }
2831
2887
  /**
2832
2888
  * Parses text for arbitrary scalables and returns NoteItem array.
@@ -3155,8 +3211,8 @@ var _Recipe = class _Recipe {
3155
3211
  const isGrouped = "group" in item && item.group !== void 0;
3156
3212
  const groupAlternatives = isGrouped ? this.choices.ingredientGroups.get(item.group) : void 0;
3157
3213
  let selectedAltIndex = 0;
3158
- let isSelected = false;
3159
- let hasExplicitChoice = false;
3214
+ let isSelected;
3215
+ let hasExplicitChoice;
3160
3216
  if (isGrouped) {
3161
3217
  const groupChoice = choices?.ingredientGroups?.get(item.group);
3162
3218
  hasExplicitChoice = groupChoice !== void 0;
@@ -3616,37 +3672,34 @@ var _Recipe = class _Recipe {
3616
3672
  arbitrary.quantity,
3617
3673
  factor
3618
3674
  );
3675
+ const optimized = applyBestUnit(
3676
+ { quantity: arbitrary.quantity, unit: arbitrary.unit },
3677
+ unitSystem
3678
+ );
3679
+ arbitrary.quantity = optimized.quantity;
3680
+ arbitrary.unit = optimized.unit;
3619
3681
  }
3620
3682
  newRecipe._populateIngredientQuantities();
3621
3683
  newRecipe.servings = (0, import_big4.default)(originalServings).times(factor).toNumber();
3622
- if (newRecipe.metadata.servings && this.metadata.servings) {
3623
- if (floatRegex.test(String(this.metadata.servings).replace(",", ".").trim())) {
3624
- const servingsValue = parseFloat(
3625
- String(this.metadata.servings).replace(",", ".")
3626
- );
3627
- newRecipe.metadata.servings = String(
3628
- (0, import_big4.default)(servingsValue).times(factor).toNumber()
3629
- );
3630
- }
3631
- }
3632
- if (newRecipe.metadata.yield && this.metadata.yield) {
3633
- if (floatRegex.test(String(this.metadata.yield).replace(",", ".").trim())) {
3634
- const yieldValue = parseFloat(
3635
- String(this.metadata.yield).replace(",", ".")
3684
+ for (const metaVar of ["servings", "yield", "serves"]) {
3685
+ if (newRecipe.metadata[metaVar] && this.metadata[metaVar]) {
3686
+ const original = this.metadata[metaVar];
3687
+ const scaledQuantity = multiplyQuantityValue(
3688
+ original.quantity,
3689
+ factor
3636
3690
  );
3637
- newRecipe.metadata.yield = String(
3638
- (0, import_big4.default)(yieldValue).times(factor).toNumber()
3639
- );
3640
- }
3641
- }
3642
- if (newRecipe.metadata.serves && this.metadata.serves) {
3643
- if (floatRegex.test(String(this.metadata.serves).replace(",", ".").trim())) {
3644
- const servesValue = parseFloat(
3645
- String(this.metadata.serves).replace(",", ".")
3646
- );
3647
- newRecipe.metadata.serves = String(
3648
- (0, import_big4.default)(servesValue).times(factor).toNumber()
3691
+ const optimized = applyBestUnit(
3692
+ { quantity: scaledQuantity, unit: original.unit },
3693
+ unitSystem
3649
3694
  );
3695
+ const scaled = {
3696
+ quantity: optimized.quantity
3697
+ };
3698
+ if (optimized.unit) scaled.unit = optimized.unit;
3699
+ if (original.textBefore) scaled.textBefore = original.textBefore;
3700
+ if (original.textAfter) scaled.textAfter = original.textAfter;
3701
+ if (original.text) scaled.text = original.text;
3702
+ newRecipe.metadata[metaVar] = scaled;
3650
3703
  }
3651
3704
  }
3652
3705
  return newRecipe;
@@ -4730,6 +4783,7 @@ function isAlternativeSelected(recipe, choices, item, alternativeIndex) {
4730
4783
  // v8 ignore else -- @preserve
4731
4784
  // v8 ignore if -- @preserve
4732
4785
  /* v8 ignore else -- expliciting error type -- @preserve */
4786
+ /* v8 ignore next 4 -- @preserve: defensive guard; regex always matches */
4733
4787
  // v8 ignore if -- @preserve: defensive type guard
4734
4788
  /* v8 ignore if -- @preserve */
4735
4789
  // v8 ignore next -- @preserve