Files
HamHam/src/lib/import.ts
T

94 lines
3.6 KiB
TypeScript

import { XMLBuilder, XMLParser, XMLValidator } from 'fast-xml-parser';
import { collectionSchema, type Recipe } from './recipe';
export function parseImport(raw: string, format: 'json' | 'xml') {
if (Buffer.byteLength(raw, 'utf8') > 2 * 1024 * 1024)
throw new Error('Importdatei ist größer als 2 MB.');
let value: unknown;
if (format === 'json') value = JSON.parse(raw);
else {
if (/<!DOCTYPE|<!ENTITY/i.test(raw))
throw new Error('DOCTYPE und Entities sind nicht erlaubt.');
if (XMLValidator.validate(raw) !== true) throw new Error('XML ist nicht wohlgeformt.');
const parser = new XMLParser({
ignoreAttributes: false,
parseTagValue: false,
processEntities: false,
isArray: (_name, path) =>
/\.(recipe|ingredient|step|timer|category|tag|ref)$|\.images\.image$/.test(String(path)),
});
const root = parser.parse(raw).hamham;
if (!root) throw new Error('XML benötigt das Wurzelelement hamham.');
const num = (x: unknown, fallback = 0) => (x === undefined || x === '' ? fallback : Number(x));
const bool = (x: unknown) => x === 'true';
const decode = (s: string) =>
s
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&amp;/g, '&');
const decodeTree = (v: unknown): unknown =>
typeof v === 'string'
? decode(v)
: Array.isArray(v)
? v.map(decodeTree)
: v && typeof v === 'object'
? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, decodeTree(x)]))
: v;
const recipes = (root.recipes?.recipe ?? []).map((r: Record<string, any>) => ({
...r,
servings: num(r.servings),
prepMinutes: num(r.prepMinutes),
cookMinutes: num(r.cookMinutes),
restMinutes: num(r.restMinutes),
published: bool(r.published),
categories: r.categories?.category ?? [],
tags: r.tags?.tag ?? [],
images: r.images?.image ?? [],
ingredients: (r.ingredients?.ingredient ?? []).map((i: Record<string, any>) => ({
...i,
amount: i.amount === '' || i.amount === undefined ? null : Number(i.amount),
optional: bool(i.optional),
})),
steps: (r.steps?.step ?? []).map((s: Record<string, any>) => ({
...s,
ingredients: s.ingredients?.ref ?? [],
timers: (s.timers?.timer ?? []).map((t: Record<string, any>) => ({
...t,
seconds: num(t.seconds),
})),
})),
nutrition:
r.nutrition && typeof r.nutrition === 'object'
? Object.fromEntries(Object.entries(r.nutrition).map(([k, v]) => [k, num(v)]))
: null,
}));
value = { version: Number(root['@_version']), recipes: decodeTree(recipes) };
}
return collectionSchema.parse(value);
}
export function exportRecipes(recipes: Recipe[], format: 'json' | 'xml') {
if (format === 'json') return JSON.stringify({ version: 1, recipes }, null, 2);
const data = recipes.map((r) => ({
...r,
categories: { category: r.categories },
tags: { tag: r.tags },
images: { image: r.images },
ingredients: { ingredient: r.ingredients.map((i) => ({ ...i, amount: i.amount ?? '' })) },
steps: {
step: r.steps.map((s) => ({
...s,
ingredients: { ref: s.ingredients },
timers: { timer: s.timers },
})),
},
nutrition: r.nutrition ?? '',
}));
return (
'<?xml version="1.0" encoding="UTF-8"?>\n' +
new XMLBuilder({ ignoreAttributes: false, format: true }).build({
hamham: { '@_version': 1, recipes: { recipe: data } },
})
);
}