@tmlmt/cooklang-parser 1.2.5 → 1.4.0
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/README.md +16 -48
- package/dist/index.cjs +57 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -62
- package/dist/index.d.ts +136 -62
- package/dist/index.js +56 -49
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
A typescript library to parse and manipulate [cooklang](https://cooklang.org/) recipes.
|
|
4
4
|
|
|
5
|
+
<picture><img src="https://badges.ws/maintenance/yes/2025" /></picture>
|
|
6
|
+
<picture><img src="https://badges.ws/npm/dt/@tmlmt/cooklang-parser" /></picture>
|
|
7
|
+
<picture><img src="https://badges.ws/npm/l/@tmlmt/cooklang-parser" /></picture>
|
|
8
|
+
<picture><img src="https://badges.ws/github/release/tmlmt/cooklang-parser" /></picture>
|
|
9
|
+
[<img src="https://badges.ws/badge/documentation-5672CD?icon=vitepress" />](https://cooklang-parser.tmlmt.com)
|
|
10
|
+
|
|
5
11
|
## Introduction
|
|
6
12
|
|
|
7
13
|
This library provides a set of tools to work with recipes written in the Cooklang format. It allows you to parse recipes, extract ingredients, cookware, and timers, scale recipes, and generate shopping lists.
|
|
8
14
|
|
|
9
|
-
The documentation is available at [https://cooklang-parser.tmlmt.com](https://cooklang-parser.tmlmt.com) (work in progress)
|
|
10
|
-
|
|
11
15
|
## Features
|
|
12
16
|
|
|
13
17
|
- **Cooklang Compliant:** Fully compliant with the Cooklang specifications.
|
|
@@ -30,14 +34,16 @@ The documentation is available at [https://cooklang-parser.tmlmt.com](https://co
|
|
|
30
34
|
- Also work with referencing etc., e.g. `Mix @wheat flour{100%g} with additional @&wheat flour|flour{50%g}` enables to get 150g of wheat flour in the ingredients list, and let you display "Mix wheat flour (100 g) with additional flour (50 g)" in your recipe renderer.
|
|
31
35
|
- **Recipe Scaling:** Scale recipes by a given factor.
|
|
32
36
|
- **Shopping Lists:** Generate shopping lists from one or more recipes.
|
|
33
|
-
- **
|
|
37
|
+
- **Category Configuration:** Categorize shopping list ingredients based on a custom category configuration.
|
|
34
38
|
- **Typescript:** Written in Typescript, providing type safety for all the data structures.
|
|
35
39
|
|
|
36
40
|
## Quick start
|
|
37
41
|
|
|
38
|
-
Install the package with your favorite package manager e.g
|
|
42
|
+
- Install the package with your favorite package manager, e.g.:
|
|
43
|
+
|
|
44
|
+
`npm install @tmlmt/cooklang-parser`
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
- Use the `Recipe` class to parse a cooklang recipe:
|
|
41
47
|
|
|
42
48
|
```typescript
|
|
43
49
|
import { Recipe } from "@tmlmt/cooklang-parser";
|
|
@@ -59,50 +65,16 @@ Serve hot.
|
|
|
59
65
|
const recipe = new Recipe(recipeString);
|
|
60
66
|
|
|
61
67
|
console.log(recipe.metadata.title); // "Pancakes"
|
|
62
|
-
console.log(recipe.ingredients);
|
|
63
|
-
console.log(recipe.cookware);
|
|
64
|
-
console.log(recipe.timers);
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
You can also create a shopping list from multiple recipes:
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
import { ShoppingList, Recipe } from "@tmlmt/cooklang-parser";
|
|
71
|
-
|
|
72
|
-
const recipe1 = new Recipe(/* ... */);
|
|
73
|
-
const recipe2 = new Recipe(/* ... */);
|
|
74
|
-
|
|
75
|
-
const shoppingList = new ShoppingList();
|
|
76
|
-
shoppingList.add_recipe(recipe1);
|
|
77
|
-
shoppingList.add_recipe(recipe2);
|
|
78
|
-
|
|
79
|
-
console.log(shoppingList.ingredients);
|
|
68
|
+
console.log(recipe.ingredients); // [{ name: "eggs", ...}, ...]
|
|
69
|
+
console.log(recipe.cookware); // [{ name: "pan", ...}]
|
|
70
|
+
console.log(recipe.timers); // [{ duration: 15, unit: "minutes", name: undefined}]
|
|
80
71
|
```
|
|
81
72
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
```typescript
|
|
85
|
-
const shoppingList = `
|
|
86
|
-
[Dairy]
|
|
87
|
-
milk
|
|
88
|
-
butter
|
|
89
|
-
|
|
90
|
-
[Bakery]
|
|
91
|
-
flour
|
|
92
|
-
sugar
|
|
93
|
-
`;
|
|
94
|
-
|
|
95
|
-
shoppingList.set_aisle_config(aisleConfig);
|
|
96
|
-
shoppingList.categorize();
|
|
97
|
-
|
|
98
|
-
console.log(shoppingList.categories);
|
|
99
|
-
```
|
|
73
|
+
- Browse the [API Reference](https://cooklang-parser.tmlmt.com/api/) to discover all functionalities
|
|
100
74
|
|
|
101
75
|
## Future plans
|
|
102
76
|
|
|
103
|
-
I plan to further develop features depending on the needs I will encounter in using this library in a practical application. The
|
|
104
|
-
|
|
105
|
-
- Ingredients aliases. See issue tmlmt/cooklang-parser#5
|
|
77
|
+
I plan to further develop features depending on the needs I will encounter in using this library in a practical application. The main item on the todo list is to improve the documentation with detailed explanation of the extensions, as well as examples.
|
|
106
78
|
|
|
107
79
|
## Test coverage
|
|
108
80
|
|
|
@@ -111,7 +83,3 @@ This project includes a test setup aimed at eventually ensuring reliable parsing
|
|
|
111
83
|
You can run the tests yourself by cloning the repository and running `pnpm test`. To see the coverage report, run `pnpm test:coverage`.
|
|
112
84
|
|
|
113
85
|
If you find any issue with your own examples of recipes, feel free to open an Issue and if you want to help fix it, to submit a Pull Request.
|
|
114
|
-
|
|
115
|
-
## License
|
|
116
|
-
|
|
117
|
-
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -22,23 +22,22 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
22
22
|
// src/index.ts
|
|
23
23
|
var index_exports = {};
|
|
24
24
|
__export(index_exports, {
|
|
25
|
-
|
|
25
|
+
CategoryConfig: () => CategoryConfig,
|
|
26
26
|
Recipe: () => Recipe,
|
|
27
27
|
Section: () => Section,
|
|
28
28
|
ShoppingList: () => ShoppingList
|
|
29
29
|
});
|
|
30
30
|
module.exports = __toCommonJS(index_exports);
|
|
31
31
|
|
|
32
|
-
// src/classes/
|
|
33
|
-
var
|
|
32
|
+
// src/classes/category_config.ts
|
|
33
|
+
var CategoryConfig = class {
|
|
34
34
|
/**
|
|
35
|
-
* Creates a new
|
|
36
|
-
* @param config - The
|
|
35
|
+
* Creates a new CategoryConfig instance.
|
|
36
|
+
* @param config - The category configuration to parse.
|
|
37
37
|
*/
|
|
38
38
|
constructor(config) {
|
|
39
39
|
/**
|
|
40
|
-
* The categories of
|
|
41
|
-
* @see {@link AisleCategory}
|
|
40
|
+
* The parsed categories of ingredients.
|
|
42
41
|
*/
|
|
43
42
|
__publicField(this, "categories", []);
|
|
44
43
|
if (config) {
|
|
@@ -46,8 +45,9 @@ var AisleConfig = class {
|
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
/**
|
|
49
|
-
* Parses
|
|
50
|
-
*
|
|
48
|
+
* Parses a category configuration from a string into property
|
|
49
|
+
* {@link CategoryConfig.categories | categories}
|
|
50
|
+
* @param config - The category configuration to parse.
|
|
51
51
|
*/
|
|
52
52
|
parse(config) {
|
|
53
53
|
let currentCategory = null;
|
|
@@ -97,7 +97,10 @@ var Section = class {
|
|
|
97
97
|
* @param name - The name of the section. Defaults to an empty string.
|
|
98
98
|
*/
|
|
99
99
|
constructor(name = "") {
|
|
100
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* The name of the section. Can be an empty string for the default (first) section.
|
|
102
|
+
* @defaultValue `""`
|
|
103
|
+
*/
|
|
101
104
|
__publicField(this, "name");
|
|
102
105
|
/** An array of steps and notes that make up the content of the section. */
|
|
103
106
|
__publicField(this, "content", []);
|
|
@@ -343,14 +346,14 @@ var units = [
|
|
|
343
346
|
name: "g",
|
|
344
347
|
type: "mass",
|
|
345
348
|
system: "metric",
|
|
346
|
-
aliases: ["gram", "grams"],
|
|
349
|
+
aliases: ["gram", "grams", "grammes"],
|
|
347
350
|
toBase: 1
|
|
348
351
|
},
|
|
349
352
|
{
|
|
350
353
|
name: "kg",
|
|
351
354
|
type: "mass",
|
|
352
355
|
system: "metric",
|
|
353
|
-
aliases: ["kilogram", "kilograms"],
|
|
356
|
+
aliases: ["kilogram", "kilograms", "kilogrammes", "kilos", "kilo"],
|
|
354
357
|
toBase: 1e3
|
|
355
358
|
},
|
|
356
359
|
// Mass (Imperial)
|
|
@@ -373,7 +376,7 @@ var units = [
|
|
|
373
376
|
name: "ml",
|
|
374
377
|
type: "volume",
|
|
375
378
|
system: "metric",
|
|
376
|
-
aliases: ["milliliter", "milliliters", "millilitre", "millilitres"],
|
|
379
|
+
aliases: ["milliliter", "milliliters", "millilitre", "millilitres", "cc"],
|
|
377
380
|
toBase: 1
|
|
378
381
|
},
|
|
379
382
|
{
|
|
@@ -438,7 +441,7 @@ var units = [
|
|
|
438
441
|
name: "piece",
|
|
439
442
|
type: "count",
|
|
440
443
|
system: "metric",
|
|
441
|
-
aliases: ["pieces"],
|
|
444
|
+
aliases: ["pieces", "pc"],
|
|
442
445
|
toBase: 1
|
|
443
446
|
}
|
|
444
447
|
];
|
|
@@ -793,32 +796,31 @@ var Recipe = class _Recipe {
|
|
|
793
796
|
*/
|
|
794
797
|
constructor(content) {
|
|
795
798
|
/**
|
|
796
|
-
* The recipe
|
|
797
|
-
* @see {@link Metadata}
|
|
799
|
+
* The parsed recipe metadata.
|
|
798
800
|
*/
|
|
799
801
|
__publicField(this, "metadata", {});
|
|
800
802
|
/**
|
|
801
|
-
* The recipe
|
|
802
|
-
* @see {@link Ingredient}
|
|
803
|
+
* The parsed recipe ingredients.
|
|
803
804
|
*/
|
|
804
805
|
__publicField(this, "ingredients", []);
|
|
805
806
|
/**
|
|
806
|
-
* The recipe
|
|
807
|
-
* @see {@link Section}
|
|
807
|
+
* The parsed recipe sections.
|
|
808
808
|
*/
|
|
809
809
|
__publicField(this, "sections", []);
|
|
810
810
|
/**
|
|
811
|
-
* The recipe
|
|
812
|
-
* @see {@link Cookware}
|
|
811
|
+
* The parsed recipe cookware.
|
|
813
812
|
*/
|
|
814
813
|
__publicField(this, "cookware", []);
|
|
815
814
|
/**
|
|
816
|
-
* The recipe
|
|
817
|
-
* @see {@link Timer}
|
|
815
|
+
* The parsed recipe timers.
|
|
818
816
|
*/
|
|
819
817
|
__publicField(this, "timers", []);
|
|
820
818
|
/**
|
|
821
|
-
* The recipe
|
|
819
|
+
* The parsed recipe servings. Used for scaling. Parsed from one of
|
|
820
|
+
* {@link Metadata.servings}, {@link Metadata.yield} or {@link Metadata.serves}
|
|
821
|
+
* metadata fields.
|
|
822
|
+
*
|
|
823
|
+
* @see {@link Recipe.scaleBy | scaleBy()} and {@link Recipe.scaleTo | scaleTo()} methods
|
|
822
824
|
*/
|
|
823
825
|
__publicField(this, "servings");
|
|
824
826
|
if (content) {
|
|
@@ -980,9 +982,12 @@ var Recipe = class _Recipe {
|
|
|
980
982
|
}
|
|
981
983
|
}
|
|
982
984
|
/**
|
|
983
|
-
* Scales the recipe to a new number of servings.
|
|
985
|
+
* Scales the recipe to a new number of servings. In practice, it calls
|
|
986
|
+
* {@link Recipe.scaleBy | scaleBy} with a factor corresponding to the ratio between `newServings`
|
|
987
|
+
* and the recipe's {@link Recipe.servings | servings} value.
|
|
984
988
|
* @param newServings - The new number of servings.
|
|
985
989
|
* @returns A new Recipe instance with the scaled ingredients.
|
|
990
|
+
* @throws `Error` if the recipe does not contains an initial {@link Recipe.servings | servings} value
|
|
986
991
|
*/
|
|
987
992
|
scaleTo(newServings) {
|
|
988
993
|
const originalServings = this.getServings();
|
|
@@ -1067,32 +1072,28 @@ var Recipe = class _Recipe {
|
|
|
1067
1072
|
// src/classes/shopping_list.ts
|
|
1068
1073
|
var ShoppingList = class {
|
|
1069
1074
|
/**
|
|
1070
|
-
* Creates a new ShoppingList instance
|
|
1071
|
-
* @param
|
|
1075
|
+
* Creates a new ShoppingList instance
|
|
1076
|
+
* @param category_config_str - The category configuration to parse.
|
|
1072
1077
|
*/
|
|
1073
|
-
constructor(
|
|
1078
|
+
constructor(category_config_str) {
|
|
1074
1079
|
/**
|
|
1075
1080
|
* The ingredients in the shopping list.
|
|
1076
|
-
* @see {@link Ingredient}
|
|
1077
1081
|
*/
|
|
1078
1082
|
__publicField(this, "ingredients", []);
|
|
1079
1083
|
/**
|
|
1080
1084
|
* The recipes in the shopping list.
|
|
1081
|
-
* @see {@link AddedRecipe}
|
|
1082
1085
|
*/
|
|
1083
1086
|
__publicField(this, "recipes", []);
|
|
1084
1087
|
/**
|
|
1085
|
-
* The
|
|
1086
|
-
* @see {@link AisleConfig}
|
|
1088
|
+
* The category configuration for the shopping list.
|
|
1087
1089
|
*/
|
|
1088
|
-
__publicField(this, "
|
|
1090
|
+
__publicField(this, "category_config");
|
|
1089
1091
|
/**
|
|
1090
1092
|
* The categorized ingredients in the shopping list.
|
|
1091
|
-
* @see {@link CategorizedIngredients}
|
|
1092
1093
|
*/
|
|
1093
1094
|
__publicField(this, "categories");
|
|
1094
|
-
if (
|
|
1095
|
-
this.
|
|
1095
|
+
if (category_config_str) {
|
|
1096
|
+
this.set_category_config(category_config_str);
|
|
1096
1097
|
}
|
|
1097
1098
|
}
|
|
1098
1099
|
calculate_ingredients() {
|
|
@@ -1148,7 +1149,8 @@ var ShoppingList = class {
|
|
|
1148
1149
|
}
|
|
1149
1150
|
}
|
|
1150
1151
|
/**
|
|
1151
|
-
* Adds a recipe to the shopping list
|
|
1152
|
+
* Adds a recipe to the shopping list, then automatically
|
|
1153
|
+
* recalculates the quantities and recategorize the ingredients.
|
|
1152
1154
|
* @param recipe - The recipe to add.
|
|
1153
1155
|
* @param factor - The factor to scale the recipe by.
|
|
1154
1156
|
*/
|
|
@@ -1158,7 +1160,8 @@ var ShoppingList = class {
|
|
|
1158
1160
|
this.categorize();
|
|
1159
1161
|
}
|
|
1160
1162
|
/**
|
|
1161
|
-
* Removes a recipe from the shopping list
|
|
1163
|
+
* Removes a recipe from the shopping list, then automatically
|
|
1164
|
+
* recalculates the quantities and recategorize the ingredients.s
|
|
1162
1165
|
* @param index - The index of the recipe to remove.
|
|
1163
1166
|
*/
|
|
1164
1167
|
remove_recipe(index) {
|
|
@@ -1170,31 +1173,35 @@ var ShoppingList = class {
|
|
|
1170
1173
|
this.categorize();
|
|
1171
1174
|
}
|
|
1172
1175
|
/**
|
|
1173
|
-
* Sets the
|
|
1174
|
-
*
|
|
1176
|
+
* Sets the category configuration for the shopping list
|
|
1177
|
+
* and automatically categorize current ingredients from the list.
|
|
1178
|
+
* @param config - The category configuration to parse.
|
|
1175
1179
|
*/
|
|
1176
|
-
|
|
1177
|
-
|
|
1180
|
+
set_category_config(config) {
|
|
1181
|
+
if (typeof config === "string")
|
|
1182
|
+
this.category_config = new CategoryConfig(config);
|
|
1183
|
+
else if (config instanceof CategoryConfig) this.category_config = config;
|
|
1184
|
+
else throw new Error("Invalid category configuration");
|
|
1178
1185
|
this.categorize();
|
|
1179
1186
|
}
|
|
1180
1187
|
/**
|
|
1181
1188
|
* Categorizes the ingredients in the shopping list
|
|
1182
|
-
* Will use the
|
|
1189
|
+
* Will use the category config if any, otherwise all ingredients will be placed in the "other" category
|
|
1183
1190
|
*/
|
|
1184
1191
|
categorize() {
|
|
1185
|
-
if (!this.
|
|
1192
|
+
if (!this.category_config) {
|
|
1186
1193
|
this.categories = { other: this.ingredients };
|
|
1187
1194
|
return;
|
|
1188
1195
|
}
|
|
1189
1196
|
const categories = { other: [] };
|
|
1190
|
-
for (const category of this.
|
|
1197
|
+
for (const category of this.category_config.categories) {
|
|
1191
1198
|
categories[category.name] = [];
|
|
1192
1199
|
}
|
|
1193
1200
|
for (const ingredient of this.ingredients) {
|
|
1194
1201
|
let found = false;
|
|
1195
|
-
for (const category of this.
|
|
1196
|
-
for (const
|
|
1197
|
-
if (
|
|
1202
|
+
for (const category of this.category_config.categories) {
|
|
1203
|
+
for (const categoryIngredient of category.ingredients) {
|
|
1204
|
+
if (categoryIngredient.aliases.includes(ingredient.name)) {
|
|
1198
1205
|
categories[category.name].push(ingredient);
|
|
1199
1206
|
found = true;
|
|
1200
1207
|
break;
|
|
@@ -1213,7 +1220,7 @@ var ShoppingList = class {
|
|
|
1213
1220
|
};
|
|
1214
1221
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1215
1222
|
0 && (module.exports = {
|
|
1216
|
-
|
|
1223
|
+
CategoryConfig,
|
|
1217
1224
|
Recipe,
|
|
1218
1225
|
Section,
|
|
1219
1226
|
ShoppingList
|