2023-07-26 13:47:01 +02:00
|
|
|
import {
|
|
|
|
type DocumentChild,
|
|
|
|
getTextOfChild,
|
|
|
|
getTextOfRange,
|
|
|
|
parseDocument,
|
2023-07-30 18:27:45 +02:00
|
|
|
} from "@lib/documents.ts";
|
2023-08-01 17:50:00 +02:00
|
|
|
import { parse } from "yaml";
|
2023-08-06 17:47:26 +02:00
|
|
|
import { parseIngredient } from "https://esm.sh/parse-ingredient@1.0.1";
|
2023-08-01 17:50:00 +02:00
|
|
|
import { createCrud } from "@lib/crud.ts";
|
2023-08-02 18:13:31 +02:00
|
|
|
import { extractHashTags } from "@lib/string.ts";
|
2023-07-26 13:47:01 +02:00
|
|
|
|
|
|
|
export type IngredientGroup = {
|
|
|
|
name: string;
|
|
|
|
ingredients: Ingredient[];
|
|
|
|
};
|
|
|
|
|
|
|
|
export type Ingredient = {
|
|
|
|
type: string;
|
|
|
|
unit?: string;
|
|
|
|
amount?: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type Ingredients = (Ingredient | IngredientGroup)[];
|
|
|
|
|
|
|
|
export type Recipe = {
|
2023-08-02 18:13:31 +02:00
|
|
|
type: "recipe";
|
2023-07-26 13:47:01 +02:00
|
|
|
id: string;
|
2023-08-02 18:13:31 +02:00
|
|
|
name: string;
|
|
|
|
description?: string;
|
|
|
|
ingredients: Ingredients;
|
|
|
|
preparation?: string;
|
|
|
|
tags: string[];
|
2023-07-26 13:47:01 +02:00
|
|
|
meta?: {
|
2023-08-04 23:36:35 +02:00
|
|
|
time?: string;
|
2023-07-26 13:47:01 +02:00
|
|
|
link?: string;
|
|
|
|
image?: string;
|
|
|
|
rating?: number;
|
|
|
|
portion?: number;
|
2023-08-02 18:13:31 +02:00
|
|
|
author?: string;
|
2023-08-11 16:13:20 +02:00
|
|
|
thumbnail?: string;
|
2023-07-26 13:47:01 +02:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
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",
|
2023-07-26 15:48:03 +02:00
|
|
|
alternates: ["el", "EL", "Tbsp", "tbsp"],
|
2023-07-26 13:47:01 +02:00
|
|
|
},
|
|
|
|
teaSpoon: {
|
|
|
|
short: "TL",
|
|
|
|
plural: "Tea Spoon",
|
2023-08-04 23:36:35 +02:00
|
|
|
alternates: ["tl", "TL", "Tsp", "tsp", "teaspoon"],
|
2023-07-26 13:47:01 +02:00
|
|
|
},
|
|
|
|
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") {
|
2023-08-01 17:50:00 +02:00
|
|
|
meta = parse(child.value) as Recipe["meta"];
|
2023-07-26 13:47:01 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2023-08-02 18:13:31 +02:00
|
|
|
let description = getTextOfRange(groups[0], original);
|
2023-07-26 13:47:01 +02:00
|
|
|
|
|
|
|
const ingredients = parseIngredients(groups[1]);
|
|
|
|
|
|
|
|
const preparation = getTextOfRange(groups[2], original);
|
|
|
|
|
2023-08-02 18:13:31 +02:00
|
|
|
const tags = extractHashTags(description || "");
|
|
|
|
if (description) {
|
|
|
|
for (const tag of tags) {
|
|
|
|
description = description.replace("#" + tag, "");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-26 13:47:01 +02:00
|
|
|
return {
|
2023-08-02 18:13:31 +02:00
|
|
|
type: "recipe",
|
2023-07-26 13:47:01 +02:00
|
|
|
id,
|
2023-07-30 18:27:45 +02:00
|
|
|
meta,
|
2023-07-26 13:47:01 +02:00
|
|
|
name,
|
2023-08-02 18:13:31 +02:00
|
|
|
tags,
|
2023-08-05 21:52:43 +02:00
|
|
|
description,
|
2023-07-26 13:47:01 +02:00
|
|
|
ingredients,
|
2023-08-05 21:52:43 +02:00
|
|
|
preparation,
|
2023-07-26 13:47:01 +02:00
|
|
|
};
|
|
|
|
}
|
2023-08-01 17:50:00 +02:00
|
|
|
|
|
|
|
const crud = createCrud<Recipe>({
|
|
|
|
prefix: `Recipes/`,
|
|
|
|
parse: parseRecipe,
|
2023-08-11 16:13:20 +02:00
|
|
|
hasThumbnails: true,
|
2023-08-01 17:50:00 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
export const getAllRecipes = crud.readAll;
|
|
|
|
export const getRecipe = crud.read;
|
|
|
|
export const updateRecipe = crud.update;
|
|
|
|
export const createRecipe = crud.create;
|