182 lines
4.1 KiB
TypeScript
182 lines
4.1 KiB
TypeScript
import {
|
|
type DocumentChild,
|
|
getTextOfChild,
|
|
getTextOfRange,
|
|
parseDocument,
|
|
renderMarkdown,
|
|
} from "@lib/documents.ts";
|
|
import { parse } from "yaml";
|
|
import { parseIngredient } from "https://esm.sh/parse-ingredient";
|
|
import { createCrud } from "@lib/crud.ts";
|
|
|
|
export type IngredientGroup = {
|
|
name: string;
|
|
ingredients: Ingredient[];
|
|
};
|
|
|
|
export type Ingredient = {
|
|
type: string;
|
|
unit?: string;
|
|
amount?: string;
|
|
};
|
|
|
|
export type Ingredients = (Ingredient | IngredientGroup)[];
|
|
|
|
export type Recipe = {
|
|
id: string;
|
|
meta?: {
|
|
link?: string;
|
|
image?: string;
|
|
rating?: number;
|
|
portion?: number;
|
|
};
|
|
name: string;
|
|
description?: string;
|
|
ingredients: Ingredients;
|
|
preparation?: string;
|
|
};
|
|
|
|
function parseIngredientItem(listItem: DocumentChild): Ingredient | undefined {
|
|
if (listItem.type === "listItem") {
|
|
const children: DocumentChild[] = listItem.children[0]?.children ||
|
|
listItem.children;
|
|
|
|
const text = children.map((c) => getTextOfChild(c)).join(" ").trim();
|
|
|
|
const ing = parseIngredient(text, {
|
|
additionalUOMs: {
|
|
tableSpoon: {
|
|
short: "EL",
|
|
plural: "Table Spoons",
|
|
alternates: ["el", "EL", "Tbsp", "tbsp"],
|
|
},
|
|
teaSpoon: {
|
|
short: "TL",
|
|
plural: "Tea Spoon",
|
|
alternates: ["tl", "TL", "Tsp", "tsp"],
|
|
},
|
|
litre: {
|
|
short: "L",
|
|
plural: "liters",
|
|
alternates: ["L", "l"],
|
|
},
|
|
paket: {
|
|
short: "Paket",
|
|
plural: "Pakets",
|
|
alternates: ["Paket", "paket"],
|
|
},
|
|
},
|
|
});
|
|
|
|
return {
|
|
type: ing[0].description,
|
|
unit: ing[0].unitOfMeasure,
|
|
amount: ing[0].quantity,
|
|
};
|
|
}
|
|
return;
|
|
}
|
|
|
|
const isIngredient = (item: Ingredient | undefined): item is Ingredient => {
|
|
return !!item;
|
|
};
|
|
|
|
function parseIngredientsList(list: DocumentChild): Ingredient[] {
|
|
if (list.type === "list" && "children" in list) {
|
|
return list.children.map((listItem) => {
|
|
return parseIngredientItem(listItem);
|
|
}).filter(isIngredient);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function parseIngredients(children: DocumentChild[]): Recipe["ingredients"] {
|
|
const ingredients: (Ingredient | IngredientGroup)[] = [];
|
|
if (!children) return [];
|
|
let skip = false;
|
|
for (let i = 0; i < children.length; i++) {
|
|
if (skip) {
|
|
skip = false;
|
|
continue;
|
|
}
|
|
const child = children[i];
|
|
|
|
if (child.type === "paragraph") {
|
|
const nextChild = children[i + 1];
|
|
|
|
if (nextChild.type !== "list") continue;
|
|
|
|
ingredients.push({
|
|
name: getTextOfChild(child) || "",
|
|
ingredients: parseIngredientsList(nextChild),
|
|
});
|
|
skip = true;
|
|
continue;
|
|
}
|
|
|
|
if (child.type === "list") {
|
|
ingredients.push(...parseIngredientsList(child));
|
|
}
|
|
}
|
|
|
|
return ingredients;
|
|
}
|
|
|
|
export function parseRecipe(original: string, id: string): Recipe {
|
|
const doc = parseDocument(original);
|
|
|
|
let name = "";
|
|
let meta: Recipe["meta"] = {};
|
|
|
|
const groups: DocumentChild[][] = [];
|
|
let group: DocumentChild[] = [];
|
|
for (const child of doc.children) {
|
|
if (child.type === "yaml") {
|
|
meta = parse(child.value) as Recipe["meta"];
|
|
continue;
|
|
}
|
|
if (
|
|
child.type === "heading" && child.depth === 1 && !name &&
|
|
child.children.length === 1 && child.children[0].type === "text"
|
|
) {
|
|
name = child.children[0].value;
|
|
continue;
|
|
}
|
|
if (child.type === "thematicBreak") {
|
|
groups.push(group);
|
|
group = [];
|
|
continue;
|
|
}
|
|
group.push(child);
|
|
}
|
|
|
|
if (group.length) {
|
|
groups.push(group);
|
|
}
|
|
|
|
const description = getTextOfRange(groups[0], original);
|
|
|
|
const ingredients = parseIngredients(groups[1]);
|
|
|
|
const preparation = getTextOfRange(groups[2], original);
|
|
|
|
return {
|
|
id,
|
|
meta,
|
|
name,
|
|
description: description ? renderMarkdown(description) : "",
|
|
ingredients,
|
|
preparation: preparation ? renderMarkdown(preparation) : "",
|
|
};
|
|
}
|
|
|
|
const crud = createCrud<Recipe>({
|
|
prefix: `Recipes/`,
|
|
parse: parseRecipe,
|
|
});
|
|
|
|
export const getAllRecipes = crud.readAll;
|
|
export const getRecipe = crud.read;
|
|
export const updateRecipe = crud.update;
|
|
export const createRecipe = crud.create;
|