@tmlmt/cooklang-parser 3.0.0-alpha.12 → 3.0.0-alpha.13

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
@@ -320,6 +320,12 @@ var i = (() => {
320
320
  })();
321
321
 
322
322
  // src/regex.ts
323
+ var metadataKeyRegex = /^([^:\n]+?):/gm;
324
+ var numericValueRegex = /^-?\d+(\.\d+)?$/;
325
+ var nestedMetaVarRegex = (varName) => new RegExp(
326
+ `^${varName}:\\s*\\r?\\n((?:[ ]+.+(?:\\r?\\n|$))+)`,
327
+ "m"
328
+ );
323
329
  var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
324
330
  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();
325
331
  var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
@@ -931,6 +937,20 @@ var InvalidQuantityFormat = class extends Error {
931
937
  this.name = "InvalidQuantityFormat";
932
938
  }
933
939
  };
940
+ var NoTabAsIndentError = class extends Error {
941
+ constructor() {
942
+ super(
943
+ `Tabs are not allowed for indentation in metadata blocks. Please use spaces only.`
944
+ );
945
+ this.name = "NoTabAsIndentError";
946
+ }
947
+ };
948
+ var BadIndentationError = class extends Error {
949
+ constructor() {
950
+ super(`Bad identation of a nested block. Please use spaces only.`);
951
+ this.name = "BadIndentationError";
952
+ }
953
+ };
934
954
 
935
955
  // src/utils/type_guards.ts
936
956
  function isGroup(x) {
@@ -1488,6 +1508,108 @@ function parseListMetaVar(content, varName) {
1488
1508
  return listMatch[2].split("\n").filter((line) => line.trim() !== "").map((line) => line.replace(/^\s*-\s*/, "").trim());
1489
1509
  }
1490
1510
  }
