Rename directory "engines" to "languages"

This commit is contained in:
Nilay Majorwar
2022-01-30 20:47:33 +05:30
parent 0bf7c0de3a
commit e3be5a8a83
82 changed files with 27 additions and 21 deletions

View File

@ -0,0 +1,31 @@
import { ChefArithmeticOp } from "../types";
/** Ingredient measures considered as dry */
export const DryMeasures = ["g", "kg", "pinch", "pinches"];
/** Ingredient measures considered as liquid */
export const LiquidMeasures = ["ml", "l", "dash", "dashes"];
/** Ingredient measures that may be dry or liquid */
export const UnknownMeasures = [
"cup",
"cups",
"teaspoon",
"teaspoons",
"tablespoon",
"tablespoons",
];
/** Types of measures - irrelevant to execution */
export const MeasureTypes = ["heaped", "level"];
/** A map from arithmetic instruction verbs to op codes */
export const ArithmeticCodes: { [k: string]: ChefArithmeticOp["code"] } = {
Add: "ADD",
Remove: "SUBTRACT",
Combine: "MULTIPLY",
Divide: "DIVIDE",
};
/** Placeholder value for loop jump addresses */
export const JumpAddressPlaceholder = -1;

View File

@ -0,0 +1,240 @@
import {
ArithmeticCodes,
DryMeasures,
JumpAddressPlaceholder,
LiquidMeasures,
MeasureTypes,
UnknownMeasures,
} from "./constants";
import { SyntaxError } from "../constants";
import * as R from "./regex";
import * as C from "../types";
/**
* Ideally, this would convert past form of verb to present form. Due to
* the requirement of an English dictionary for sufficient accuracy, we instead
* require the past form to be the same as present form in Esolang Park. Thus,
* this function is currently a no-op.
*
* @param verbed Past form of verb
* @returns Present imperative form of verb
*/
const toPresentTense = (verbed: string) => {
return verbed;
};
/** Parse a string as an ingredient measure */
const parseMeasure = (measure: string): C.StackItemType | undefined => {
if (DryMeasures.includes(measure)) return "dry";
if (LiquidMeasures.includes(measure)) return "liquid";
if (UnknownMeasures.includes(measure)) return "unknown";
};
/** Validate and parse string as integer. Empty string is treated as 1 */
const parseIndex = (str: string): number => {
if (!str || str.trim().length === 0) return 1;
const parsed = parseInt(str.trim(), 10);
if (Number.isNaN(parsed)) throw new SyntaxError("Not a number");
return parsed;
};
/** Parse a string as an ordinal identifier (1st, 2nd, etc) */
const parseOrdinal = (measure: string): number => {
if (!measure || measure.trim().length === 0) return 1;
const parsed = parseInt(measure.trim(), 10);
if (Number.isNaN(parsed))
throw new SyntaxError("Invalid dish/bowl identifier");
return parsed;
};
/** Parse a line as an arithmetic operation in Chef */
const parseArithmeticOp = (line: string): C.ChefArithmeticOp => {
const matches = assertMatch(line, R.ArithmeticOpRegex);
const code = ArithmeticCodes[matches[1]];
const bowlId = parseIndex(matches[4]);
// If mixing bowl segment is entirely missing...
if (!matches[3]) return { code, ing: matches[2], bowlId };
// Case-wise checks for each operation
if (
(matches[1] === "Add" && matches[3] === "to") ||
(matches[1] === "Remove" && matches[3] === "from") ||
(matches[1] === "Combine" && matches[3] === "into") ||
(matches[1] === "Divide" && matches[3] === "into")
)
return { code, ing: matches[2], bowlId };
throw new SyntaxError("Instruction has incorrect syntax");
};
/** Assert that a line matches the given regex and return matches */
const assertMatch = (line: string, regex: RegExp): RegExpMatchArray => {
const matches = line.match(regex);
if (!matches) throw new SyntaxError("Unknown instruction");
return matches;
};
/**
* Parse a line as the definition of an ingredient in Chef
* @param line Line to be parsed as ingredient definition
* @returns Ingredient definition in parsed form
*/
export const parseIngredientItem = (
line: string
): { name: C.IngredientName; item: C.IngredientItem } => {
const words = line.trim().split(/\s+/).reverse();
// Try to parse the first word as a number
const parsedValue = parseInt(words[words.length - 1], 10);
const quantity = Number.isNaN(parsedValue) ? undefined : parsedValue;
if (quantity != null) words.pop();
// Try to parse next word as measure type (heaped/level)
const measureType = words[words.length - 1];
const hasMeasureType = MeasureTypes.includes(measureType);
if (hasMeasureType) words.pop();
// Parse next word as measurement unit
const measure = parseMeasure(words[words.length - 1]);
if (hasMeasureType && !measure) throw new SyntaxError("Invalid measure");
if (measure) words.pop();
// Parse rest of word as name of ingredient
const ingredientName = words.reverse().join(" ");
// Return parsed ingredient item
return {
name: ingredientName,
item: { type: measure || "unknown", value: quantity },
};
};
/**
* Parse a line as a single instruction of a Chef recipe.
*
* Note that loop-closer and loop-opener addresses are inserted as -1 as this function
* does not have scope of the entire method and loop stack. These addresses must be modified
* by the caller by tracking loop statements.
*
* @param line Line containing instruction, ending just before period.
*/
export const parseMethodStep = (line: string): C.ChefOperation => {
if (line.startsWith("Take ")) {
// Take `ingredient` from refrigerator
const matches = assertMatch(line, R.TakeFromFridgeRegex);
return { code: "STDIN", ing: matches[1] };
//========================================================================
} else if (line.startsWith("Put ")) {
// Put `ingredient` into [nth] mixing bowl
const matches = assertMatch(line, R.PutInBowlRegex);
return { code: "PUSH", ing: matches[1], bowlId: parseOrdinal(matches[2]) };
//========================================================================
} else if (line.startsWith("Fold ")) {
// Fold `ingredient` into [nth] mixing bowl
const matches = assertMatch(line, R.FoldIntoBowlRegex);
return { code: "POP", ing: matches[1], bowlId: parseOrdinal(matches[2]) };
//========================================================================
} else if (line.startsWith("Add dry ingredients")) {
// Add dry ingredients [into [nth] mixing bowl]
const matches = assertMatch(line, R.AddDryIngsOpRegex);
return { code: "ADD-DRY", bowlId: parseIndex(matches[1]) };
//========================================================================
} else if (
["Add", "Remove", "Combine", "Divide"].includes(line.split(" ", 1)[0])
) {
// Add | Remove | Combine | Divide ...
return parseArithmeticOp(line);
//========================================================================
} else if (
line.startsWith("Liquefy contents of the ") ||
line.startsWith("Liquefy the contents of the ")
) {
// Liquefy contents of the [nth] mixing bowl
const matches = assertMatch(line, R.LiquefyBowlRegex);
return { code: "LIQ-BOWL", bowlId: parseIndex(matches[1]) };
//========================================================================
} else if (line.startsWith("Liquefy ")) {
// Liquefy `ingredient`
const matches = assertMatch(line, R.LiquefyIngRegex);
return { code: "LIQ-ING", ing: matches[1] };
//========================================================================
} else if (
line.startsWith("Stir ") &&
(line.endsWith("minute") || line.endsWith("minutes"))
) {
// Stir [the [nth] mixing bowl] for `number` minutes
const matches = assertMatch(line, R.StirBowlRegex);
return {
code: "ROLL-BOWL",
bowlId: parseIndex(matches[1]),
num: parseIndex(matches[2]),
};
//========================================================================
} else if (line.startsWith("Stir ")) {
// Stir ingredient into the [nth] mixing bowl
const matches = assertMatch(line, R.StirIngredientRegex);
return {
code: "ROLL-ING",
ing: matches[1],
bowlId: parseIndex(matches[2]),
};
//========================================================================
} else if (line.startsWith("Mix ")) {
// Mix [the [nth] mixing bowl] well
const matches = assertMatch(line, R.MixBowlRegex);
return { code: "RANDOM", bowlId: parseIndex(matches[1]) };
//========================================================================
} else if (line.startsWith("Clean ")) {
// Clean [nth] mixing bowl
const matches = assertMatch(line, R.CleanBowlRegex);
return { code: "CLEAR", bowlId: parseIndex(matches[1]) };
//========================================================================
} else if (line.startsWith("Pour ")) {
// Pour contents of [nth] mixing bowl into [pth] baking dish
const matches = assertMatch(line, R.PourBowlRegex);
return {
code: "COPY",
bowlId: parseIndex(matches[1]),
dishId: parseIndex(matches[2]),
};
//========================================================================
} else if (line === "Set aside") {
// Set aside
return { code: "LOOP-BREAK", closer: JumpAddressPlaceholder };
//========================================================================
} else if (line.startsWith("Serve with ")) {
// Serve with `auxiliary recipe`
const matches = assertMatch(line, R.ServeWithRegex);
return { code: "FNCALL", recipe: matches[1] };
//========================================================================
} else if (line.startsWith("Refrigerate")) {
// Refrigerate [for `number` hours]
const matches = assertMatch(line, R.RefrigerateRegex);
const num = matches[1] ? parseIndex(matches[1]) : undefined;
return { code: "END", num };
//========================================================================
} else if (line.includes(" until ")) {
// `Verb` [the `ingredient`] until `verbed`
const matches = assertMatch(line, R.LoopEnderRegex);
const ingredient = matches[1] || undefined;
const verb = toPresentTense(matches[2]);
return {
code: "LOOP-CLOSE",
ing: ingredient,
verb,
opener: JumpAddressPlaceholder,
};
//========================================================================
} else {
// `Verb` [the] `ingredient`
const matches = assertMatch(line, R.LoopOpenerRegex);
return {
code: "LOOP-OPEN",
verb: matches[1].toLowerCase(),
ing: matches[2],
closer: JumpAddressPlaceholder,
};
}
};

