gitea源码

generate-svg.ts 4.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. #!/usr/bin/env node
  2. import {optimize} from 'svgo';
  3. import {dirname, parse} from 'node:path';
  4. import {globSync, writeFileSync} from 'node:fs';
  5. import {readFile, writeFile, mkdir} from 'node:fs/promises';
  6. import {fileURLToPath} from 'node:url';
  7. import {exit} from 'node:process';
  8. import type {Manifest} from 'material-icon-theme';
  9. const glob = (pattern: string) => globSync(pattern, {cwd: dirname(import.meta.dirname)});
  10. type Opts = {
  11. prefix?: string,
  12. fullName?: string,
  13. };
  14. async function processAssetsSvgFile(path: string, {prefix, fullName}: Opts = {}) {
  15. let name = fullName;
  16. if (!name) {
  17. name = parse(path).name;
  18. if (prefix) name = `${prefix}-${name}`;
  19. if (prefix === 'octicon') name = name.replace(/-[0-9]+$/, ''); // chop of '-16' on octicons
  20. }
  21. // Set the `xmlns` attribute so that the files are displayable in standalone documents
  22. // The svg backend module will strip the attribute during startup for inline display
  23. const {data} = optimize(await readFile(path, 'utf8'), {
  24. plugins: [
  25. {name: 'preset-default'},
  26. {name: 'removeDimensions'},
  27. {name: 'removeTitle'},
  28. {name: 'prefixIds', params: {prefix: () => name}},
  29. {name: 'addClassesToSVGElement', params: {classNames: ['svg', name]}},
  30. {
  31. name: 'addAttributesToSVGElement', params: {
  32. attributes: [
  33. {'xmlns': 'http://www.w3.org/2000/svg'},
  34. {'width': '16'}, {'height': '16'}, {'aria-hidden': 'true'},
  35. ],
  36. },
  37. },
  38. ],
  39. });
  40. await writeFile(fileURLToPath(new URL(`../public/assets/img/svg/${name}.svg`, import.meta.url)), data);
  41. }
  42. function processAssetsSvgFiles(pattern: string, opts: Opts = {}) {
  43. return glob(pattern).map((path) => processAssetsSvgFile(path, opts));
  44. }
  45. async function processMaterialFileIcons() {
  46. const paths = glob('node_modules/material-icon-theme/icons/*.svg');
  47. const svgSymbols: Record<string, string> = {};
  48. for (const path of paths) {
  49. // remove all unnecessary attributes, only keep "viewBox"
  50. const {data} = optimize(await readFile(path, 'utf8'), {
  51. plugins: [
  52. {name: 'preset-default'},
  53. {name: 'removeDimensions'},
  54. {name: 'removeXMLNS'},
  55. {name: 'removeAttrs', params: {attrs: 'xml:space', elemSeparator: ','}},
  56. ],
  57. });
  58. const svgName = parse(path).name;
  59. // intentionally use single quote here to avoid escaping
  60. svgSymbols[svgName] = data.replace(/"/g, `'`);
  61. }
  62. writeFileSync(fileURLToPath(new URL(`../options/fileicon/material-icon-svgs.json`, import.meta.url)), JSON.stringify(svgSymbols, null, 2));
  63. const vscodeExtensionsJson = await readFile(fileURLToPath(new URL(`generate-svg-vscode-extensions.json`, import.meta.url)), 'utf8');
  64. const vscodeExtensions = JSON.parse(vscodeExtensionsJson) as Record<string, string>;
  65. const iconRulesJson = await readFile(fileURLToPath(new URL(`../node_modules/material-icon-theme/dist/material-icons.json`, import.meta.url)), 'utf8');
  66. const iconRules = JSON.parse(iconRulesJson) as Manifest;
  67. // The rules are from VSCode material-icon-theme, we need to adjust them to our needs
  68. // 1. We only use lowercase filenames to match (it should be good enough for most cases and more efficient)
  69. // 2. We do not have a "Language ID" system:
  70. // * https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers
  71. // * https://github.com/microsoft/vscode/tree/1.98.0/extensions
  72. delete iconRules.iconDefinitions;
  73. for (const [k, v] of Object.entries(iconRules.fileNames)) iconRules.fileNames[k.toLowerCase()] = v;
  74. for (const [k, v] of Object.entries(iconRules.folderNames)) iconRules.folderNames[k.toLowerCase()] = v;
  75. for (const [k, v] of Object.entries(iconRules.fileExtensions)) iconRules.fileExtensions[k.toLowerCase()] = v;
  76. // Use VSCode's "Language ID" mapping from its extensions
  77. for (const [_, langIdExtMap] of Object.entries(vscodeExtensions)) {
  78. for (const [langId, names] of Object.entries(langIdExtMap)) {
  79. for (const name of names) {
  80. const nameLower = name.toLowerCase();
  81. if (nameLower[0] === '.') {
  82. iconRules.fileExtensions[nameLower.substring(1)] ??= langId;
  83. } else {
  84. iconRules.fileNames[nameLower] ??= langId;
  85. }
  86. }
  87. }
  88. }
  89. const iconRulesPretty = JSON.stringify(iconRules, null, 2);
  90. writeFileSync(fileURLToPath(new URL(`../options/fileicon/material-icon-rules.json`, import.meta.url)), iconRulesPretty);
  91. }
  92. async function main() {
  93. await mkdir(fileURLToPath(new URL('../public/assets/img/svg', import.meta.url)), {recursive: true});
  94. await Promise.all([
  95. ...processAssetsSvgFiles('node_modules/@primer/octicons/build/svg/*-16.svg', {prefix: 'octicon'}),
  96. ...processAssetsSvgFiles('web_src/svg/*.svg'),
  97. ...processAssetsSvgFiles('public/assets/img/gitea.svg', {fullName: 'gitea-gitea'}),
  98. processMaterialFileIcons(),
  99. ]);
  100. }
  101. try {
  102. await main();
  103. } catch (err) {
  104. console.error(err);
  105. exit(1);
  106. }