Files
mangabaka-seeder/src/core/html.ts
T
Lucas WintherandClaude Opus 5.5 baa3476b73 fix(server): inert proxy responses, API-call header, image-only uploads; origin check by host
From the final review: proxied archived HTML could run on our origin and read a
remembered token; /api/upload accepted any Wayback URL and GIF-prefixed bytes; the
origin check broke behind a TLS proxy; one unreadable snapshot could fail a run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:49:52 +02:00

42 lines
1.7 KiB
TypeScript

// Small, forgiving HTML helpers. Not a parser: enough for <meta>, <link>, <div style>, <img src>.
const NAMED: Readonly<Record<string, string>> = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
export function decodeEntities(s: string): string {
return s.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, entity: string) => {
const e = entity.toLowerCase();
if (e.startsWith("#")) {
const code = e.startsWith("#x") ? parseInt(e.slice(2), 16) : parseInt(e.slice(1), 10);
// Malformed pages carry entities like &#99999999;: leave them as written rather than throw.
return Number.isInteger(code) && code >= 0 && code <= 0x10ffff ? String.fromCodePoint(code) : match;
}
return NAMED[e] ?? match;
});
}
export function parseAttributes(tag: string): Record<string, string> {
const out: Record<string, string> = {};
const body = tag.replace(/^<\s*[\w-]+/, "").replace(/\/?\s*>$/, "");
for (const m of body.matchAll(/([^\s=/"'>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g)) {
const name = m[1]!.toLowerCase();
if (!(name in out)) out[name] = decodeEntities(m[2] ?? m[3] ?? m[4] ?? "");
}
return out;
}
export function findTags(html: string, name: string): Record<string, string>[] {
return [...html.matchAll(new RegExp(`<${name}\\b[^>]*>`, "gi"))].map((m) => parseAttributes(m[0]));
}
/** `<meta property|name="key" content="…">` */
export function metaContent(html: string, key: string): string | undefined {
for (const a of findTags(html, "meta")) {
if ((a.property ?? a.name) === key && a.content) return a.content;
}
return undefined;
}
export function textOf(fragment: string): string {
return decodeEntities(fragment.replace(/<[^>]*>/g, " ")).replace(/\s+/g, " ").trim();
}