Files
forge/tools/generate-svg.ts
zeekayandHanzo Dev d16d6013f0 Drop the migrate-from-a-Gitea-instance downloader
The downloader spoke to a remote instance through gitea.dev/sdk, the last
external dependency this tree could not name for itself. We migrate from
GitHub, so the path earned nothing and is gone, along with its service enum
name, migrate form, locale string, mock fixtures and icon rules. Enum slot 3
stays reserved: repositories already record it in original_service_type, and
renumbering would change what those rows mean.

TestDumpRestore went with it. It dumped from our own instance through that
downloader, so the round trip has no coverage until a downloader that speaks
our own API exists.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 18:03:46 -07:00

146 lines
5.7 KiB
JavaScript
Executable File

#!/usr/bin/env node
import {optimize} from 'svgo';
import {dirname, parse} from 'node:path';
import {globSync, writeFileSync} from 'node:fs';
import {readFile, writeFile, mkdir} from 'node:fs/promises';
import {fileURLToPath} from 'node:url';
import {exit} from 'node:process';
import type {Manifest} from 'material-icon-theme';
const glob = (pattern: string) => globSync(pattern, {cwd: dirname(import.meta.dirname)});
type Opts = {
prefix?: string,
fullName?: string,
};
async function processAssetsSvgFile(path: string, {prefix, fullName}: Opts = {}) {
let name = fullName;
if (!name) {
name = parse(path).name;
if (prefix) name = `${prefix}-${name}`;
if (prefix === 'octicon') name = name.replace(/-[0-9]+$/, ''); // chop of '-16' on octicons
}
// Set the `xmlns` attribute so that the files are displayable in standalone documents
// The svg backend module will strip the attribute during startup for inline display
const {data} = optimize(await readFile(path, 'utf8'), {
plugins: [
{name: 'preset-default'},
{name: 'removeDimensions'},
{name: 'removeTitle'},
{name: 'prefixIds', params: {prefix: () => name}},
{name: 'addClassesToSVGElement', params: {classNames: ['svg', name]}},
{
name: 'addAttributesToSVGElement', params: {
attributes: [
{'xmlns': 'http://www.w3.org/2000/svg'},
{'width': '16'}, {'height': '16'}, {'aria-hidden': 'true'},
],
},
},
],
});
await writeFile(fileURLToPath(new URL(`../public/assets/img/svg/${name}.svg`, import.meta.url)), data);
}
function processAssetsSvgFiles(pattern: string, opts: Opts = {}) {
return glob(pattern).map((path) => processAssetsSvgFile(path, opts));
}
// Names carried by the upstream icon theme that this product has no rule for.
const unusedIcons = ['gitea'];
function unusedIconName(name: string) {
return unusedIcons.some((n) => name.toLowerCase().includes(n));
}
function lowercaseKeys(obj: Record<string, any>) {
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value]));
}
async function processMaterialFileIcons() {
const paths = glob('node_modules/material-icon-theme/icons/*.svg');
const svgSymbols: Record<string, string> = {};
for (const path of paths) {
const svgName = parse(path).name;
if (unusedIconName(svgName)) continue;
// remove all unnecessary attributes, only keep "viewBox"
const {data} = optimize(await readFile(path, 'utf8'), {
plugins: [
{name: 'preset-default'},
{name: 'removeDimensions'},
{name: 'removeXMLNS'},
{name: 'removeAttrs', params: {attrs: 'xml:space', elemSeparator: ','}},
],
});
// intentionally use single quote here to avoid escaping
svgSymbols[svgName] = data.replace(/"/g, `'`);
}
writeFileSync(fileURLToPath(new URL(`../options/fileicon/material-icon-svgs.json`, import.meta.url)), JSON.stringify(svgSymbols, null, 2));
const vscodeExtensionsJson = await readFile(fileURLToPath(new URL(`generate-svg-vscode-extensions.json`, import.meta.url)), 'utf8');
const vscodeExtensions = JSON.parse(vscodeExtensionsJson) as Record<string, string>;
const iconRulesJson = await readFile(fileURLToPath(new URL(`../node_modules/material-icon-theme/dist/material-icons.json`, import.meta.url)), 'utf8');
const iconRules = JSON.parse(iconRulesJson) as Manifest;
// The rules are from VSCode material-icon-theme, we need to adjust them to our needs
// 1. We only use lowercase filenames to match (it should be good enough for most cases and more efficient)
// 2. We do not have a "Language ID" system:
// * https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers
// * https://github.com/microsoft/vscode/tree/1.98.0/extensions
delete iconRules.iconDefinitions;
if (iconRules.fileNames) {
iconRules.fileNames = lowercaseKeys(iconRules.fileNames);
}
if (iconRules.folderNames) {
iconRules.folderNames = lowercaseKeys(iconRules.folderNames);
}
if (iconRules.fileExtensions) {
iconRules.fileExtensions = lowercaseKeys(iconRules.fileExtensions);
}
// Use VSCode's "Language ID" mapping from its extensions
for (const langIdExtMap of Object.values(vscodeExtensions)) {
for (const [langId, names] of Object.entries(langIdExtMap)) {
for (const name of names) {
const nameLower = name.toLowerCase();
if (nameLower[0] === '.') {
if (iconRules.fileExtensions) {
iconRules.fileExtensions[nameLower.substring(1)] ??= langId;
}
} else {
if (iconRules.fileNames) {
iconRules.fileNames[nameLower] ??= langId;
}
}
}
}
}
// Drop rules for names the theme carries but we have no use for.
for (const section of [iconRules.fileNames, iconRules.folderNames, iconRules.folderNamesExpanded, iconRules.fileExtensions]) {
if (!section) continue;
for (const key of Object.keys(section)) {
if (unusedIconName(key) || unusedIconName(section[key])) delete section[key];
}
}
const iconRulesPretty = JSON.stringify(iconRules, null, 2);
writeFileSync(fileURLToPath(new URL(`../options/fileicon/material-icon-rules.json`, import.meta.url)), iconRulesPretty);
}
async function main() {
await mkdir(fileURLToPath(new URL('../public/assets/img/svg', import.meta.url)), {recursive: true});
await Promise.all([
...processAssetsSvgFiles('node_modules/@primer/octicons/build/svg/*-16.svg', {prefix: 'octicon'}),
...processAssetsSvgFiles('web_src/svg/*.svg'),
...processAssetsSvgFiles('public/assets/img/logo.svg', {fullName: 'hanzo-hanzo'}),
processMaterialFileIcons(),
]);
}
try {
await main();
} catch (err) {
console.error(err);
exit(1);
}