@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.js CHANGED
@@ -265,6 +265,12 @@ var i = (() => {
265
265
  })();
266
266
 
267
267
  // src/regex.ts
268
+ var metadataKeyRegex = /^([^:\n]+?):/gm;
269
+ var numericValueRegex = /^-?\d+(\.\d+)?$/;
270
+ var nestedMetaVarRegex = (varName) => new RegExp(
271
+ `^${varName}:\\s*\\r?\\n((?:[ ]+.+(?:\\r?\\n|$))+)`,
272
+ "m"
273
+ );
268
274
  var metadataRegex = d().literal("---").newline().startCaptureGroup().anyCharacter().zeroOrMore().optional().endGroup().newline().literal("---").dotAll().toRegExp();
269
275
  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();
270
276
  var nonWordChar = "\\s@#~\\[\\]{(,;:!?";
@@ -876,6 +882,20 @@ var InvalidQuantityFormat = class extends Error {
876
882
  this.name = "InvalidQuantityFormat";
877
883
  }
878
884
  };
885
+ var NoTabAsIndentError = class extends Error {
886
+ constructor() {
887
+ super(
888
+ `Tabs are not allowed for indentation in metadata blocks. Please use spaces only.`
889
+ );
890
+ this.name = "NoTabAsIndentError";
891
+ }
892
+ };
893
+ var BadIndentationError = class extends Error {
894
+ constructor() {
895
+ super(`Bad identation of a nested block. Please use spaces only.`);
896
+ this.name = "BadIndentationError";
897
+ }
898
+ };
879
899
 
880
900
  // src/utils/type_guards.ts
881
901
  function isGroup(x) {
@@ -1433,6 +1453,108 @@ function parseListMetaVar(content, varName) {
1433
1453
  return listMatch[2].split("\n").filter((line) => line.trim() !== "").map((line) => line.replace(/^\s*-\s*/, "").trim());
1434
1454
  }
1435
1455
  }
