@tmlmt/cooklang-parser 3.0.0-alpha.4 → 3.0.0-alpha.7

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
@@ -312,17 +312,19 @@ var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
312
312
  var nonWordCharStrict = "\\s@#~\\[\\]{(,;:!?|";
313
313
  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();
314
314
  var inlineIngredientAlternativesRegex = new RegExp("\\|" + ingredientWithAlternativeRegex.source.slice(1));
315
- var quantityAlternativeRegex = d().startNamedGroup("ingredientQuantityValue").notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").startNamedGroup("ingredientUnit").notAnyOf("|}").oneOrMore().endGroup().endGroup().optional().startGroup().literal("|").startNamedGroup("ingredientAltQuantity").startGroup().notAnyOf("}").oneOrMore().endGroup().zeroOrMore().endGroup().endGroup().optional().toRegExp();
315
+ 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();
316
316
  var ingredientWithGroupKeyRegex = d().literal("@|").startNamedGroup("gIngredientGroupKey").notAnyOf(nonWordCharStrict).oneOrMore().endGroup().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().toRegExp();
317
317
  var ingredientAliasRegex = d().startAnchor().startNamedGroup("ingredientListName").notAnyOf("|").oneOrMore().endGroup().literal("|").startNamedGroup("ingredientDisplayName").notAnyOf("|").oneOrMore().endGroup().endAnchor().toRegExp();
318
318
  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();
319
319
  var timerRegex = d().literal("~").startNamedGroup("timerName").anyCharacter().zeroOrMore().lazy().endGroup().literal("{").startNamedGroup("timerQuantity").anyCharacter().oneOrMore().lazy().endGroup().startGroup().literal("%").startNamedGroup("timerUnit").anyCharacter().oneOrMore().lazy().endGroup().endGroup().optional().literal("}").toRegExp();
320
+ var arbitraryScalableRegex = d().literal("{{").startGroup().startNamedGroup("arbitraryName").notAnyOf("}:%").oneOrMore().endGroup().literal(":").endGroup().optional().startNamedGroup("arbitraryQuantity").startGroup().notAnyOf("}|%").oneOrMore().endGroup().optional().startGroup().literal("%").notAnyOf("|}").oneOrMore().lazy().endGroup().optional().startGroup().literal("|").notAnyOf("}").oneOrMore().lazy().endGroup().zeroOrMore().endGroup().literal("}}").toRegExp();
320
321
  var tokensRegex = new RegExp(
321
322
  [
322
323
  ingredientWithGroupKeyRegex,
323
324
  ingredientWithAlternativeRegex,
324
325
  cookwareRegex,
325
- timerRegex
326
+ timerRegex,
327
+ arbitraryScalableRegex
326
328
  ].map((r2) => r2.source).join("|"),
327
329
  "gu"
328
330
  );
@@ -554,8 +556,8 @@ function multiplyQuantityValue(value, factor) {
554
556
  value.value,
555
557
  (0, import_big.default)(factor)
556
558
  );
557
- if (factor === parseInt(factor.toString()) || // e.g. 2 === int
558
- (0, import_big.default)(1).div(factor).toNumber() === parseInt((0, import_big.default)(1).div(factor).toString())) {
559
+ if (newValue.type === "fraction" && ((0, import_big.default)(factor).toNumber() === parseInt((0, import_big.default)(factor).toString()) || // e.g. 2 === int
560
+ (0, import_big.default)(1).div(factor).toNumber() === parseInt((0, import_big.default)(1).div(factor).toString()))) {
559
561
  return {
560
562
  type: "fixed",
561
563
  value: newValue
@@ -642,8 +644,10 @@ var IncompatibleUnitsError = class extends Error {
642
644
  }
643
645
  };
644
646
  var InvalidQuantityFormat = class extends Error {
645
- constructor(value) {
646
- super(`Invalid quantity format found in: ${value}`);
647
+ constructor(value, extra) {
648
+ super(
649
+ `Invalid quantity format found in: ${value}${extra ? ` (${extra})` : ""}`
650
+ );
647
651
  this.name = "InvalidQuantityFormat";
648
652
  }
649
653
  };
@@ -833,7 +837,7 @@ var flattenPlainUnitGroup = (summed) => {
833
837
  }
834
838
  ];
835
839
  } else {
836
- return andEntries.map((entry) => ({ groupQuantity: entry }));
840
+ return andEntries;
837
841
  }
838
842
  }
