Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657import { join } from 'node:path';import { readdir, readFile } from 'node:fs/promises';import { shardConfigDir } from '@/contracts/paths';
const MAX_THEME_FILES = 128;const MAX_THEME_BYTES = 64 * 1024;
export function resolveThemeDir(env: Record<string, string | undefined> = process.env): string { return join(shardConfigDir(env), 'themes');}
/** Minimal TOML reader for theme files: top-level id/name and [light]/[dark] string values. */export function parseThemeToml(source: string): unknown { const result: Record<string, unknown> = {}; let target = result; for (const [index, raw] of source.split(/\r?\n/).entries()) { const line = raw.replace(/\s+#.*$/, '').trim(); if (!line) continue; const table = line.match(/^\[([A-Za-z0-9_-]+)\]$/); if (table) { const name = table[1]!; if (name !== 'light' && name !== 'dark') throw new Error(`unsupported table [${name}] on line ${index + 1}`); const section: Record<string, string> = {}; result[name] = section; target = section; continue; } const pair = line.match(/^([A-Za-z][A-Za-z0-9_-]*)\s*=\s*"((?:[^"\\]|\\.)*)"\s*$/); if (!pair) throw new Error(`invalid assignment on line ${index + 1}`); target[pair[1]!] = JSON.parse(`"${pair[2]!}"`) as string; } return result;}
export function parseThemeFile(filename: string, source: string): unknown { if (Buffer.byteLength(source, 'utf8') > MAX_THEME_BYTES) throw new Error('theme file exceeds 64 KiB'); if (filename.endsWith('.json')) return JSON.parse(source) as unknown; if (filename.endsWith('.toml')) return parseThemeToml(source); throw new Error('unsupported theme file extension');}
export async function loadThemeFiles(themeDir = resolveThemeDir()): Promise<unknown[]> { let names: string[]; try { names = await readdir(themeDir); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; throw error; } const files = names.filter((name) => /^[^/.][^/]*\.(?:json|toml)$/i.test(name)).sort().slice(0, MAX_THEME_FILES); const themes: unknown[] = []; for (const filename of files) { try { themes.push(parseThemeFile(filename.toLowerCase(), await readFile(join(themeDir, filename), 'utf8'))); } catch { /* One malformed optional theme must not prevent the web UI from loading. */ } } return themes;}