1456
+ function extractAllMetadataKeys(content) {
1457
+ const keys = [];
1458
+ for (const match of content.matchAll(metadataKeyRegex)) {
1459
+ keys.push(match[1].trim());
1460
+ }
1461
+ return [...new Set(keys)];
1462
+ }
1463
+ function parseNestedMetaVar(content, varName) {
1464
+ const match = content.match(nestedMetaVarRegex(varName));
1465
+ if (!match) return void 0;
1466
+ const nestedContent = match[1];
1467
+ return parseNestedBlock(nestedContent);
1468
+ }
1469
+ function parseNestedBlock(content) {
1470
+ const lines = content.split(/\r?\n/).filter((line) => line.trim() !== "");
1471
+ if (lines.length === 0) return void 0;
1472
+ const baseIndentMatch = lines[0].match(/^(\s*)/);
1473
+ if (baseIndentMatch?.[0]?.includes(" ")) {
1474
+ throw new NoTabAsIndentError();
1475
+ }
1476
+ const baseIndent = baseIndentMatch?.[1]?.length;
1477
+ if (lines[0].trim().startsWith("- ")) return void 0;
1478
+ const result = {};
1479
+ let i2 = 0;
1480
+ while (i2 < lines.length) {
1481
+ const line = lines[i2];
1482
+ const leadingWhitespace = line.match(/^(\s*)/)?.[1];
1483
+ if (leadingWhitespace && leadingWhitespace.includes(" ")) {
1484
+ throw new NoTabAsIndentError();
1485
+ }
1486
+ const currentIndent = leadingWhitespace.length;
1487
+ if (currentIndent < baseIndent) {
1488
+ break;
1489
+ }
1490
+ if (currentIndent !== baseIndent) {
1491
+ throw new BadIndentationError();
1492
+ }
1493
+ const keyValueMatch = line.match(/^[ ]*([^:\n]+?):\s*(.*)$/);
1494
+ if (!keyValueMatch) {
1495
+ i2++;
1496
+ continue;
1497
+ }
1498
+ const key = keyValueMatch[1].trim();
1499
+ const rawValue = keyValueMatch[2].trim();
1500
+ if (rawValue === "") {
1501
+ const childLines = [];
1502
+ let j = i2 + 1;
1503
+ while (j < lines.length) {
1504
+ const childLine = lines[j];
1505
+ const childIndent = childLine.match(/^([ ]*)/)?.[1]?.length;
1506
+ if (childIndent && childIndent > baseIndent) {
1507
+ childLines.push(childLine);
1508
+ j++;
1509
+ } else {
1510
+ break;
1511
+ }
1512
+ }
1513
+ if (childLines.length > 0) {
1514
+ const firstChildTrimmed = childLines[0].trim();
1515
+ if (firstChildTrimmed.startsWith("- ")) {
1516
+ const reconstructedContent = `${key}:
1517
+ ${childLines.join("\n")}`;
1518
+ const listResult = parseListMetaVar(reconstructedContent, key);
1519
+ if (listResult) {
1520
+ result[key] = listResult.map(
1521
+ (item) => parseMetadataValue(item)
1522
+ );
1523
+ }
1524
+ } else {
1525
+ const childContent = childLines.join("\n");
1526
+ const nested = parseNestedBlock(childContent);
1527
+ if (nested) {
1528
+ result[key] = nested;
1529
+ }
1530
+ }
1531
+ }
1532
+ i2 = j;
1533
+ } else {
1534
+ result[key] = parseMetadataValue(rawValue);
1535
+ i2++;
1536
+ }
1537
+ }
1538
+ return result;
1539
+ }
1540
+ function parseMetadataValue(rawValue) {
1541
+ if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
1542
+ return rawValue.slice(1, -1).split(",").map((item) => item.trim());
1543
+ }
1544
+ if (numericValueRegex.test(rawValue)) {
1545
+ return Number(rawValue);
1546
+ }
1547
+ return rawValue;
1548
+ }
1549
+ function parseAnyMetaVar(content, varName) {
1550
+ const nested = parseNestedMetaVar(content, varName);
1551
+ if (nested) return nested;
1552
+ const list = parseListMetaVar(content, varName);
1553
+ if (list) return list;
1554
+ const simple = parseSimpleMetaVar(content, varName);
1555
+ if (simple) return parseMetadataValue(simple);
1556
+ return void 0;
1557
+ }
1436
1558
  function extractMetadata(content) {
1437
1559
  const metadata = {};
1438
1560
  let servings = void 0;
@@ -1440,13 +1562,24 @@ function extractMetadata(content) {
1440
1562
  if (!metadataContent) {
1441
1563
  return { metadata };
1442
1564
  }
1443
- for (const metaVar of [
1565
+ const handledKeys = /* @__PURE__ */ new Set([
1566
+ // Simple string fields
1444
1567
  "title",
1568
+ "author",
1569
+ "locale",
1570
+ "introduction",
1571
+ "description",
1572
+ "course",
1573
+ "category",
1574
+ "diet",
1575
+ "cuisine",
1576
+ "difficulty",
1577
+ // Source fields
1445
1578
  "source",
1446
1579
  "source.name",
1447
1580
  "source.url",
1448
- "author",
1449
1581
  "source.author",
1582
+ // Time fields
1450
1583
  "prep time",
1451
1584
  "time.prep",
1452
1585
  "cook time",
@@ -1454,6 +1587,23 @@ function extractMetadata(content) {
1454
1587
  "time required",
1455
1588
  "time",
1456
1589
  "duration",
1590
+ // Image fields
1591
+ "image",
1592
+ "picture",
1593
+ "images",
1594
+ "pictures",
1595
+ // Unit system
1596
+ "unit system",
1597
+ // Scaling fields
1598
+ "servings",
1599
+ "yield",
1600
+ "serves",
1601
+ // List fields
1602
+ "tags"
1603
+ ]);
1604
+ for (const metaVar of [
1605
+ "title",
1606
+ "author",
1457
1607
  "locale",
1458
1608
  "introduction",
1459
1609
  "description",
@@ -1461,17 +1611,57 @@ function extractMetadata(content) {
1461
1611
  "category",
1462
1612
  "diet",
1463
1613
  "cuisine",
1464
- "difficulty",
1465
- "image",
1466
- "picture"
1614
+ "difficulty"
1467
1615
  ]) {
1468
1616
  const stringMetaValue = parseSimpleMetaVar(metadataContent, metaVar);
1469
1617
  if (stringMetaValue) metadata[metaVar] = stringMetaValue;
1470
1618
  }
1619
+ const sourceNested = parseNestedMetaVar(metadataContent, "source");
1620
+ const sourceTxt = parseSimpleMetaVar(metadataContent, "source");
1621
+ const sourceName = parseSimpleMetaVar(metadataContent, "source.name");
1622
+ const sourceUrl = parseSimpleMetaVar(metadataContent, "source.url");
1623
+ const sourceAuthor = parseSimpleMetaVar(metadataContent, "source.author");
1624
+ if (sourceNested) {
1625
+ const source = {};
1626
+ if (typeof sourceNested.name === "string") source.name = sourceNested.name;
1627
+ if (typeof sourceNested.url === "string") source.url = sourceNested.url;
1628
+ if (typeof sourceNested.author === "string")
1629
+ source.author = sourceNested.author;
1630
+ if (Object.keys(source).length > 0) metadata.source = source;
1631
+ } else if (sourceName || sourceAuthor || sourceUrl) {
1632
+ const source = {};
1633
+ if (sourceName) source.name = sourceName;
1634
+ if (sourceUrl) source.url = sourceUrl;
1635
+ if (sourceAuthor) source.author = sourceAuthor;
1636
+ metadata.source = source;
1637
+ } else if (sourceTxt) {
1638
+ metadata.source = sourceTxt;
1639
+ }
1640
+ const timeNested = parseNestedMetaVar(metadataContent, "time");
1641
+ const prepTime = parseSimpleMetaVar(metadataContent, "prep time") ?? parseSimpleMetaVar(metadataContent, "time.prep");
1642
+ const cookTime = parseSimpleMetaVar(metadataContent, "cook time") ?? parseSimpleMetaVar(metadataContent, "time.cook");
1643
+ const totalTime = parseSimpleMetaVar(metadataContent, "time required") ?? parseSimpleMetaVar(metadataContent, "time") ?? parseSimpleMetaVar(metadataContent, "duration");
1644
+ if (timeNested) {
1645
+ const time = {};
1646
+ if (typeof timeNested.prep === "string") time.prep = timeNested.prep;
1647
+ if (typeof timeNested.cook === "string") time.cook = timeNested.cook;
1648
+ if (typeof timeNested.total === "string") time.total = timeNested.total;
1649
+ if (Object.keys(time).length > 0) metadata.time = time;
1650
+ } else if (prepTime || cookTime || totalTime) {
1651
+ const time = {};
1652
+ if (prepTime) time.prep = prepTime;
1653
+ if (cookTime) time.cook = cookTime;
1654
+ if (totalTime) time.total = totalTime;
1655
+ metadata.time = time;
1656
+ }
1657
+ const image = parseSimpleMetaVar(metadataContent, "image") ?? parseSimpleMetaVar(metadataContent, "picture");
1658
+ if (image) metadata.image = image;
1659
+ const images = parseListMetaVar(metadataContent, "images") ?? parseListMetaVar(metadataContent, "pictures");
1660
+ if (images) metadata.images = images;
1471
1661
  let unitSystem;
1472
1662
  const unitSystemRaw = parseSimpleMetaVar(metadataContent, "unit system");
1473
1663
  if (unitSystemRaw) {
1474
- metadata["unit system"] = unitSystemRaw;
1664
+ metadata.unitSystem = unitSystemRaw;
1475
1665
  const unitSystemMap = {
1476
1666
  metric: "metric",
1477
1667
  us: "US",
@@ -1480,16 +1670,22 @@ function extractMetadata(content) {
1480
1670
  };
1481
1671
  unitSystem = unitSystemMap[unitSystemRaw.toLowerCase()];
1482
1672
  }
1483
- for (const metaVar of ["serves", "yield", "servings"]) {
1673
+ for (const metaVar of ["servings", "yield", "serves"]) {
1484
1674
  const scalingMetaValue = parseScalingMetaVar(metadataContent, metaVar);
1485
1675
  if (scalingMetaValue && scalingMetaValue[1]) {
1486
1676
  metadata[metaVar] = scalingMetaValue[1];
1487
1677
  servings = scalingMetaValue[0];
1488
1678
  }
1489
1679
  }
1490
- for (const metaVar of ["tags", "images", "pictures"]) {
1491
- const listMetaValue = parseListMetaVar(metadataContent, metaVar);
1492
- if (listMetaValue) metadata[metaVar] = listMetaValue;
1680
+ const tags = parseListMetaVar(metadataContent, "tags");
1681
+ if (tags) metadata.tags = tags;
1682
+ const allKeys = extractAllMetadataKeys(metadataContent);
1683
+ for (const key of allKeys) {
1684
+ if (handledKeys.has(key)) continue;
1685
+ const value = parseAnyMetaVar(metadataContent, key);
1686
+ if (value !== void 0) {
1687
+ metadata[key] = value;
1688
+ }
1493
1689
  }
1494
1690
  return { metadata, servings, unitSystem };
1495
1691
  }
@@ -2390,7 +2586,7 @@ var _Recipe = class _Recipe {
2390
2586
  * Quantities are grouped by their alternative signature and summed using addEquivalentsAndSimplify.
2391
2587
  * @internal
2392
2588
  */
2393
- _populate_ingredient_quantities() {
2589
+ _populateIngredientQuantities() {
2394
2590
  for (const ing of this.ingredients) {
2395
2591
  delete ing.quantities;
2396
2592
  delete ing.usedAsPrimary;
@@ -2751,7 +2947,7 @@ var _Recipe = class _Recipe {
2751
2947
  if (!section.isBlank()) {
2752
2948
  this.sections.push(section);
2753
2949
  }
2754
- this._populate_ingredient_quantities();
2950
+ this._populateIngredientQuantities();
2755
2951
  }
2756
2952
  /**
2757
2953
  * Scales the recipe to a new number of servings. In practice, it calls
@@ -2849,7 +3045,7 @@ var _Recipe = class _Recipe {
2849
3045
  factor
2850
3046
  );
2851
3047
  }
2852
- newRecipe._populate_ingredient_quantities();
3048
+ newRecipe._populateIngredientQuantities();
2853
3049
  newRecipe.servings = Big4(originalServings).times(factor).toNumber();
2854
3050
  if (newRecipe.metadata.servings && this.metadata.servings) {
2855
3051
  if (floatRegex.test(String(this.metadata.servings).replace(",", ".").trim())) {
@@ -2914,9 +3110,9 @@ var _Recipe = class _Recipe {
2914
3110
  if (method === "remove") {
2915
3111
  return newPrimary;
2916
3112
  } else if (method === "replace") {
3113
+ if (source === "converted") remainingEquivalents.push(oldPrimary);
2917
3114
  if (remainingEquivalents.length > 0) {
2918
3115
  newPrimary.equivalents = remainingEquivalents;
2919
- if (source === "converted") newPrimary.equivalents.push(oldPrimary);
2920
3116
  }
2921
3117
  } else {
2922
3118
  newPrimary.equivalents = [oldPrimary, ...remainingEquivalents];
@@ -3034,7 +3230,7 @@ var _Recipe = class _Recipe {
3034
3230
  for (const alternatives of newRecipe.choices.ingredientItems.values()) {
3035
3231
  convertAlternatives(alternatives);
3036
3232
  }
3037
- newRecipe._populate_ingredient_quantities();
3233
+ newRecipe._populateIngredientQuantities();
3038
3234
  if (method !== "keep") _Recipe.unitSystems.set(newRecipe, system);
3039
3235
  return newRecipe;
3040
3236
  }
@@ -3087,9 +3283,9 @@ var Recipe = _Recipe;
3087
3283
  var ShoppingList = class {
3088
3284
  /**
3089
3285
  * Creates a new ShoppingList instance
3090
- * @param category_config_str - The category configuration to parse.
3286
+ * @param categoryConfigStr - The category configuration to parse.
3091
3287
  */
3092
- constructor(category_config_str) {
3288
+ constructor(categoryConfigStr) {
3093
3289
  // TODO: backport type change
3094
3290
  /**
3095
3291
  * The ingredients in the shopping list.
@@ -3102,16 +3298,16 @@ var ShoppingList = class {
3102
3298
  /**
3103
3299
  * The category configuration for the shopping list.
3104
3300
  */
3105
- __publicField(this, "category_config");
3301
+ __publicField(this, "categoryConfig");
3106
3302
  /**
3107
3303
  * The categorized ingredients in the shopping list.
3108
3304
  */
3109
3305
  __publicField(this, "categories");
3110
- if (category_config_str) {
3111
- this.set_category_config(category_config_str);
3306
+ if (categoryConfigStr) {
3307
+ this.setCategoryConfig(categoryConfigStr);
3112
3308
  }
3113
3309
  }
3114
- calculate_ingredients() {
3310
+ calculateIngredients() {
3115
3311
  this.ingredients = [];
3116
3312
  const addIngredientQuantity = (name, quantityTotal) => {
3117
3313
  const quantityTotalExtended = extendAllUnits(quantityTotal);
@@ -3198,7 +3394,7 @@ var ShoppingList = class {
3198
3394
  * @param options - Options for adding the recipe.
3199
3395
  * @throws Error if the recipe has alternatives without corresponding choices.
3200
3396
  */
3201
- add_recipe(recipe, options = {}) {
3397
+ addRecipe(recipe, options = {}) {
3202
3398
  const errorMessage = this.getUnresolvedAlternativesError(
3203
3399
  recipe,
3204
3400
  options.choices
@@ -3227,7 +3423,7 @@ var ShoppingList = class {
3227
3423
  });
3228
3424
  }
3229
3425
  }
3230
- this.calculate_ingredients();
3426
+ this.calculateIngredients();
3231
3427
  this.categorize();
3232
3428
  }
3233
3429
  /**
@@ -3267,15 +3463,15 @@ var ShoppingList = class {
3267
3463
  }
3268
3464
  /**
3269
3465
  * Removes a recipe from the shopping list, then automatically
3270
- * recalculates the quantities and recategorize the ingredients.s
3466
+ * recalculates the quantities and recategorize the ingredients.
3271
3467
  * @param index - The index of the recipe to remove.
3272
3468
  */
3273
- remove_recipe(index) {
3469
+ removeRecipe(index) {
3274
3470
  if (index < 0 || index >= this.recipes.length) {
3275
3471
  throw new Error("Index out of bounds");
3276
3472
  }
3277
3473
  this.recipes.splice(index, 1);
3278
- this.calculate_ingredients();
3474
+ this.calculateIngredients();
3279
3475
  this.categorize();
3280
3476
  }
3281
3477
  /**
@@ -3283,10 +3479,10 @@ var ShoppingList = class {
3283
3479
  * and automatically categorize current ingredients from the list.
3284
3480
  * @param config - The category configuration to parse.
3285
3481
  */
3286
- set_category_config(config) {
3482
+ setCategoryConfig(config) {
3287
3483
  if (typeof config === "string")
3288
- this.category_config = new CategoryConfig(config);
3289
- else if (config instanceof CategoryConfig) this.category_config = config;
3484
+ this.categoryConfig = new CategoryConfig(config);
3485
+ else if (config instanceof CategoryConfig) this.categoryConfig = config;
3290
3486
  else throw new Error("Invalid category configuration");
3291
3487
  this.categorize();
3292
3488
  }
@@ -3295,17 +3491,17 @@ var ShoppingList = class {
3295
3491
  * Will use the category config if any, otherwise all ingredients will be placed in the "other" category
3296
3492
  */
3297
3493
  categorize() {
3298
- if (!this.category_config) {
3494
+ if (!this.categoryConfig) {
3299
3495
  this.categories = { other: this.ingredients };
3300
3496
  return;
3301
3497
  }
3302
3498
  const categories = { other: [] };
3303
- for (const category of this.category_config.categories) {
3499
+ for (const category of this.categoryConfig.categories) {
3304
3500
  categories[category.name] = [];
3305
3501
  }
3306
3502
  for (const ingredient of this.ingredients) {
3307
3503
  let found = false;
3308
- for (const category of this.category_config.categories) {
3504
+ for (const category of this.categoryConfig.categories) {
3309
3505
  for (const categoryIngredient of category.ingredients) {
3310
3506
  if (categoryIngredient.aliases.includes(ingredient.name)) {
3311
3507
  categories[category.name].push(ingredient);
@@ -3369,7 +3565,6 @@ var ShoppingCart = class {
3369
3565
  setProductCatalog(catalog) {
3370
3566
  this.productCatalog = catalog;
3371
3567
  }
3372
- // TODO: harmonize recipe name to use underscores
3373
3568
  /**
3374
3569
  * Sets the shopping list to build the cart from.
3375
3570
  * To use if a shopping list was not provided at the creation of the instance