import fs from 'node:fs' import path from 'node:path' import vm from 'node:vm' import ts from '../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/typescript.js' const rootDir = process.cwd() const outputDir = path.join(rootDir, 'client', 'public', 'badges') const manifestPath = path.join(rootDir, 'server', 'src', 'config', 'badges.generated.json') const manifestTsPath = path.join(rootDir, 'server', 'src', 'config', 'badges.generated.ts') const sourceFiles = [ { fileName: 'aaaa.html', sourceId: 'core', sourceLabel: '核心科幻' }, { fileName: 'bbbb.html', sourceId: 'astro-zodiac', sourceLabel: '星相生肖' } ] function writeManifestTs(manifest) { fs.writeFileSync( manifestTsPath, `export const badgeCatalogData = ${JSON.stringify(manifest, null, 2)} as const\n` ) } const passthroughAttributes = new Set(['viewBox', 'xmlns', 'preserveAspectRatio']) function toKebabCase(value) { return value.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`) } function toAttributeName(name) { if (name === 'className') return 'class' if (passthroughAttributes.has(name)) return name return toKebabCase(name) } function escapeAttribute(value) { return String(value) .replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>') } function serializeStyle(style) { return Object.entries(style) .map(([key, value]) => `${toKebabCase(key)}:${value}`) .join(';') } function h(tag, props, ...children) { const normalizedProps = props || {} if (typeof tag === 'function') { return tag({ ...normalizedProps, children }) } const attributes = Object.entries(normalizedProps) .filter(([, value]) => value !== null && value !== undefined && value !== false) .map(([key, value]) => { if (value === true) { return toAttributeName(key) } const attrValue = key === 'style' && typeof value === 'object' ? serializeStyle(value) : value return `${toAttributeName(key)}="${escapeAttribute(attrValue)}"` }) .join(' ') const content = children .flat(Infinity) .filter(value => value !== null && value !== undefined && value !== false) .join('') return `<${tag}${attributes ? ` ${attributes}` : ''}>${content}` } function Fragment(_props, ...children) { return children.flat(Infinity).join('') } const gradients = [ { id: 'supreme', dark: ['#2a2d3e', '#0a0a0f'], light: ['#ffffff', '#cbd5e1'] }, { id: 'void', dark: ['#1a0033', '#05000a', '#000000'], light: ['#312e81', '#0f172a', '#020617'] }, { id: 'ice', dark: ['#002244', '#000a1a'], light: ['#e0f2fe', '#7dd3fc'] }, { id: 'gold', dark: ['#4a3b1c', '#1a140a'], light: ['#fef3c7', '#fbbf24'] }, { id: 'moon', dark: ['#334155', '#0f172a'], light: ['#f8fafc', '#94a3b8'] }, { id: 'omega', dark: ['#4a0000', '#1a0000'], light: ['#fee2e2', '#f87171'] }, { id: 'matrix', dark: ['#003311', '#001100'], light: ['#dcfce7', '#4ade80'] }, { id: 'platinum', dark: ['#475569', '#1e293b'], light: ['#ffffff', '#94a3b8'] }, { id: 'nebula', dark: ['#581c87', '#831843', '#171717'], light: ['#f3e8ff', '#fbcfe8', '#cbd5e1'] }, { id: 'aegis', dark: ['#0f172a', '#020617'], light: ['#f1f5f9', '#94a3b8'] }, { id: 'reactor', dark: ['#14532d', '#052e16'], light: ['#dcfce7', '#4ade80'] }, { id: 'tactical', dark: ['#450a0a', '#2a0000'], light: ['#fee2e2', '#f87171'] }, { id: 'arcane', dark: ['#3b0764', '#17002e'], light: ['#f3e8ff', '#c084fc'] } ] function buildDefs() { const filters = ` ` const gradientMarkup = gradients.map(gradient => { const buildGradient = (colors, theme) => { const stops = colors.map((color, index) => { const offset = colors.length === 1 ? '0%' : `${(index / (colors.length - 1)) * 100}%` return `` }).join('') return `${stops}` } return [ buildGradient(gradient.dark, 'd'), buildGradient(gradient.light, 'l') ].join('') }).join('') return `${filters}${gradientMarkup}` } const defsMarkup = buildDefs() function injectDefs(svg, extraStyles = '') { const withXmlns = svg.replace('${extraStyles}` : '' return withXmlns.replace(/^(]*>)/, `$1${styleBlock}${defsMarkup}`) } function splitLabel(label) { const matched = label.match(/^(.*?)\s+\((.*?)\)$/) if (!matched) { return { name: label.trim(), nameEn: null } } return { name: matched[1].trim(), nameEn: matched[2].trim() } } function splitSeriesMeta(title) { const normalized = title.replace(/^[^\p{L}\p{N}]+/u, '').trim() const matched = normalized.match(/^([A-Z]+)\s+(.+)$/) if (!matched) { return { seriesId: 'misc', seriesNameEn: null, seriesNameZh: normalized } } return { seriesId: matched[1].toLowerCase(), seriesNameEn: matched[1], seriesNameZh: matched[2].trim() } } const availableSourceFiles = sourceFiles.filter(source => fs.existsSync(path.join(rootDir, source.fileName))) if (availableSourceFiles.length !== sourceFiles.length) { if (fs.existsSync(manifestPath)) { const existingManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) writeManifestTs(existingManifest) console.warn('Badge source html files are missing. Kept existing badge assets and regenerated TypeScript manifest from JSON only.') process.exit(0) } throw new Error('Badge source html files are missing and no existing manifest is available.') } fs.rmSync(outputDir, { recursive: true, force: true }) fs.mkdirSync(outputDir, { recursive: true }) fs.mkdirSync(path.join(outputDir, 'dark'), { recursive: true }) fs.mkdirSync(path.join(outputDir, 'light'), { recursive: true }) const manifest = [] const seenBadgeIds = new Set() for (const source of availableSourceFiles) { const sourcePath = path.join(rootDir, source.fileName) const html = fs.readFileSync(sourcePath, 'utf8') const styleMatches = Array.from(html.matchAll(/