feat: init

This commit is contained in:
2023-07-26 13:47:01 +02:00
commit 8e461cea26
28 changed files with 732 additions and 0 deletions

73
lib/documents.ts Normal file
View File

@ -0,0 +1,73 @@
import { unified } from "npm:unified";
import remarkParse from "npm:remark-parse";
import remarkFrontmatter from "https://esm.sh/remark-frontmatter@4";
import { parse } from "https://deno.land/std@0.194.0/yaml/mod.ts";
export type Document = {
name: string;
lastModified: number;
contentType: string;
size: number;
perm: string;
};
export function parseFrontmatter(yaml: string) {
return parse(yaml);
}
export async function getDocuments(): Promise<Document[]> {
const headers = new Headers();
headers.append("Accept", "application/json");
const response = await fetch("http://192.168.178.56:3007/index.json", {
headers: headers,
});
return response.json();
}
export async function getDocument(name: string): Promise<string> {
const response = await fetch("http://192.168.178.56:3007/" + name);
return await response.text();
}
export function parseDocument(doc: string) {
return unified()
.use(remarkParse).use(remarkFrontmatter, ["yaml", "toml"])
.parse(doc);
}
export type ParsedDocument = ReturnType<typeof parseDocument>;
export type DocumentChild = ParsedDocument["children"][number];
export function findRangeOfChildren(children: DocumentChild[]) {
const firstChild = children[0];
const lastChild = children.length > 1
? children[children.length - 1]
: firstChild;
const start = firstChild.position?.start.offset;
const end = lastChild.position?.end.offset;
if (typeof start !== "number" || typeof end !== "number") return;
return [start, end];
}
export function getTextOfRange(children: DocumentChild[], text: string) {
if (!children || children.length === 0) {
return;
}
const range = findRangeOfChildren(children);
if (!range) return;
return text.substring(range[0], range[1]);
}
export function getTextOfChild(child: DocumentChild): string | undefined {
if ("value" in child) return child.value;
if ("children" in child) {
return getTextOfChild(child.children[0]);
}
return;
}

1
lib/index.ts Normal file
View File

@ -0,0 +1 @@
export * from "./documents.ts";

173
lib/recipes.ts Normal file
View File

@ -0,0 +1,173 @@
import {
type DocumentChild,
getTextOfChild,
getTextOfRange,
parseDocument,
parseFrontmatter,
} from "./documents.ts";
import { parseIngredient } from "npm:parse-ingredient";
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"],
},
teaSpoon: {
short: "TL",
plural: "Tea Spoon",
alternates: ["tl", "TL"],
},
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 = parseFrontmatter(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: {
...meta,
image: meta?.image?.replace(/^Recipes\/images/, "/api/recipes/images"),
},
name,
description,
ingredients,
preparation,
};
}