feat: init
This commit is contained in:
73
lib/documents.ts
Normal file
73
lib/documents.ts
Normal 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
1
lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./documents.ts";
|
173
lib/recipes.ts
Normal file
173
lib/recipes.ts
Normal 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,
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user