839
843
  const simpleEntries = entries.filter(
@@ -847,12 +851,10 @@ var flattenPlainUnitGroup = (summed) => {
847
851
  if (simpleEntries.length > 1) {
848
852
  result.equivalents = simpleEntries.slice(1);
849
853
  }
850
- return [{ groupQuantity: result }];
854
+ return [result];
851
855
  } else {
852
856
  const first = entries[0];
853
- return [
854
- { groupQuantity: { quantity: first.quantity, unit: first.unit } }
855
- ];
857
+ return [{ quantity: first.quantity, unit: first.unit }];
856
858
  }
857
859
  } else if (isGroup(summed)) {
858
860
  const andEntries = [];
@@ -877,7 +879,7 @@ var flattenPlainUnitGroup = (summed) => {
877
879
  }
878
880
  }
879
881
  if (equivalentsList.length === 0) {
880
- return andEntries.map((entry) => ({ groupQuantity: entry }));
882
+ return andEntries;
881
883
  }
882
884
  const result = {
883
885
  type: "and",
@@ -886,19 +888,17 @@ var flattenPlainUnitGroup = (summed) => {
886
888
  };
887
889
  return [result];
888
890
  } else {
889
- return [
890
- { groupQuantity: { quantity: summed.quantity, unit: summed.unit } }
891
- ];
891
+ return [{ quantity: summed.quantity, unit: summed.unit }];
892
892
  }
893
893
  };
894
894
 
895
895
  // src/utils/parser_helpers.ts
