@tmlmt/cooklang-parser
Version:
Cooklang parsers and utilities
610 lines (603 loc) • 18.1 kB
text/typescript
/**
* Represents a recipe section
*
* Wrapped as a _Class_ and not defined as a simple _Type_ to expose some useful helper
* classes (e.g. {@link Section.isBlank | isBlank()})
*
* @category Types
*/
declare class Section {
/**
* The name of the section. Can be an empty string for the default (first) section.
* @defaultValue `""`
*/
name: string;
/** An array of steps and notes that make up the content of the section. */
content: (Step | Note)[];
/**
* Creates an instance of Section.
* @param name - The name of the section. Defaults to an empty string.
*/
constructor(name?: string);
/**
* Checks if the section is blank (has no name and no content).
* Used during recipe parsing
* @returns `true` if the section is blank, otherwise `false`.
*/
isBlank(): boolean;
}
/**
* Recipe parser.
*
* ## Usage
*
* You can either directly provide the recipe string when creating the instance
* e.g. `const recipe = new Recipe('Add @eggs{3}')`, or create it first and then pass
* the recipe string to the {@link Recipe.parse | parse()} method.
*
* Look at the [properties](#properties) to see how the recipe's properties are parsed.
*
* @category Classes
*
* @example
* ```typescript
* import { Recipe } from "@tmlmt/cooklang-parser";
*
* const recipeString = `
* ---
* title: Pancakes
* tags: [breakfast, easy]
* ---
* Crack the @eggs{3} with @flour{100%g} and @milk{200%mL}
*
* Melt some @butter{50%g} in a #pan on medium heat.
*
* Cook for ~{5%minutes} on each side.
* `
* const recipe = new Recipe(recipeString);
* ```
*/
declare class Recipe {
/**
* The parsed recipe metadata.
*/
metadata: Metadata;
/**
* The parsed recipe ingredients.
*/
ingredients: Ingredient[];
/**
* The parsed recipe sections.
*/
sections: Section[];
/**
* The parsed recipe cookware.
*/
cookware: Cookware[];
/**
* The parsed recipe timers.
*/
timers: Timer[];
/**
* The parsed recipe servings. Used for scaling. Parsed from one of
* {@link Metadata.servings}, {@link Metadata.yield} or {@link Metadata.serves}
* metadata fields.
*
* @see {@link Recipe.scaleBy | scaleBy()} and {@link Recipe.scaleTo | scaleTo()} methods
*/
servings?: number;
/**
* Creates a new Recipe instance.
* @param content - The recipe content to parse.
*/
constructor(content?: string);
/**
* Parses a recipe from a string.
* @param content - The recipe content to parse.
*/
parse(content: string): void;
/**
* Scales the recipe to a new number of servings. In practice, it calls
* {@link Recipe.scaleBy | scaleBy} with a factor corresponding to the ratio between `newServings`
* and the recipe's {@link Recipe.servings | servings} value.
* @param newServings - The new number of servings.
* @returns A new Recipe instance with the scaled ingredients.
* @throws `Error` if the recipe does not contains an initial {@link Recipe.servings | servings} value
*/
scaleTo(newServings: number): Recipe;
/**
* Scales the recipe by a factor.
* @param factor - The factor to scale the recipe by.
* @returns A new Recipe instance with the scaled ingredients.
*/
scaleBy(factor: number): Recipe;
/**
* Gets the number of servings for the recipe.
* @private
* @returns The number of servings, or undefined if not set.
*/
private getServings;
/**
* Clones the recipe.
* @returns A new Recipe instance with the same properties.
*/
clone(): Recipe;
}
interface Quantity {
value: FixedValue | Range;
unit?: string;
}
/**
* Represents the metadata of a recipe.
* @category Types
*/
interface Metadata {
/** The title of the recipe. */
title?: string;
/** The tags of the recipe. */
tags?: string[];
/** The source of the recipe. */
source?: string;
/** The source name of the recipe. */
"source.name"?: string;
/** The source url of the recipe. */
"source.url"?: string;
/** The source author of the recipe. */
"source.author"?: string;
/** The author of the recipe. */
author?: string;
/** The number of servings the recipe makes.
* Should be either a number or a string which starts with a number
* (which will be used for scaling) followed by a comma and then
* whatever you want.
*
* Interchangeable with `yield` or `serves`. If multiple ones are defined,
* the prevailance order for the number which will used for scaling
* is `servings` \> `yield` \> `serves`.
*
* @example
* ```yaml
* servings: 4
* ```
*
* @example
* ```yaml
* servings: 2, a few
* ```
*/
servings?: number | string;
/** The yield of the recipe.
* Should be either a number or a string which starts with a number
* (which will be used for scaling) followed by a comma and then
* whatever you want.
*
* Interchangeable with `servings` or `serves`. If multiple ones are defined,
* the prevailance order for the number which will used for scaling
* is `servings` \> `yield` \> `serves`. See {@link Metadata.servings | servings}
* for examples.
*/
yield?: number | string;
/** The number of people the recipe serves.
* Should be either a number or a string which starts with a number
* (which will be used for scaling) followed by a comma and then
* whatever you want.
*
* Interchangeable with `servings` or `yield`. If multiple ones are defined,
* the prevailance order for the number which will used for scaling
* is `servings` \> `yield` \> `serves`. See {@link Metadata.servings | servings}
* for examples.
*/
serves?: number | string;
/** The course of the recipe. */
course?: string;
/** The category of the recipe. */
category?: string;
/** The locale of the recipe. */
locale?: string;
/**
* The preparation time of the recipe.
* Will not be further parsed into any DateTime format nor normalize
*/
"prep time"?: string;
/**
* Alias of `prep time`
*/
"time.prep"?: string;
/**
* The cooking time of the recipe.
* Will not be further parsed into any DateTime format nor normalize
*/
"cook time"?: string;
/**
* Alias of `cook time`
*/
"time.cook"?: string;
/**
* The total time of the recipe.
* Will not be further parsed into any DateTime format nor normalize
*/
"time required"?: string;
time?: string;
duration?: string;
/** The difficulty of the recipe. */
difficulty?: string;
/** The cuisine of the recipe. */
cuisine?: string;
/** The diet of the recipe. */
diet?: string;
/** The description of the recipe. */
description?: string;
/** The images of the recipe. Alias of `pictures` */
images?: string[];
/** The images of the recipe. Alias of `images` */
pictures?: string[];
/** The picture of the recipe. Alias of `picture` */
image?: string;
/** The picture of the recipe. Alias of `image` */
picture?: string;
/** The introduction of the recipe. */
introduction?: string;
}
/**
* Represents a quantity described by text, e.g. "a pinch"
* @category Types
*/
interface TextValue {
type: "text";
value: string;
}
/**
* Represents a quantity described by a decimal number, e.g. "1.5"
* @category Types
*/
interface DecimalValue {
type: "decimal";
value: number;
}
/**
* Represents a quantity described by a fraction, e.g. "1/2"
* @category Types
*/
interface FractionValue {
type: "fraction";
/** The numerator of the fraction */
num: number;
/** The denominator of the fraction */
den: number;
}
/**
* Represents a single, fixed quantity.
* This can be a text, decimal, or fraction.
* @category Types
*/
interface FixedValue {
type: "fixed";
value: TextValue | DecimalValue | FractionValue;
}
/**
* Represents a range of quantities, e.g. "1-2"
* @category Types
*/
interface Range {
type: "range";
min: DecimalValue | FractionValue;
max: DecimalValue | FractionValue;
}
/**
* Represents a contributor to an ingredient's total quantity
* @category Types
*/
interface QuantityPart extends Quantity {
/** - If _true_, the quantity will scale
* - If _false_, the quantity is fixed
*/
scalable: boolean;
}
/**
* Represents a possible state modifier or other flag for an ingredient in a recipe
* @category Types
*/
type IngredientFlag = "optional" | "hidden" | "recipe";
/**
* Represents the collection of possible additional metadata for an ingredient in a recipe
* @category Types
*/
interface IngredientExtras {
/**
* The path of the ingredient-recipe, relative to the present recipe
* Used if: the ingredient is a recipe
*
* @example
* ```cooklang
* Take @./essentials/doughs/pizza dough{1} out of the freezer and let it unfreeze overnight
* ```
* Would lead to:
* ```yaml
* path: 'essentials/doughts/pizza dough.cook'
* ```
*/
path: string;
}
/**
* Represents an ingredient in a recipe.
* @category Types
*/
interface Ingredient {
/** The name of the ingredient. */
name: string;
/** The quantity of the ingredient. */
quantity?: FixedValue | Range;
/** The unit of the ingredient. */
unit?: string;
/** The array of contributors to the ingredient's total quantity. */
quantityParts?: QuantityPart[];
/** The preparation of the ingredient. */
preparation?: string;
/** A list of potential state modifiers or other flags for the ingredient */
flags?: IngredientFlag[];
/** The collection of potential additional metadata for the ingredient */
extras?: IngredientExtras;
}
/**
* Represents a timer in a recipe.
* @category Types
*/
interface Timer {
/** The name of the timer. */
name?: string;
/** The duration of the timer. */
duration: FixedValue | Range;
/** The unit of the timer. */
unit: string;
}
/**
* Represents a text item in a recipe step.
* @category Types
*/
interface TextItem {
/** The type of the item. */
type: "text";
/** The content of the text item. */
value: string;
}
/**
* Represents an ingredient item in a recipe step.
* @category Types
*/
interface IngredientItem {
/** The type of the item. */
type: "ingredient";
/** The index of the ingredient, within the {@link Recipe.ingredients | list of ingredients} */
index: number;
/** Index of the quantity part corresponding to this item / this occurence
* of the ingredient, which may be referenced elsewhere. */
quantityPartIndex?: number;
/** The alias/name of the ingredient as it should be displayed in the preparation
* for this occurence */
displayName: string;
}
/**
* Represents a cookware item in a recipe step.
* @category Types
*/
interface CookwareItem {
/** The type of the item. */
type: "cookware";
/** The index of the cookware, within the {@link Recipe.cookware | list of cookware} */
index: number;
/** Index of the quantity part corresponding to this item / this occurence
* of the cookware, which may be referenced elsewhere. */
quantityPartIndex?: number;
}
/**
* Represents a timer item in a recipe step.
* @category Types
*/
interface TimerItem {
/** The type of the item. */
type: "timer";
/** The index of the timer, within the {@link Recipe.timers | list of timers} */
index: number;
}
/**
* Represents an item in a recipe step.
* @category Types
*/
type Item = TextItem | IngredientItem | CookwareItem | TimerItem;
/**
* Represents a step in a recipe.
* @category Types
*/
interface Step {
type: "step";
/** The items in the step. */
items: Item[];
}
/**
* Represents a note in a recipe.
* @category Types
*/
interface Note {
type: "note";
/** The content of the note. */
note: string;
}
/**
* Represents a possible state modifier or other flag for cookware used in a recipe
* @category Types
*/
type CookwareFlag = "optional" | "hidden";
/**
* Represents a piece of cookware in a recipe.
* @category Types
*/
interface Cookware {
/** The name of the cookware. */
name: string;
/** The quantity of cookware */
quantity?: FixedValue | Range;
/** The array of contributors to the cookware's total quantity. */
quantityParts?: (FixedValue | Range)[];
/** A list of potential state modifiers or other flags for the cookware */
flags: CookwareFlag[];
}
/**
* Represents categorized ingredients.
* @category Types
*/
interface CategorizedIngredients {
[category: string]: Ingredient[];
}
/**
* Represents a recipe that has been added to a shopping list.
* @category Types
*/
interface AddedRecipe {
/** The recipe that was added. */
recipe: Recipe;
/** The factor the recipe was scaled by. */
factor: number;
}
/**
* Represents an ingredient in a category.
* @category Types
*/
interface CategoryIngredient {
/** The name of the ingredient. */
name: string;
/** The aliases of the ingredient. */
aliases: string[];
}
/**
* Represents a category of ingredients.
* @category Types
*/
interface Category {
/** The name of the category. */
name: string;
/** The ingredients in the category. */
ingredients: CategoryIngredient[];
}
/**
* Parser for category configurations specified à-la-cooklang.
*
* ## Usage
*
* You can either directly provide the category configuration string when creating the instance
* e.g. `const categoryConfig = new CategoryConfig(<...>)`, or create it first and then pass
* the category configuration string to the {@link CategoryConfig.parse | parse()} method.
*
* The initialized `CategoryConfig` can then be fed to a {@link ShoppingList}
*
* @example
* ```typescript
* import { CategoryConfig } from @tmlmt/cooklang-parser;
*
* const categoryConfigString = `
* [Dairy]
* milk
* butter
*
* [Bakery]
* flour
* sugar`;
*
* const categoryConfig = new CategoryConfig(categoryConfigString);
* ```
*
* @see [Category Configuration](https://cooklang.org/docs/spec/#shopping-lists) section of the cooklang specs
*
* @category Classes
*/
declare class CategoryConfig {
/**
* The parsed categories of ingredients.
*/
categories: Category[];
/**
* Creates a new CategoryConfig instance.
* @param config - The category configuration to parse.
*/
constructor(config?: string);
/**
* Parses a category configuration from a string into property
* {@link CategoryConfig.categories | categories}
* @param config - The category configuration to parse.
*/
parse(config: string): void;
}
/**
* Shopping List generator.
*
* ## Usage
*
* - Create a new ShoppingList instance with an optional category configuration (see {@link ShoppingList."constructor" | constructor})
* - Add recipes, scaling them as needed (see {@link ShoppingList.add_recipe | add_recipe()})
* - Categorize the ingredients (see {@link ShoppingList.categorize | categorize()})
*
* @example
*
* ```typescript
* import * as fs from "fs";
* import { ShoppingList } from @tmlmt/cooklang-parser;
*
* const categoryConfig = fs.readFileSync("./myconfig.txt", "utf-8")
* const recipe1 = new Recipe(fs.readFileSync("./myrecipe.cook", "utf-8"));
* const shoppingList = new ShoppingList();
* shoppingList.set_category_config(categoryConfig);
* // Quantities are automatically calculated and ingredients categorized
* // when adding a recipe
* shoppingList.add_recipe(recipe1);
* ```
*
* @category Classes
*/
declare class ShoppingList {
/**
* The ingredients in the shopping list.
*/
ingredients: Ingredient[];
/**
* The recipes in the shopping list.
*/
recipes: AddedRecipe[];
/**
* The category configuration for the shopping list.
*/
category_config?: CategoryConfig;
/**
* The categorized ingredients in the shopping list.
*/
categories?: CategorizedIngredients;
/**
* Creates a new ShoppingList instance
* @param category_config_str - The category configuration to parse.
*/
constructor(category_config_str?: string | CategoryConfig);
private calculate_ingredients;
/**
* Adds a recipe to the shopping list, then automatically
* recalculates the quantities and recategorize the ingredients.
* @param recipe - The recipe to add.
* @param factor - The factor to scale the recipe by.
*/
add_recipe(recipe: Recipe, factor?: number): void;
/**
* Removes a recipe from the shopping list, then automatically
* recalculates the quantities and recategorize the ingredients.s
* @param index - The index of the recipe to remove.
*/
remove_recipe(index: number): void;
/**
* Sets the category configuration for the shopping list
* and automatically categorize current ingredients from the list.
* @param config - The category configuration to parse.
*/
set_category_config(config: string | CategoryConfig): void;
/**
* Categorizes the ingredients in the shopping list
* Will use the category config if any, otherwise all ingredients will be placed in the "other" category
*/
categorize(): void;
}
export { type AddedRecipe, type CategorizedIngredients, type Category, CategoryConfig, type CategoryIngredient, type Cookware, type CookwareFlag, type CookwareItem, type DecimalValue, type FixedValue, type FractionValue, type Ingredient, type IngredientExtras, type IngredientFlag, type IngredientItem, type Item, type Metadata, type Note, type QuantityPart, type Range, Recipe, Section, ShoppingList, type Step, type TextItem, type TextValue, type Timer, type TimerItem };