@alexanderson1993/cooklang-ts
Version:
Cooklang-TS is a TypeScript library for parsing and manipulating Cooklang recipes.
208 lines • 8.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const tokens_1 = require("./tokens");
class Parser {
constructor(options) {
this.defaultUnits = "";
this.defaultCookwareAmount = options?.defaultCookwareAmount ?? 1;
this.defaultIngredientAmount = options?.defaultIngredientAmount ?? "some";
this.includeStepNumber = options?.includeStepNumber ?? false;
}
parse(source) {
const ingredients = [];
const cookwares = [];
const metadata = {};
const steps = [];
const shoppingList = {};
source = source.replace(tokens_1.comment, "").replace(tokens_1.blockComment, " ");
for (let match of source.matchAll(tokens_1.shoppingList)) {
const groups = createNamedGroups(match, "shoppingList");
if (!groups?.name)
continue;
shoppingList[groups.name] = parseShoppingListCategory(groups.items || "");
source = source.substring(0, match.index || 0);
+source.substring((match.index || 0) + match[0].length);
}
const lines = source.split(/\r?\n/).filter((l) => l.trim().length > 0);
let stepNumber = 0;
stepLoop: for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const step = [];
let pos = 0;
for (let match of line.matchAll(tokens_1.tokens)) {
const metadataGroups = createNamedGroups(match, "metadata");
if (metadataGroups?.key && metadataGroups.value) {
metadata[metadataGroups.key.trim()] = metadataGroups.value.trim();
continue stepLoop;
}
if (pos < (match.index || 0)) {
step.push({
type: "text",
value: line.substring(pos, match.index),
});
}
const sIngredientGroup = createNamedGroups(match, "singleWordIngredient");
if (sIngredientGroup?.sIngredientName) {
const ingredient = {
type: "ingredient",
name: sIngredientGroup.sIngredientName,
quantity: this.defaultIngredientAmount,
units: this.defaultUnits,
};
if (this.includeStepNumber)
ingredient.step = stepNumber;
ingredients.push(ingredient);
step.push(ingredient);
}
const mIngredientGroup = createNamedGroups(match, "multiwordIngredient");
if (mIngredientGroup?.mIngredientName) {
const ingredient = {
type: "ingredient",
name: mIngredientGroup.mIngredientName,
quantity: parseQuantity(mIngredientGroup.mIngredientQuantity) ?? this.defaultIngredientAmount,
units: parseUnits(mIngredientGroup.mIngredientUnits) ?? this.defaultUnits,
...(mIngredientGroup.mIngredientPreparation
? { preparation: mIngredientGroup.mIngredientPreparation }
: null),
...(mIngredientGroup.mIngredientReference
? { reference: mIngredientGroup.mIngredientReference }
: null),
};
if (this.includeStepNumber)
ingredient.step = stepNumber;
ingredients.push(ingredient);
step.push(ingredient);
}
const sCookwareGroup = createNamedGroups(match, "singleWordCookware");
if (sCookwareGroup?.sCookwareName) {
const cookware = {
type: "cookware",
name: sCookwareGroup.sCookwareName,
quantity: this.defaultCookwareAmount,
};
if (this.includeStepNumber)
cookware.step = stepNumber;
cookwares.push(cookware);
step.push(cookware);
}
const mCookwareGroup = createNamedGroups(match, "multiwordCookware");
if (mCookwareGroup?.mCookwareName) {
const cookware = {
type: "cookware",
name: mCookwareGroup?.mCookwareName,
quantity: parseQuantity(mCookwareGroup?.mCookwareQuantity) ?? this.defaultCookwareAmount,
};
if (this.includeStepNumber)
cookware.step = stepNumber;
cookwares.push(cookware);
step.push(cookware);
}
const timerGroup = createNamedGroups(match, "timer");
if (timerGroup?.timerQuantity) {
step.push({
type: "timer",
name: timerGroup.timerName,
quantity: parseQuantity(timerGroup.timerQuantity) ?? 0,
units: parseUnits(timerGroup.timerUnits) ?? this.defaultUnits,
});
}
pos = (match.index || 0) + match[0].length;
}
if (pos < line.length) {
step.push({
type: "text",
value: line.substring(pos),
});
}
if (step.length > 0) {
steps.push(step);
stepNumber++;
}
}
return { ingredients, cookwares, metadata, steps, shoppingList };
}
}
exports.default = Parser;
function parseQuantity(quantity) {
if (!quantity || quantity.trim() === "") {
return undefined;
}
quantity = quantity.trim();
const [left, right] = quantity.split("/");
const [numLeft, numRight] = [Number(left), Number(right)];
if (right && isNaN(numRight))
return quantity;
if (!isNaN(numLeft) && !numRight)
return numLeft;
else if (!isNaN(numLeft) && !isNaN(numRight) && !(left.startsWith("0") || right.startsWith("0")))
return numLeft / numRight;
return quantity.trim();
}
function parseUnits(units) {
if (!units || units.trim() === "") {
return undefined;
}
return units.trim();
}
function parseShoppingListCategory(items) {
const list = [];
for (let item of items.split("\n")) {
item = item.trim();
if (item == "")
continue;
const [name, synonym] = item.split("|");
list.push({
name: name.trim(),
synonym: synonym?.trim() || "",
});
}
return list;
}
function createNamedGroups(match, type) {
if (!match)
return null;
const groupMappings = {
metadata: {
key: 1,
value: 2,
},
multiwordIngredient: {
mIngredientName: 3,
mIngredientQuantity: 4,
mIngredientUnits: 5,
mIngredientPreparation: 6,
mIngredientReference: 7,
},
singleWordIngredient: {
sIngredientName: 8,
},
multiwordCookware: {
mCookwareName: 9,
mCookwareQuantity: 10,
},
singleWordCookware: {
sCookwareName: 11,
},
timer: {
timerName: 12,
timerQuantity: 13,
timerUnits: 14,
},
shoppingList: {
name: 15,
items: 16,
},
};
const mapping = groupMappings[type];
if (!mapping) {
throw new Error(`Unknown regex type: ${type}`);
}
const groups = {};
for (const [groupName, index] of Object.entries(mapping)) {
if (match[index] !== undefined) {
groups[groupName] = match[index];
}
}
return groups;
}
//# sourceMappingURL=Parser.js.map