896
- function flushPendingNote(section, note) {
897
- if (note.length > 0) {
898
- section.content.push({ type: "note", note });
899
- return "";
896
+ function flushPendingNote(section, noteItems) {
897
+ if (noteItems.length > 0) {
898
+ section.content.push({ type: "note", items: [...noteItems] });
899
+ return [];
900
900
  }
901
- return note;
901
+ return noteItems;
902
902
  }
903
903
  function flushPendingItems(section, items) {
904
904
  if (items.length > 0) {
@@ -1671,6 +1671,10 @@ var _Recipe = class _Recipe {
1671
1671
  * The parsed recipe timers.
1672
1672
  */
1673
1673
  __publicField(this, "timers", []);
1674
+ /**
1675
+ * The parsed arbitrary quantities.
1676
+ */
1677
+ __publicField(this, "arbitraries", []);
1674
1678
  /**
1675
1679
  * The parsed recipe servings. Used for scaling. Parsed from one of
1676
1680
  * {@link Metadata.servings}, {@link Metadata.yield} or {@link Metadata.serves}
@@ -1698,12 +1702,64 @@ var _Recipe = class _Recipe {
1698
1702
  _Recipe.itemCounts.set(this, current + 1);
1699
1703
  return current;
1700
1704
  }
1705
+ /**
1706
+ * Parses a matched arbitrary scalable quantity and adds it to the given array.
1707
+ * @private
1708
+ * @param regexMatchGroups - The regex match groups from arbitrary scalable regex.
1709
+ * @param intoArray - The array to push the parsed arbitrary scalable item into.
1710
+ */
1711
+ _parseArbitraryScalable(regexMatchGroups, intoArray) {
1712
+ if (!regexMatchGroups || !regexMatchGroups.arbitraryQuantity) return;
1713
+ const quantityMatch = regexMatchGroups.arbitraryQuantity?.trim().match(quantityAlternativeRegex);
1714
+ if (quantityMatch?.groups) {
1715
+ const value = quantityMatch.groups.quantity ? parseQuantityInput(quantityMatch.groups.quantity) : void 0;
1716
+ const unit = quantityMatch.groups.unit;
1717
+ const name = regexMatchGroups.arbitraryName || void 0;
1718
+ if (!value || value.type === "fixed" && value.value.type === "text") {
1719
+ throw new InvalidQuantityFormat(
1720
+ regexMatchGroups.arbitraryQuantity?.trim(),
1721
+ "Arbitrary quantities must have a numerical value"
1722
+ );
1723
+ }
1724
+ const arbitrary = {
1725
+ quantity: value
1726
+ };
1727
+ if (name) arbitrary.name = name;
1728
+ if (unit) arbitrary.unit = unit;
1729
+ intoArray.push({
1730
+ type: "arbitrary",
1731
+ index: this.arbitraries.push(arbitrary) - 1
1732
+ });
1733
+ }
1734
+ }
1735
+ /**
1736
+ * Parses text for arbitrary scalables and returns NoteItem array.
1737
+ * @param text - The text to parse for arbitrary scalables.
1738
+ * @returns Array of NoteItem (text and arbitrary scalable items).
1739
+ */
1740
+ _parseNoteText(text) {
1741
+ const noteItems = [];
1742
+ let cursor = 0;
1743
+ const globalRegex = new RegExp(arbitraryScalableRegex.source, "g");
1744
+ for (const match of text.matchAll(globalRegex)) {
1745
+ const idx = match.index;
1746
+ if (idx > cursor) {
1747
+ noteItems.push({ type: "text", value: text.slice(cursor, idx) });
1748
+ }
1749
+ this._parseArbitraryScalable(match.groups, noteItems);
1750
+ cursor = idx + match[0].length;
1751
+ }
1752
+ if (cursor < text.length) {
1753
+ noteItems.push({ type: "text", value: text.slice(cursor) });
1754
+ }
1755
+ return noteItems;
1756
+ }
1701
1757
  _parseQuantityRecursive(quantityRaw) {
1702
1758
  let quantityMatch = quantityRaw.match(quantityAlternativeRegex);
1703
1759
  const quantities = [];
1704
1760
  while (quantityMatch?.groups) {
1705
- const value = quantityMatch.groups.ingredientQuantityValue ? parseQuantityInput(quantityMatch.groups.ingredientQuantityValue) : void 0;
1706
- const unit = quantityMatch.groups.ingredientUnit;
1761
+ const value = quantityMatch.groups.quantity ? parseQuantityInput(quantityMatch.groups.quantity) : void 0;
1762
+ const unit = quantityMatch.groups.unit;
1707
1763
  if (value) {
1708
1764
  const newQuantity = { quantity: value };
1709
1765
  if (unit) {
@@ -1720,9 +1776,7 @@ var _Recipe = class _Recipe {
1720
1776
  } else {
1721
1777
  throw new InvalidQuantityFormat(quantityRaw);
1722
1778
  }
1723
- quantityMatch = quantityMatch.groups.ingredientAltQuantity ? quantityMatch.groups.ingredientAltQuantity.match(
1724
- quantityAlternativeRegex
1725
- ) : null;
1779
+ quantityMatch = quantityMatch.groups.alternative ? quantityMatch.groups.alternative.match(quantityAlternativeRegex) : null;
1726
1780
  }
1727
1781
  return quantities;
1728
1782
  }
@@ -2025,7 +2079,7 @@ var _Recipe = class _Recipe {
2025
2079
  (eq) => toPlainUnit(eq)
2026
2080
  );
2027
2081
  }
2028
- newRef.alternativeQuantities = [altQty];
2082
+ newRef.quantities = [altQty];
2029
2083
  }
2030
2084
  alternativeRefs.push(newRef);
2031
2085
  }
@@ -2050,7 +2104,7 @@ var _Recipe = class _Recipe {
2050
2104
  }
2051
2105
  alternativeRefs.push({
2052
2106
  index: otherAlt.index,
2053
- alternativeQuantities: [altQty]
2107
+ quantities: [altQty]
2054
2108
  });
2055
2109
  }
2056
2110
  }
@@ -2077,8 +2131,8 @@ var _Recipe = class _Recipe {
2077
2131
  if (!group.alternativeQuantities.has(ref.index)) {
2078
2132
  group.alternativeQuantities.set(ref.index, []);
2079
2133
  }
2080
- if (ref.alternativeQuantities && ref.alternativeQuantities.length > 0) {
2081
- for (const altQty of ref.alternativeQuantities) {
2134
+ if (ref.quantities && ref.quantities.length > 0) {
2135
+ for (const altQty of ref.quantities) {
2082
2136
  if (altQty.equivalents && altQty.equivalents.length > 0) {
2083
2137
  const entries = [
2084
2138
  toExtendedUnit({
@@ -2121,9 +2175,9 @@ var _Recipe = class _Recipe {
2121
2175
  ...altQuantities
2122
2176
  );
2123
2177
  const flattenedAlt = flattenPlainUnitGroup(summedAltQuantity);
2124
- ref.alternativeQuantities = flattenedAlt.flatMap((item) => {
2125
- if ("groupQuantity" in item) {
2126
- return [item.groupQuantity];
2178
+ ref.quantities = flattenedAlt.flatMap((item) => {
2179
+ if ("quantity" in item) {
2180
+ return [item];
2127
2181
  } else {
2128
2182
  return item.entries;
2129
2183
  }
@@ -2254,19 +2308,27 @@ var _Recipe = class _Recipe {
2254
2308
  let blankLineBefore = true;
2255
2309
  let section = new Section();
2256
2310
  const items = [];
2257
- let note = "";
2311
+ let noteText = "";
2258
2312
  let inNote = false;
2259
2313
  for (const line of cleanContent) {
2260
2314
  if (line.trim().length === 0) {
2261
2315
  flushPendingItems(section, items);
2262
- note = flushPendingNote(section, note);
2316
+ flushPendingNote(
2317
+ section,
2318
+ noteText ? this._parseNoteText(noteText) : []
2319
+ );
2320
+ noteText = "";
2263
2321
  blankLineBefore = true;
2264
2322
  inNote = false;
2265
2323
  continue;
2266
2324
  }
2267
2325
  if (line.startsWith("=")) {
2268
2326
  flushPendingItems(section, items);
2269
- note = flushPendingNote(section, note);
2327
+ flushPendingNote(
2328
+ section,
2329
+ noteText ? this._parseNoteText(noteText) : []
2330
+ );
2331
+ noteText = "";
2270
2332
  if (this.sections.length === 0 && section.isBlank()) {
2271
2333
  section.name = line.replace(/^=+|=+$/g, "").trim();
2272
2334
  } else {
@@ -2281,22 +2343,26 @@ var _Recipe = class _Recipe {
2281
2343
  }
2282
2344
  if (blankLineBefore && line.startsWith(">")) {
2283
2345
  flushPendingItems(section, items);
2284
- note = flushPendingNote(section, note);
2285
- note += line.substring(1).trim();
2346
+ flushPendingNote(
2347
+ section,
2348
+ noteText ? this._parseNoteText(noteText) : []
2349
+ );
2350
+ noteText = line.substring(1).trim();
2286
2351
  inNote = true;
2287
2352
  blankLineBefore = false;
2288
2353
  continue;
2289
2354
  }
2290
2355
  if (inNote) {
2291
2356
  if (line.startsWith(">")) {
2292
- note += " " + line.substring(1).trim();
2357
+ noteText += " " + line.substring(1).trim();
2293
2358
  } else {
2294
- note += " " + line.trim();
2359
+ noteText += " " + line.trim();
2295
2360
  }
2296
2361
  blankLineBefore = false;
2297
2362
  continue;
2298
2363
  }
2299
- note = flushPendingNote(section, note);
2364
+ flushPendingNote(section, noteText ? this._parseNoteText(noteText) : []);
2365
+ noteText = "";
2300
2366
  let cursor = 0;
2301
2367
  for (const match of line.matchAll(tokensRegex)) {
2302
2368
  const idx = match.index;
@@ -2343,6 +2409,8 @@ var _Recipe = class _Recipe {
2343
2409
  newItem.quantity = quantity;
2344
2410
  }
2345
2411
  items.push(newItem);
2412
+ } else if (groups.arbitraryQuantity) {
2413
+ this._parseArbitraryScalable(groups, items);
2346
2414
  } else {
2347
2415
  const durationStr = groups.timerQuantity.trim();
2348
2416
  const unit = (groups.timerUnit || "").trim();
@@ -2366,7 +2434,7 @@ var _Recipe = class _Recipe {
2366
2434
  blankLineBefore = false;
2367
2435
  }
2368
2436
  flushPendingItems(section, items);
2369
- note = flushPendingNote(section, note);
2437
+ flushPendingNote(section, noteText ? this._parseNoteText(noteText) : []);
2370
2438
  if (!section.isBlank()) {
2371
2439
  this.sections.push(section);
2372
2440
  }
@@ -2381,9 +2449,9 @@ var _Recipe = class _Recipe {
2381
2449
  * @throws `Error` if the recipe does not contains an initial {@link Recipe.servings | servings} value
2382
2450
  */
2383
2451
  scaleTo(newServings) {
2384
- const originalServings = this.getServings();
2452
+ let originalServings = this.getServings();
2385
2453
  if (originalServings === void 0 || originalServings === 0) {
2386
- throw new Error("Error scaling recipe: no initial servings value set");
2454
+ originalServings = 1;
2387
2455
  }
2388
2456
  const factor = (0, import_big4.default)(newServings).div(originalServings);
2389
2457
  return this.scaleBy(factor);
@@ -2396,9 +2464,9 @@ var _Recipe = class _Recipe {
2396
2464
  */
2397
2465
  scaleBy(factor) {
2398
2466
  const newRecipe = this.clone();
2399
- const originalServings = newRecipe.getServings();
2467
+ let originalServings = newRecipe.getServings();
2400
2468
  if (originalServings === void 0 || originalServings === 0) {
2401
- throw new Error("Error scaling recipe: no initial servings value set");
2469
+ originalServings = 1;
2402
2470
  }
2403
2471
  function scaleAlternativesBy(alternatives, factor2) {
2404
2472
  for (const alternative of alternatives) {
@@ -2447,6 +2515,12 @@ var _Recipe = class _Recipe {
2447
2515
  for (const alternatives of newRecipe.choices.ingredientItems.values()) {
2448
2516
  scaleAlternativesBy(alternatives, factor);
2449
2517
  }
2518
+ for (const arbitrary of newRecipe.arbitraries) {
2519
+ arbitrary.quantity = multiplyQuantityValue(
2520
+ arbitrary.quantity,
2521
+ factor
2522
+ );
2523
+ }
2450
2524
  newRecipe._populate_ingredient_quantities();
2451
2525
  newRecipe.servings = (0, import_big4.default)(originalServings).times(factor).toNumber();
2452
2526
  if (newRecipe.metadata.servings && this.metadata.servings) {
@@ -2509,6 +2583,7 @@ var _Recipe = class _Recipe {
2509
2583
  });
2510
2584
  newRecipe.cookware = deepClone(this.cookware);
2511
2585
  newRecipe.timers = deepClone(this.timers);
2586
+ newRecipe.arbitraries = deepClone(this.arbitraries);
2512
2587
  newRecipe.servings = this.servings;
2513
2588
  return newRecipe;
2514
2589
  }