#!/usr/bin/env node /** * Build the frontend SVG sprite from the single source of truth. * * Source: assets/icons/sprite/{key}.svg — one 1024x1024 icon per file. * The file name (without .svg) is the sprite key used by * aksrt_icon_markup('zibsvg-{key}') and . * Output: assets/js/runtime/svg-icon.js — aksrt_svgs['key'] = '' map * plus the runtime that injects definitions into the page. * * Usage: node tools/build-svg-sprite.mjs * * Rules for new icons: * - viewBox must be "0 0 1024 1024" (the runtime hardcodes it on ). * - File name: lowercase letters, digits, "-" or "_". * - Only the inner markup of is kept; editor metadata (p-id, t, class, * width/height, XML prolog, DOCTYPE) is stripped. */ import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = join(dirname(fileURLToPath(import.meta.url)), '..'); const srcDir = join(root, 'assets/icons/sprite'); const outFile = join(root, 'assets/js/runtime/svg-icon.js'); const files = readdirSync(srcDir).filter((f) => f.endsWith('.svg')).sort(); const errors = []; const entries = []; for (const file of files) { const key = file.slice(0, -4); if (!/^[a-z0-9_-]+$/.test(key)) { errors.push(`${file}: invalid key (use a-z, 0-9, "-" or "_")`); continue; } const raw = readFileSync(join(srcDir, file), 'utf8'); const open = raw.match(/]*>/i); const close = raw.lastIndexOf(''); if (!open || close < 0) { errors.push(`${file}: missing root`); continue; } const viewBox = (open[0].match(/viewBox="([^"]*)"/i) || [])[1]; if (viewBox !== '0 0 1024 1024') { errors.push(`${file}: viewBox must be "0 0 1024 1024" (got ${viewBox ?? 'none'})`); continue; } const inner = raw .slice(open.index + open[0].length, close) .replace(/\s+p-id="[^"]*"/g, '') .replace(/\r?\n\s*/g, '') .trim(); if (inner === '') { errors.push(`${file}: empty icon`); continue; } if (/[\\']/.test(inner)) { errors.push(`${file}: contains a backslash or single quote`); continue; } entries.push(`aksrt_svgs['${key}'] = '${inner}';`); } if (errors.length) { console.error(errors.join('\n')); process.exit(1); } const runtime = ` function show_svg() { jQuery('[data-svg]').each(function () { var _t = jQuery(this), viewbox = _t.attr('data-viewbox'), svgclass = _t.attr('data-class'), svgname = _t.attr('data-svg'), svg_html = '' + aksrt_svgs[svgname] + ''; _t.html(svg_html); }); } !(function () { var svg = ''; for (let key in aksrt_svgs) { svg += aksrt_svgs[key] ? '' + aksrt_svgs[key] + '' : ''; } var i = ''; if (!jQuery('svg#svg-icons').length) { jQuery('body').prepend(i); } })(window); `; const header = `/* Generated by tools/build-svg-sprite.mjs from assets/icons/sprite/*.svg — do not edit by hand. */\n'use strict';\nvar aksrt_svgs = [];\n`; writeFileSync(outFile, header + entries.join('\n') + '\n' + runtime); console.log(`Built ${entries.length} sprite icons -> assets/js/runtime/svg-icon.js`);