View File

@ -0,0 +1,347 @@
import * as T from "../types";
import { DocumentRange } from "../../types";
import { parseIngredientItem, parseMethodStep } from "./core";
import { ParseError } from "../../worker-errors";
import { isSyntaxError, SyntaxError } from "../constants";
/**
* We parse a Chef program by creating an array containing the lines of code (with line nos)
* in reverse order. This array represents a stack with the first line at the top.
*
* Each parse step then pops and parses statements from the stack. For instance,
* the parseTitle function pops one statement from the stack and parses it for the title.
* The rest of the stack is then parsed by firther steps.
*/
/** Reversed array of lines of code, used as a stack */
type CodeStack = { line: string; row: number }[];
/** Text and location of a single method instruction */
type MethodSegment = { str: string; location: DocumentRange };
/** Parse a Chef program */
export const parseProgram = (code: string): T.ChefProgram => {
// Convert code to a reverse stack
const stack: CodeStack = code
.split("\n")
.map((l, idx) => ({ line: l, row: idx }))
.reverse();
// Location of code's last char, used for errors
const lastCharPosition = stack[0]?.line.length - 1 || 0;
const lastCharRange: DocumentRange = {
line: stack.length - 1,
charRange: { start: lastCharPosition, end: lastCharPosition + 1 },
};
// Exhaust any empty lines at the start of the program
exhaustEmptyLines(stack);
// Parse the main recipe
const mainRecipe = parseRecipe(stack, lastCharRange);
exhaustEmptyLines(stack);
// Parse any auxiliary recipes
const auxRecipes: { [k: string]: T.ChefRecipe } = {};
while (stack.length && stack[stack.length - 1].line.trim() !== "") {
const recipe = parseRecipe(stack, lastCharRange);
auxRecipes[recipe.name] = recipe;
exhaustEmptyLines(stack);
}
const program = { main: mainRecipe, auxes: auxRecipes };
validateProgram(program);
return program;
};
/** Pop all empty lines from top of the stack */
const exhaustEmptyLines = (stack: CodeStack): void => {
while (stack.length && stack[stack.length - 1].line.trim() === "")
stack.pop();
};
/**
* Parse the stack for recipe title
* @param stack Code stack to parse next line of
* @param lastCharRange Error range used if stack is found to be empty
*/
const parseTitle = (stack: CodeStack, lastCharRange: DocumentRange): string => {
const { line, row } = popCodeStack(stack, true);
if (line === null)
throw new ParseError("Expected recipe title", lastCharRange);
if (!line) throw new ParseError("Expected recipe title", { line: row });
if (!line.endsWith("."))
throw new ParseError("Recipe title must end with period", { line: row });
return line.slice(0, -1);
};
/**
* Parse the stack for an empty line
* @param stack Code stack to parse next line of
* @param lastCharRange Error range used if stack is found to be empty
*/
const parseEmptyLine = (
stack: CodeStack,
lastCharRange: DocumentRange
): void => {
const { line, row } = popCodeStack(stack, true);
if (line === null) throw new ParseError("Expected blank line", lastCharRange);
if (line) throw new ParseError("Expected blank line", { line: row });
};
/** Parse the stack for method instructions section */
const parseRecipeComments = (stack: CodeStack): void => {
while (stack[stack.length - 1].line.trim() !== "") stack.pop();
};
/** Parse the stack for the header of ingredients section */
const parseIngredientsHeader = (stack: CodeStack): void => {
const { line, row } = popCodeStack(stack, true);
if (line !== "Ingredients.")
throw new ParseError("Expected ingredients header", { line: row });
};
/** Parse the stack for ingredient definition lines */
const parseIngredientsSection = (stack: CodeStack): T.IngredientBox => {
const box: T.IngredientBox = {};
while (stack[stack.length - 1].line.trim() !== "") {
const { line, row } = popCodeStack(stack);
const { name, item } = parseIngredientItem(line!);
box[name] = item;
}
return box;
};
/** Parse stack for cooking time statement. No data is returned. */
const parseCookingTime = (stack: CodeStack): void => {
const regex = /^Cooking time: \d+ (?:hours?|minutes?).$/;
const { line, row } = popCodeStack(stack, true);
if (!line!.match(regex))
throw new ParseError("Invalid cooking time statement", { line: row });
};
/** Parse stack for oven setting statement. No data is returned. */
const parseOvenSetting = (stack: CodeStack): void => {
const regex =
/^Pre-heat oven to \d+ degrees Celsius(?: \(gas mark [\d/]+\))?.$/;
const { line, row } = popCodeStack(stack, true);
if (!line!.match(regex))
throw new ParseError("Invalid oven setting statement", { line: row });
};
/** Parse the stack for the header of method section */
const parseMethodHeader = (stack: CodeStack): void => {
const { line, row } = popCodeStack(stack, true);
if (line !== "Method.")
throw new ParseError('Expected "Method."', { line: row });
};
/** Parse the stack for method instructions section */
const parseMethodSection = (stack: CodeStack): T.ChefOpWithLocation[] => {
const loopStack: { opener: number; verb: string }[] = [];
const pendingBreaks: number[] = [];
const segments = serializeMethodOps(stack);
const ops: T.ChefOpWithLocation[] = [];
segments.forEach((segment, index) => {
try {
processMethodSegment(segment, index, ops, loopStack, pendingBreaks);
} catch (error) {
if (isSyntaxError(error))
throw new ParseError(error.message, segment.location);
else throw error;
}
});
return ops;
};
/**
* Process a single method segment
* @param segment Method segment to process
* @param index Index of segment in the method section
* @param ops List of already processed method segments
* @param loopStack Stack of currently active loops
* @param pendingBreaks Loop-breaks in the currently active loop
*/
const processMethodSegment = (
segment: MethodSegment,
index: number,
ops: T.ChefOpWithLocation[],
loopStack: { opener: number; verb: string }[],
pendingBreaks: number[]
) => {
// Parse operation and push to result
const op = parseMethodStep(segment.str);
ops.push({ op, location: segment.location });
switch (op.code) {
case "LOOP-OPEN": {
loopStack.push({ opener: index, verb: op.verb });
// `closer` will be added while handling loop-closer
break;
}
case "LOOP-BREAK": {
pendingBreaks.push(index);
// `closer` will be added while handling loop-closer
break;
}
case "LOOP-CLOSE": {
// Validate match with innermost loop
const loop = loopStack.pop()!;
if (loop.verb !== op.verb)
throw new SyntaxError(
`Loop verb mismatch: expected '${loop.verb}', found '${op.verb}'`
);
op.opener = loop.opener;
// Add jump address to loop opener
const openerOp = ops[loop.opener].op;
if (openerOp.code !== "LOOP-OPEN") throw new Error("Bad jump address");
openerOp.closer = index;
// Add jump address to intermediate loop-breaks
while (pendingBreaks.length) {
const breaker = ops[pendingBreaks.pop()!].op;
if (breaker.code !== "LOOP-BREAK")
throw new Error("Memorized op not a breaker");
breaker.closer = index;
}
break;
}
}
};
/** Parse a method section and serialize to list of instructions and corresponding document ranges */
const serializeMethodOps = (stack: CodeStack): MethodSegment[] => {
const segments: MethodSegment[] = [];
while (stack.length && stack[stack.length - 1].line.trim() !== "") {
const item = stack.pop()!;
// Find all the periods in the line
const periodIdxs: number[] = [-1];
for (let i = 0; i < item.line.length; ++i) {
if (item.line[i] === ".") periodIdxs.push(i);
}
// Parse each period-separated segment
for (let i = 0; i < periodIdxs.length - 1; ++i) {
const start = periodIdxs[i] + 1;
const end = periodIdxs[i + 1];
const range = { line: item.row, charRange: { start, end } };
segments.push({
str: item.line.slice(start, end).trim(),
location: range,
});
}
}
return segments;
};
/** Parse the stack for a "Serves N" statement */
const parseServesLine = (stack: CodeStack): T.ChefRecipeServes => {
const { line, row } = popCodeStack(stack, true);
const match = line!.match(/^Serves (\d+).$/);
if (!match) throw new ParseError("Malformed serves statement", { line: row });
return { line: row, num: parseInt(match[1], 10) };
};
/** Parse the stack for a single Chef recipe */
const parseRecipe = (
stack: CodeStack,
lastCharRange: DocumentRange
): T.ChefRecipe => {
// Title of the recipe
const title = parseTitle(stack, lastCharRange);
parseEmptyLine(stack, lastCharRange);
// Check if exists and parse recipe comments
if (stack[stack.length - 1].line.trim() !== "Ingredients.") {
parseRecipeComments(stack);
parseEmptyLine(stack, lastCharRange);
}
// Parse ingredients
parseIngredientsHeader(stack);
const ingredientBox = parseIngredientsSection(stack);
parseEmptyLine(stack, lastCharRange);
// Check if exists and parse cooking time
if (stack[stack.length - 1].line.trim().startsWith("Cooking time: ")) {
parseCookingTime(stack);
parseEmptyLine(stack, lastCharRange);
}
// Check if exists and parse oven temperature
if (stack[stack.length - 1].line.trim().startsWith("Pre-heat oven ")) {
parseOvenSetting(stack);
parseEmptyLine(stack, lastCharRange);
}
// Parse method
parseMethodHeader(stack);
const method = parseMethodSection(stack);
exhaustEmptyLines(stack);
// Check if exists and parse recipe
const serves = stack[stack.length - 1]?.line.trim().startsWith("Serves ")
? parseServesLine(stack)
: undefined;
return { name: title, ingredients: ingredientBox, method, serves };
};
/**
* Validate the provided recipe.
* - Check that ingredient names used in method are valid.
* - Check that auxiliary recipe names used ar valid.
* @param recipe Recipe to validate
* @param auxes Map of auxiliary recipes, keyed by name
*/
const validateRecipe = (
recipe: T.ChefRecipe,
auxes: T.ChefProgram["auxes"]
): void => {
for (const line of recipe.method) {
const ingName = (line.op as any).ing;
if (ingName && !recipe.ingredients[ingName])
throw new ParseError(`Invalid ingredient: ${ingName}`, line.location);
if (line.op.code === "FNCALL" && !auxes[line.op.recipe])
throw new ParseError(
`Invalid recipe name: ${line.op.recipe}`,
line.location
);
}
};
/**
* Validate all recipes in the given parsed Chef program
* @param program Chef program to validate
*/
const validateProgram = (program: T.ChefProgram): void => {
validateRecipe(program.main, program.auxes);
for (const auxName in program.auxes)
validateRecipe(program.auxes[auxName], program.auxes);
};
/**
* Utility to pop a line off the code stack.
* @param stack Code stack to pop line off
* @param trim Pass true if result should contain trimmed line
* @returns Object containing `line` and `row` of popped line
*/
const popCodeStack = (
stack: CodeStack,
trim?: boolean
): { line: string | null; row: number } => {
const item = stack.pop();
if (!item) return { line: null, row: -1 };
const line = trim ? item.line.trim() : item.line;
return { line, row: item.row };
};