1511
+ function extractAllMetadataKeys(content) {
1512
+ const keys = [];
1513
+ for (const match of content.matchAll(metadataKeyRegex)) {
1514
+ keys.push(match[1].trim());
1515
+ }
1516
+ return [...new Set(keys)];
1517
+ }
1518
+ function parseNestedMetaVar(content, varName) {
1519
+ const match = content.match(nestedMetaVarRegex(varName));
1520
+ if (!match) return void 0;
1521
+ const nestedContent = match[1];
1522
+ return parseNestedBlock(nestedContent);
1523
+ }
1524
+ function parseNestedBlock(content) {
1525
+ const lines = content.split(/\r?\n/).filter((line) => line.trim() !== "");
1526
+ if (lines.length === 0) return void 0;
1527
+ const baseIndentMatch = lines[0].match(/^(\s*)/);
1528
+ if (baseIndentMatch?.[0]?.includes(" ")) {
1529
+ throw new NoTabAsIndentError();
1530
+ }
1531
+ const baseIndent = baseIndentMatch?.[1]?.length;
1532
+ if (lines[0].trim().startsWith("- ")) return void 0;
1533
+ const result = {};
1534
+ let i2 = 0;
1535
+ while (i2 < lines.length) {
1536
+ const line = lines[i2];
1537
+ const leadingWhitespace = line.match(/^(\s*)/)?.[1];
1538
+ if (leadingWhitespace && leadingWhitespace.includes(" ")) {
1539
+ throw new NoTabAsIndentError();
1540
+ }
1541
+ const currentIndent = leadingWhitespace.length;
1542
+ if (currentIndent < baseIndent) {
1543
+ break;
1544
+ }
1545
+ if (currentIndent !== baseIndent) {
1546
+ throw new BadIndentationError();
1547
+ }
1548
+ const keyValueMatch = line.match(/^[ ]*([^:\n]+?):\s*(.*)$/);
1549
+ if (!keyValueMatch) {
1550
+ i2++;
1551
+ continue;
1552
+ }
1553
+ const key = keyValueMatch[1].trim();
1554
+ const rawValue = keyValueMatch[2].trim();
1555
+ if (rawValue === "") {
1556
+ const childLines = [];
1557
+ let j = i2 + 1;
1558
+ while (j < lines.length) {
1559
+ const childLine = lines[j];
1560
+ const childIndent = childLine.match(/^([ ]*)/)?.[1]?.length;
1561
+ if (childIndent && childIndent > baseIndent) {
1562
+ childLines.push(childLine);
1563
+ j++;
1564
+ } else {
1565
+ break;
1566
+ }
1567
+ }
1568
+ if (childLines.length > 0) {
1569
+ const firstChildTrimmed = childLines[0].trim();
1570
+ if (firstChildTrimmed.startsWith("- ")) {
1571
+ const reconstructedContent = `${key}:
1572
+ ${childLines.join("\n")}`;
1573
+ const listResult = parseListMetaVar(reconstructedContent, key);
1574
+ if (listResult) {
1575
+ result[key] = listResult.map(
1576
+ (item) => parseMetadataValue(item)
1577
+ );
1578
+ }
1579
+ } else {
1580
+ const childContent = childLines.join("\n");
1581
+ const nested = parseNestedBlock(childContent);
1582
+ if (nested) {
1583
+ result[key] = nested;
1584
+ }
1585
+ }
1586
+ }
1587
+ i2 = j;
1588
+ } else {
1589
+ result[key] = parseMetadataValue(rawValue);
1590
+ i2++;
1591
+ }
1592
+ }
1593
+ return result;
1594
+ }
1595
+ function parseMetadataValue(rawValue) {
1596
+ if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
1597
+ return rawValue.slice(1, -1).split(",").map((item) => item.trim());
1598
+ }
1599
+ if (numericValueRegex.test(rawValue)) {
1600
+ return Number(rawValue);
1601
+ }
1602
+ return rawValue;
1603
+ }
1604
+ function parseAnyMetaVar(content, varName) {
1605
+ const nested = parseNestedMetaVar(content, varName);
1606
+ if (nested) return nested;
1607
+ const list = parseListMetaVar(content, varName);
1608
+ if (list) return list;
1609
+ const simple = parseSimpleMetaVar(content, varName);
1610
+ if (simple) return parseMetadataValue(simple);
1611
+ return void 0;
1612
+ }
1491
1613
  function extractMetadata(content) {
1492
1614
  const metadata = {};
1493
1615
  let servings = void 0;
@@ -1495,13 +1617,24 @@ function extractMetadata(content) {
1495
1617
  if (!metadataContent) {
1496
1618
  return { metadata };
1497
1619
  }
1498
- for (const metaVar of [
1620
+ const handledKeys = /* @__PURE__ */ new Set([
1621
+ // Simple string fields
1499
1622
  "title",
1623
+ "author",
1624
+ "locale",
1625
+ "introduction",
1626
+ "description",
1627
+ "course",
1628
+ "category",
1629
+ "diet",
1630
+ "cuisine",
1631
+ "difficulty",
1632
+ // Source fields
1500
1633
  "source",
1501
1634
  "source.name",
1502
1635
  "source.url",
1503
- "author",
1504
1636
  "source.author",
1637
+ // Time fields
1505
1638
  "prep time",
1506
1639
  "time.prep",
1507
1640
  "cook time",
@@ -1509,6 +1642,23 @@ function extractMetadata(content) {
1509
1642
  "time required",
1510
1643
  "time",
1511
1644
  "duration",
1645
+ // Image fields
1646
+ "image",
1647
+ "picture",
1648
+ "images",
1649
+ "pictures",
1650
+ // Unit system
1651
+ "unit system",
1652
+ // Scaling fields
1653
+ "servings",
1654
+ "yield",
1655
+ "serves",
1656
+ // List fields
1657
+ "tags"
1658
+ ]);
1659
+ for (const metaVar of [
1660
+ "title",
1661
+ "author",
1512
1662
  "locale",
1513
1663
  "introduction",
1514
1664
  "description",
@@ -1516,17 +1666,57 @@ function extractMetadata(content) {
1516
1666
  "category",
1517
1667
  "diet",
1518
1668
  "cuisine",
1519
- "difficulty",
1520
- "image",
1521
- "picture"
1669
+ "difficulty"
1522
1670
  ]) {
1523
1671
  const stringMetaValue = parseSimpleMetaVar(metadataContent, metaVar);
1524
1672
  if (stringMetaValue) metadata[metaVar] = stringMetaValue;
1525
1673
  }
1674
+ const sourceNested = parseNestedMetaVar(metadataContent, "source");
1675
+ const sourceTxt = parseSimpleMetaVar(metadataContent, "source");
1676
+ const sourceName = parseSimpleMetaVar(metadataContent, "source.name");
1677
+ const sourceUrl = parseSimpleMetaVar(metadataContent, "source.url");
1678
+ const sourceAuthor = parseSimpleMetaVar(metadataContent, "source.author");
1679
+ if (sourceNested) {
1680
+ const source = {};
1681
+ if (typeof sourceNested.name === "string") source.name = sourceNested.name;
1682
+ if (typeof sourceNested.url === "string") source.url = sourceNested.url;
1683
+ if (typeof sourceNested.author === "string")
1684
+ source.author = sourceNested.author;
1685
+ if (Object.keys(source).length > 0) metadata.source = source;
1686
+ } else if (sourceName || sourceAuthor || sourceUrl) {
1687
+ const source = {};
1688
+ if (sourceName) source.name = sourceName;
1689
+ if (sourceUrl) source.url = sourceUrl;
1690
+ if (sourceAuthor) source.author = sourceAuthor;
1691
+ metadata.source = source;
1692
+ } else if (sourceTxt) {
1693
+ metadata.source = sourceTxt;
1694
+ }
1695
+ const timeNested = parseNestedMetaVar(metadataContent, "time");
1696
+ const prepTime = parseSimpleMetaVar(metadataContent, "prep time") ?? parseSimpleMetaVar(metadataContent, "time.prep");
1697
+ const cookTime = parseSimpleMetaVar(metadataContent, "cook time") ?? parseSimpleMetaVar(metadataContent, "time.cook");
1698
+ const totalTime = parseSimpleMetaVar(metadataContent, "time required") ?? parseSimpleMetaVar(metadataContent, "time") ?? parseSimpleMetaVar(metadataContent, "duration");
1699
+ if (timeNested) {
1700
+ const time = {};
1701
+ if (typeof timeNested.prep === "string") time.prep = timeNested.prep;
1702
+ if (typeof timeNested.cook === "string") time.cook = timeNested.cook;
1703
+ if (typeof timeNested.total === "string") time.total = timeNested.total;
1704
+ if (Object.keys(time).length > 0) metadata.time = time;
1705
+ } else if (prepTime || cookTime || totalTime) {
1706
+ const time = {};
1707
+ if (prepTime) time.prep = prepTime;
1708
+ if (cookTime) time.cook = cookTime;
1709
+ if (totalTime) time.total = totalTime;
1710
+ metadata.time = time;
1711
+ }
1712
+ const image = parseSimpleMetaVar(metadataContent, "image") ?? parseSimpleMetaVar(metadataContent, "picture");
1713
+ if (image) metadata.image = image;
1714
+ const images = parseListMetaVar(metadataContent, "images") ?? parseListMetaVar(metadataContent, "pictures");
1715
+ if (images) metadata.images = images;
1526
1716
  let unitSystem;
1527
1717
  const unitSystemRaw = parseSimpleMetaVar(metadataContent, "unit system");
1528
1718
  if (unitSystemRaw) {
1529
- metadata["unit system"] = unitSystemRaw;
1719
+ metadata.unitSystem = unitSystemRaw;
1530
1720
  const unitSystemMap = {
1531
1721
  metric: "metric",
1532
1722
  us: "US",
@@ -1535,16 +1725,22 @@ function extractMetadata(content) {
1535
1725
  };
1536
1726
  unitSystem = unitSystemMap[unitSystemRaw.toLowerCase()];
1537
1727
  }
1538
- for (const metaVar of ["serves", "yield", "servings"]) {
1728
+ for (const metaVar of ["servings", "yield", "serves"]) {
1539
1729
  const scalingMetaValue = parseScalingMetaVar(metadataContent, metaVar);
1540
1730
  if (scalingMetaValue && scalingMetaValue[1]) {
1541
1731
  metadata[metaVar] = scalingMetaValue[1];
1542
1732
  servings = scalingMetaValue[0];
1543
1733
  }
1544
1734
  }
1545
- for (const metaVar of ["tags", "images", "pictures"]) {
1546
- const listMetaValue = parseListMetaVar(metadataContent, metaVar);
1547
- if (listMetaValue) metadata[metaVar] = listMetaValue;
1735
+ const tags = parseListMetaVar(metadataContent, "tags");
1736
+ if (tags) metadata.tags = tags;
1737
+ const allKeys = extractAllMetadataKeys(metadataContent);
1738
+ for (const key of allKeys) {
1739
+ if (handledKeys.has(key)) continue;
1740
+ const value = parseAnyMetaVar(metadataContent, key);
1741
+ if (value !== void 0) {
1742
+ metadata[key] = value;
1743
+ }
1548
1744
  }
1549
1745
  return { metadata, servings, unitSystem };
1550
1746
  }
@@ -2445,7 +2641,7 @@ var _Recipe = class _Recipe {
2445
2641
  * Quantities are grouped by their alternative signature and summed using addEquivalentsAndSimplify.
2446
2642
  * @internal
2447
2643
  */
2448
- _populate_ingredient_quantities() {
2644
+ _populateIngredientQuantities() {
2449
2645
  for (const ing of this.ingredients) {
2450
2646
  delete ing.quantities;
2451
2647
  delete ing.usedAsPrimary;
@@ -2806,7 +3002,7 @@ var _Recipe = class _Recipe {
2806
3002
  if (!section.isBlank()) {
2807
3003
  this.sections.push(section);
2808
3004
  }
2809
- this._populate_ingredient_quantities();
3005
+ this._populateIngredientQuantities();
2810
3006
  }
2811
3007
  /**
2812
3008
  * Scales the recipe to a new number of servings. In practice, it calls
@@ -2904,7 +3100,7 @@ var _Recipe = class _Recipe {
2904
3100
  factor
2905
3101
  );
2906
3102
  }
2907
- newRecipe._populate_ingredient_quantities();
3103
+ newRecipe._populateIngredientQuantities();
2908
3104
  newRecipe.servings = (0, import_big4.default)(originalServings).times(factor).toNumber();
2909
3105
  if (newRecipe.metadata.servings && this.metadata.servings) {
2910
3106
  if (floatRegex.test(String(this.metadata.servings).replace(",", ".").trim())) {
@@ -2969,9 +3165,9 @@ var _Recipe = class _Recipe {
2969
3165
  if (method === "remove") {
2970
3166
  return newPrimary;
2971
3167
  } else if (method === "replace") {
3168
+ if (source === "converted") remainingEquivalents.push(oldPrimary);
2972
3169
  if (remainingEquivalents.length > 0) {
2973
3170
  newPrimary.equivalents = remainingEquivalents;
2974
- if (source === "converted") newPrimary.equivalents.push(oldPrimary);
2975
3171
  }
2976
3172
  } else {
2977
3173
  newPrimary.equivalents = [oldPrimary, ...remainingEquivalents];
@@ -3089,7 +3285,7 @@ var _Recipe = class _Recipe {
3089
3285
  for (const alternatives of newRecipe.choices.ingredientItems.values()) {
3090
3286
  convertAlternatives(alternatives);
3091
3287
  }
3092
- newRecipe._populate_ingredient_quantities();
3288
+ newRecipe._populateIngredientQuantities();
3093
3289
  if (method !== "keep") _Recipe.unitSystems.set(newRecipe, system);
3094
3290
  return newRecipe;
3095
3291
  }
@@ -3142,9 +3338,9 @@ var Recipe = _Recipe;
3142
3338
  var ShoppingList = class {
3143
3339
  /**
3144
3340
  * Creates a new ShoppingList instance
3145
- * @param category_config_str - The category configuration to parse.
3341
+ * @param categoryConfigStr - The category configuration to parse.
3146
3342
  */
3147
- constructor(category_config_str) {
3343
+ constructor(categoryConfigStr) {
3148
3344
  // TODO: backport type change
3149
3345
  /**
3150
3346
  * The ingredients in the shopping list.
@@ -3157,16 +3353,16 @@ var ShoppingList = class {
3157
3353
  /**
3158
3354
  * The category configuration for the shopping list.
3159
3355
  */
3160
- __publicField(this, "category_config");
3356
+ __publicField(this, "categoryConfig");
3161
3357
  /**
3162
3358
  * The categorized ingredients in the shopping list.
3163
3359
  */
3164
3360
  __publicField(this, "categories");
3165
- if (category_config_str) {
3166
- this.set_category_config(category_config_str);
3361
+ if (categoryConfigStr) {
3362
+ this.setCategoryConfig(categoryConfigStr);
3167
3363
  }
3168
3364
  }
3169
- calculate_ingredients() {
3365
+ calculateIngredients() {
3170
3366
  this.ingredients = [];
3171
3367
  const addIngredientQuantity = (name, quantityTotal) => {
3172
3368
  const quantityTotalExtended = extendAllUnits(quantityTotal);
@@ -3253,7 +3449,7 @@ var ShoppingList = class {
3253
3449
  * @param options - Options for adding the recipe.
3254
3450
  * @throws Error if the recipe has alternatives without corresponding choices.
3255
3451
  */
3256
- add_recipe(recipe, options = {}) {
3452
+ addRecipe(recipe, options = {}) {
3257
3453
  const errorMessage = this.getUnresolvedAlternativesError(
3258
3454
  recipe,
3259
3455
  options.choices
@@ -3282,7 +3478,7 @@ var ShoppingList = class {
3282
3478
  });
3283
3479
  }
3284
3480
  }
3285
- this.calculate_ingredients();
3481
+ this.calculateIngredients();
3286
3482
  this.categorize();
3287
3483
  }
3288
3484
  /**
@@ -3322,15 +3518,15 @@ var ShoppingList = class {
3322
3518
  }
3323
3519
  /**
3324
3520
  * Removes a recipe from the shopping list, then automatically
3325
- * recalculates the quantities and recategorize the ingredients.s
3521
+ * recalculates the quantities and recategorize the ingredients.
3326
3522
  * @param index - The index of the recipe to remove.
3327
3523
  */
3328
- remove_recipe(index) {
3524
+ removeRecipe(index) {
3329
3525
  if (index < 0 || index >= this.recipes.length) {
3330
3526
  throw new Error("Index out of bounds");
3331
3527
  }
3332
3528
  this.recipes.splice(index, 1);
3333
- this.calculate_ingredients();
3529
+ this.calculateIngredients();
3334
3530
  this.categorize();
3335
3531
  }
3336
3532
  /**
@@ -3338,10 +3534,10 @@ var ShoppingList = class {
3338
3534
  * and automatically categorize current ingredients from the list.
3339
3535
  * @param config - The category configuration to parse.
3340
3536
  */
3341
- set_category_config(config) {
3537
+ setCategoryConfig(config) {
3342
3538
  if (typeof config === "string")
3343
- this.category_config = new CategoryConfig(config);
3344
- else if (config instanceof CategoryConfig) this.category_config = config;
3539
+ this.categoryConfig = new CategoryConfig(config);
3540
+ else if (config instanceof CategoryConfig) this.categoryConfig = config;
3345
3541
  else throw new Error("Invalid category configuration");
3346
3542
  this.categorize();
3347
3543
  }
@@ -3350,17 +3546,17 @@ var ShoppingList = class {
3350
3546
  * Will use the category config if any, otherwise all ingredients will be placed in the "other" category
3351
3547
  */
3352
3548
  categorize() {
3353
- if (!this.category_config) {
3549
+ if (!this.categoryConfig) {
3354
3550
  this.categories = { other: this.ingredients };
3355
3551
  return;
3356
3552
  }
3357
3553
  const categories = { other: [] };
3358
- for (const category of this.category_config.categories) {
3554
+ for (const category of this.categoryConfig.categories) {
3359
3555
  categories[category.name] = [];
3360
3556
  }
3361
3557
  for (const ingredient of this.ingredients) {
3362
3558
  let found = false;
3363
- for (const category of this.category_config.categories) {
3559
+ for (const category of this.categoryConfig.categories) {
3364
3560
  for (const categoryIngredient of category.ingredients) {
3365
3561
  if (categoryIngredient.aliases.includes(ingredient.name)) {
3366
3562
  categories[category.name].push(ingredient);
@@ -3424,7 +3620,6 @@ var ShoppingCart = class {
3424
3620
  setProductCatalog(catalog) {
3425
3621
  this.productCatalog = catalog;
3426
3622
  }
3427
- // TODO: harmonize recipe name to use underscores
3428
3623
  /**
3429
3624
  * Sets the shopping list to build the cart from.
3430
3625
  * To use if a shopping list was not provided at the creation of the instance