View File

@ -0,0 +1,160 @@
/**
* For each regular expression below:
* - Doc comments include the details of each capture group in the regex.
* - Regex comments provide a little overview of the regex, where
* [...] denotes optional clause, <...> denotes capture group.
*/
/**
* Regular expression for `Take ingredient from refrigerator` op
* Capture groups:
* 1. **Ingredient name**: string with letters and spaces
*/
export const TakeFromFridgeRegex =
/** <Ingredient> */
/^Take ([a-zA-Z ]+?) from(?: the)? refrigerator$/;
/**
* Regular expression for `Put ingredient into nth bowl` op
* Capture groups:
* 1. **Ingredient name**: string with letters and spaces
* 2. **Mixing bowl index** (optional): integer
*/
export const PutInBowlRegex =
/** <Ingredient> [ <Bowl identifier> ] */
/^Put(?: the)? ([a-zA-Z ]+?) into(?: the)?(?: (\d+)(?:nd|rd|th|st))? mixing bowl$/;
/**
* Regular expression for `Fold ingredient into nth bowl` op
* Capture groups:
* 1. **Ingredient name**: string with letters and spaces
* 2. **Mixing bowl index** (optional): integer
*/
export const FoldIntoBowlRegex =
/** <Ingredient> [ <Bowl identifier> ] */
/^Fold(?: the)? ([a-zA-Z ]+?) into(?: the)?(?: (\d+)(?:nd|rd|th|st))? mixing bowl$/;
/**
* Regular expression to match the four main arithmetic operations in Chef.
* Capture groups:
* 1. **Operation name**: `"Add" | "Remove" | "Combine" | "Divide"`
* 2. **Ingredient name**: string with letters and spaces
* 3. **Proverb** (optional): `"to" | "into" | "from"`
* 4. **Mixing bowl index** (optional): integer
*/
export const ArithmeticOpRegex =
/** <Operation name> <Ingredient> [ <Proverb> [ <Bowl identifier> ] ] */
/^(Add|Remove|Combine|Divide) ([a-zA-Z ]+?)(?: (to|into|from)(?: the)?(?: (\d+)(?:nd|rd|th|st))? mixing bowl)?$/;
/**
* Regular expression for the `Add dry ingredients ...` op.
* Capture groups:
* 1. **Mixing bowl index** (optional): integer
*/
export const AddDryIngsOpRegex =
/** [ [ <Bowl identifier> ] ] */
/^Add dry ingredients(?: to(?: the)?(?: (\d+)(?:nd|rd|th|st))? mixing bowl)?$/;
/**
* Regular expression for the `Liquefy contents` op
* Capture groups:
* 1. **Mixing bowl index** (optional): integer
*/
export const LiquefyBowlRegex =
/** [ <Bowl identifier> ] */
/^Liquefy(?: the)? contents of the(?: (\d+)(?:nd|rd|th|st))? mixing bowl$/;
/**
* Regular expression for the `Liquefy ingredient` op
* Capture groups:
* 1. **Ingredient name**: string with letters and spaces
*/
export const LiquefyIngRegex =
/** <Ingredient> */
/^Liquefy(?: the)? ([a-zA-Z ]+?)$/;
/**
* Regular expression to match the `Stir <bowl> for <n> minutes` op.
* Capture groups:
* 1. **Mixing bowl index** (optional): integer
* 2. **Number of mins**: integer
*/
export const StirBowlRegex =
/** [ [ <Bowl identifier> ]? ]? <Number> */
/^Stir(?: the(?: (\d+)(?:nd|rd|th|st))? mixing bowl)? for (\d+) minutes?$/;
/**
* Regular expression to match the `Stir <ingredient> into [nth] mixing bowl` op.
* Capture groups:
* 1. **Ingredient name**: string with letters and spaces
* 2. **Mixing bowl index** (optional): integer
*/
export const StirIngredientRegex =
/** <Ingredient> [ <Bowl identifier> ] */
/^Stir ([a-zA-Z ]+?) into the(?: (\d+)(?:nd|rd|th|st))? mixing bowl$/;
/**
* Regular expression to match the `Mix [the [nth] mixing bowl] well` op.
* Capture groups:
* 1. **Mixing bowl index** (optional): integer
*/
export const MixBowlRegex =
/** [ [ <Bowl identifier> ]? ]? */
/^Mix(?: the(?: (\d+)(?:nd|rd|th|st))? mixing bowl)? well$/;
/**
* Regular expression for the `Clean bowl` op
* Capture groups:
* 1. **Mixing bowl index** (optional): integer
*/
export const CleanBowlRegex =
/** [ <Bowl identifier> ] */
/^Clean(?: the)?(?: (\d+)(?:nd|rd|th|st))? mixing bowl$/;
/**
* Regular expression to match the `Pour ...` op.
* Capture groups:
* 1. **Mixing bowl index** (optional): integer
* 2. **Baking dish index** (optional): integer
*/
export const PourBowlRegex =
/** [ <Bowl identifier> ]? [ <Bowl identifier> ]? */
/^Pour contents of the(?: (\d+)(?:nd|rd|th|st))? mixing bowl into the(?: (\d+)(?:nd|rd|th|st))? baking dish$/;
/**
* Regular expression to match the `Serve with` op.
* Capture groups:
* 1. **Name of aux recipe**: string with alphanumerics and spaces
*/
export const ServeWithRegex =
/** <aux recipe> */
/^Serve with ([a-zA-Z0-9 ]+)$/;
/**
* Regular expression to match the `Refrigerate` op.
* Capture groups:
* 1. **Number of hours** (optional): integer
*/
export const RefrigerateRegex =
/** [ <num of hours> ] */
/^Refrigerate(?: for (\d+) hours?)?$/;
/**
* Regular expression to match the `Verb the ingredient` op.
* Capture groups:
* 1. **Verb**: string with letters
* 2. **Ingredient name**: string with letters and spaces
*/
export const LoopOpenerRegex =
/** <Verb> <Ingredient> */
/^([a-zA-Z]+?)(?: the)? ([a-zA-Z ]+)$/;
/**
* Regular expression to match the `Verb [the ing] until verbed` op.
* Capture groups:
* 1. **Ingredient name** (optional): string with letters and spaces
* 2. **Matched verb**: string with letters
*/
export const LoopEnderRegex =
/** Verb [ <Ingredient> ] <Verbed> */
/^(?:[a-zA-Z]+?)(?: the)?(?: ([a-zA-Z ]+?))? until ([a-zA-Z]+)$/;