helpers.mjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. import fs from 'fs';
  2. import path, { resolve, basename } from 'path';
  3. import { fileURLToPath } from 'url';
  4. import svgParse from 'parse-svg-path';
  5. import svgpath from 'svgpath';
  6. import cheerio from 'cheerio';
  7. import { minify } from 'html-minifier';
  8. import { parseSync } from 'svgson';
  9. import { optimize } from 'svgo';
  10. import cp from 'child_process';
  11. import minimist from 'minimist';
  12. import matter from 'gray-matter';
  13. import { globSync } from 'glob';
  14. import { exec } from 'child_process';
  15. import slash from 'slash';
  16. export const iconTemplate = (type) =>
  17. type === 'outline'
  18. ? `<svg
  19. xmlns="http://www.w3.org/2000/svg"
  20. width="24"
  21. height="24"
  22. viewBox="0 0 24 24"
  23. fill="none"
  24. stroke="currentColor"
  25. stroke-width="2"
  26. stroke-linecap="round"
  27. stroke-linejoin="round"
  28. >`
  29. : `<svg
  30. xmlns="http://www.w3.org/2000/svg"
  31. width="24"
  32. height="24"
  33. viewBox="0 0 24 24"
  34. fill="currentColor"
  35. >`;
  36. export const blankSquare = '<path stroke="none" d="M0 0h24v24H0z" fill="none"/>';
  37. export const types = ['outline', 'filled'];
  38. export const getCurrentDirPath = () => {
  39. return path.dirname(fileURLToPath(import.meta.url));
  40. };
  41. export const HOME_DIR = resolve(getCurrentDirPath(), '..');
  42. export const ICONS_SRC_DIR = resolve(HOME_DIR, 'icons');
  43. export const PACKAGES_DIR = resolve(HOME_DIR, 'packages');
  44. export const GITHUB_DIR = resolve(HOME_DIR, '.github');
  45. export const parseMatter = (icon) => {
  46. const { data, content } = matter.read(icon, { delims: ['<!--', '-->'] });
  47. return { data, content };
  48. };
  49. const getSvgContent = (svg, type, name) => {
  50. return svg
  51. .replace(/<svg([^>]+)>/, (m, m1) => {
  52. return `<svg${m1} class="icon icon-tabler icons-tabler-${type} icon-tabler-${name}"\n>\n ${blankSquare}`;
  53. })
  54. .trim();
  55. };
  56. export const getAllIcons = (withContent = false, withObject = false) => {
  57. let icons = {};
  58. const limit = process.env['ICONS_LIMIT'] || Infinity;
  59. types.forEach((type) => {
  60. icons[type] = globSync(slash(path.join(ICONS_SRC_DIR, `${type}/*.svg`)))
  61. .slice(0, limit)
  62. .sort()
  63. .map((i) => {
  64. const { data, content } = parseMatter(i),
  65. name = basename(i, '.svg');
  66. return {
  67. name,
  68. namePascal: toPascalCase(`icon ${name}`),
  69. path: i,
  70. category: data.category || '',
  71. tags: data.tags || [],
  72. version: data.version || '',
  73. unicode: data.unicode || '',
  74. ...(withContent ? { content: getSvgContent(content, type, name) } : {}),
  75. ...(withObject ? { obj: parseSync(content.replace(blankSquare, '')) } : {}),
  76. };
  77. })
  78. .sort();
  79. });
  80. return icons;
  81. };
  82. export const getAllIconsMerged = (withContent = false, withObject = false) => {
  83. const allIcons = getAllIcons(true);
  84. const icons = {};
  85. allIcons.outline.forEach((icon) => {
  86. icons[icon.name] = {
  87. name: icon.name,
  88. category: icon.category || '',
  89. tags: icon.tags || [],
  90. styles: {
  91. outline: {
  92. version: icon.version || '',
  93. unicode: icon.unicode || '',
  94. ...(withContent ? { content: icon.content } : {}),
  95. ...(withObject ? { obj: icon.obj } : {}),
  96. },
  97. },
  98. };
  99. });
  100. allIcons.filled.forEach((icon) => {
  101. if (icons[icon.name]) {
  102. icons[icon.name].styles.filled = {
  103. version: icon.version || '',
  104. unicode: icon.unicode || '',
  105. ...(withContent ? { content: icon.content } : {}),
  106. ...(withObject ? { obj: icon.obj } : {}),
  107. };
  108. }
  109. });
  110. return icons;
  111. };
  112. export const getArgvs = () => {
  113. return minimist(process.argv.slice(2));
  114. };
  115. export const getPackageDir = (packageName) => {
  116. return `${PACKAGES_DIR}/${packageName}`;
  117. };
  118. /**
  119. * Return project package.json
  120. * @returns {any}
  121. */
  122. export const getPackageJson = () => {
  123. return JSON.parse(fs.readFileSync(resolve(HOME_DIR, 'package.json'), 'utf-8'));
  124. };
  125. /**
  126. * Reads SVGs from directory
  127. *
  128. * @param directory
  129. * @returns {string[]}
  130. */
  131. export const readSvgDirectory = (directory) => {
  132. return fs.readdirSync(directory).filter((file) => path.extname(file) === '.svg');
  133. };
  134. export const getAliases = (groupped = false) => {
  135. const allAliases = JSON.parse(fs.readFileSync(resolve(HOME_DIR, 'aliases.json'), 'utf-8'));
  136. const allIcons = getAllIcons();
  137. if (groupped) {
  138. let aliases = [];
  139. types.forEach((type) => {
  140. const icons = allIcons[type].map((i) => i.name);
  141. aliases[type] = {};
  142. for (const [key, value] of Object.entries(allAliases[type])) {
  143. if (icons.includes(value)) {
  144. aliases[type][key] = value;
  145. }
  146. }
  147. });
  148. return aliases;
  149. } else {
  150. let aliases = [];
  151. types.forEach((type) => {
  152. const icons = allIcons[type].map((i) => i.name);
  153. for (const [key, value] of Object.entries(allAliases[type])) {
  154. if (icons.includes(value)) {
  155. aliases[`${key}${type !== 'outline' ? `-${type}` : ''}`] =
  156. `${value}${type !== 'outline' ? `-${type}` : ''}`;
  157. }
  158. }
  159. });
  160. return aliases;
  161. }
  162. };
  163. /**
  164. * Read SVG
  165. *
  166. * @param fileName
  167. * @param directory
  168. * @returns {string}
  169. */
  170. export const readSvg = (fileName, directory) => {
  171. return fs.readFileSync(path.join(directory, fileName), 'utf-8');
  172. };
  173. /**
  174. * Create directory if not exists
  175. * @param dir
  176. */
  177. export const createDirectory = (dir) => {
  178. if (!fs.existsSync(dir)) {
  179. fs.mkdirSync(dir);
  180. }
  181. };
  182. /**
  183. * Get SVG name
  184. * @param fileName
  185. * @returns {string}
  186. */
  187. export const getSvgName = (fileName) => {
  188. return path.basename(fileName, '.svg');
  189. };
  190. /**
  191. * Convert string to CamelCase
  192. * @param string
  193. * @returns {*}
  194. */
  195. export const toCamelCase = (string) => {
  196. return string.replace(/^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) =>
  197. p2 ? p2.toUpperCase() : p1.toLowerCase(),
  198. );
  199. };
  200. export const toPascalCase = (string) => {
  201. const camelCase = toCamelCase(string);
  202. return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
  203. };
  204. export const addFloats = function (n1, n2) {
  205. return Math.round((parseFloat(n1) + parseFloat(n2)) * 1000) / 1000;
  206. };
  207. export const optimizePath = function (path) {
  208. let transformed = svgpath(path).rel().round(3).toString();
  209. return svgParse(transformed)
  210. .map(function (a) {
  211. return a.join(' ');
  212. })
  213. .join('');
  214. };
  215. export const optimizeSVG = (data) => {
  216. return optimize(data, {
  217. js2svg: {
  218. indent: 2,
  219. pretty: true,
  220. },
  221. plugins: [
  222. {
  223. name: 'preset-default',
  224. params: {
  225. overrides: {
  226. mergePaths: false,
  227. },
  228. },
  229. },
  230. ],
  231. }).data;
  232. };
  233. export function buildIconsObject(svgFiles, getSvg) {
  234. return svgFiles
  235. .map((svgFile) => {
  236. const name = path.basename(svgFile, '.svg');
  237. const svg = getSvg(svgFile);
  238. const contents = getSvgContents(svg);
  239. return { name, contents };
  240. })
  241. .reduce((icons, icon) => {
  242. icons[icon.name] = icon.contents;
  243. return icons;
  244. }, {});
  245. }
  246. function getSvgContents(svg) {
  247. const $ = cheerio.load(svg);
  248. return minify($('svg').html(), { collapseWhitespace: true });
  249. }
  250. export const asyncForEach = async (array, callback) => {
  251. for (let index = 0; index < array.length; index++) {
  252. await callback(array[index], index, array);
  253. }
  254. };
  255. export const createScreenshot = (filePath, retina = true) => {
  256. cp.execSync(`rsvg-convert -x 2 -y 2 ${filePath} > ${filePath.replace('.svg', '.png')}`);
  257. if (retina) {
  258. cp.execSync(`rsvg-convert -x 4 -y 4 ${filePath} > ${filePath.replace('.svg', '@2x.png')}`);
  259. }
  260. };
  261. export const createSvgSymbol = (svg, name, stroke) => {
  262. return svg
  263. .replace('<svg', `<symbol id="${name}"`)
  264. .replace(' width="24" height="24"', '')
  265. .replace(' stroke-width="2"', ` stroke-width="${stroke}"`)
  266. .replace('</svg>', '</symbol>')
  267. .replace(/\n\s+/g, ' ')
  268. .replace(/<!--(.*?)-->/gis, '')
  269. .trim();
  270. };
  271. export const generateIconsPreview = async function (
  272. files,
  273. destFile,
  274. {
  275. columnsCount = 19,
  276. paddingOuter = 7,
  277. color = '#354052',
  278. background = '#fff',
  279. png = true,
  280. stroke = 2,
  281. retina = true,
  282. } = {},
  283. ) {
  284. const padding = 20,
  285. iconSize = 24;
  286. const iconsCount = files.length,
  287. rowsCount = Math.ceil(iconsCount / columnsCount),
  288. width = columnsCount * (iconSize + padding) + 2 * paddingOuter - padding,
  289. height = rowsCount * (iconSize + padding) + 2 * paddingOuter - padding;
  290. let svgContentSymbols = '',
  291. svgContentIcons = '',
  292. x = paddingOuter,
  293. y = paddingOuter;
  294. files.forEach(function (file, i) {
  295. const name = file.replace(/^(.*)\/([^\/]+)\/([^.]+).svg$/g, '$2-$3');
  296. let svgFile = fs.readFileSync(file),
  297. svgFileContent = svgFile.toString();
  298. svgFileContent = createSvgSymbol(svgFileContent, name, stroke);
  299. svgContentSymbols += `\t${svgFileContent}\n`;
  300. svgContentIcons += `\t<use xlink:href="#${name}" x="${x}" y="${y}" width="${iconSize}" height="${iconSize}" />\n`;
  301. x += padding + iconSize;
  302. if (i % columnsCount === columnsCount - 1) {
  303. x = paddingOuter;
  304. y += padding + iconSize;
  305. }
  306. });
  307. const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" style="color: ${color}"><rect x="0" y="0" width="${width}" height="${height}" fill="${background}"></rect>\n${svgContentSymbols}\n${svgContentIcons}\n</svg>`;
  308. console.log(destFile);
  309. fs.writeFileSync(destFile, svgContent);
  310. if (png) {
  311. await createScreenshot(destFile, retina);
  312. }
  313. };
  314. export const printChangelog = function (newIcons, modifiedIcons, renamedIcons, pretty = false) {
  315. if (newIcons.length > 0) {
  316. if (pretty) {
  317. console.log(`### ${newIcons.length} new icon${newIcons.length > 1 ? 's' : ''}:\n`);
  318. newIcons.forEach(function (icon, i) {
  319. console.log(`- \`${icon}\``);
  320. });
  321. } else {
  322. let str = '';
  323. str += `${newIcons.length} new icon${newIcons.length > 1 ? 's' : ''}: `;
  324. newIcons.forEach(function (icon, i) {
  325. str += `\`${icon}\``;
  326. if (i + 1 <= newIcons.length - 1) {
  327. str += ', ';
  328. }
  329. });
  330. console.log(str);
  331. }
  332. console.log('');
  333. }
  334. if (modifiedIcons.length > 0) {
  335. let str = '';
  336. str += `Fixed icon${modifiedIcons.length > 1 ? 's' : ''}: `;
  337. modifiedIcons.forEach(function (icon, i) {
  338. str += `\`${icon}\``;
  339. if (i + 1 <= modifiedIcons.length - 1) {
  340. str += ', ';
  341. }
  342. });
  343. console.log(str);
  344. console.log('');
  345. }
  346. if (renamedIcons.length > 0) {
  347. console.log(`Renamed icons: `);
  348. renamedIcons.forEach(function (icon, i) {
  349. console.log(`- \`${icon[0]}\` renamed to \`${icon[1]}\``);
  350. });
  351. }
  352. };
  353. export const getCompileOptions = () => {
  354. const compileOptions = {
  355. includeIcons: [],
  356. strokeWidth: null,
  357. fontForge: 'fontforge',
  358. };
  359. if (fs.existsSync('../compile-options.json')) {
  360. try {
  361. const tempOptions = JSON.parse(fs.readFileSync('../compile-options.json').toString());
  362. if (typeof tempOptions !== 'object') {
  363. throw 'Compile options file does not contain an json object';
  364. }
  365. if (typeof tempOptions.includeIcons !== 'undefined') {
  366. if (!Array.isArray(tempOptions.includeIcons)) {
  367. throw 'property inludeIcons is not an array';
  368. }
  369. compileOptions.includeIcons = tempOptions.includeIcons;
  370. }
  371. if (typeof tempOptions.includeCategories !== 'undefined') {
  372. if (typeof tempOptions.includeCategories === 'string') {
  373. tempOptions.includeCategories = tempOptions.includeCategories.split(' ');
  374. }
  375. if (!Array.isArray(tempOptions.includeCategories)) {
  376. throw 'property includeCategories is not an array or string';
  377. }
  378. const tags = Object.entries(require('./tags.json'));
  379. tempOptions.includeCategories.forEach(function (category) {
  380. category = category.charAt(0).toUpperCase() + category.slice(1);
  381. for (const [icon, data] of tags) {
  382. if (data.category === category && compileOptions.includeIcons.indexOf(icon) === -1) {
  383. compileOptions.includeIcons.push(icon);
  384. }
  385. }
  386. });
  387. }
  388. if (typeof tempOptions.excludeIcons !== 'undefined') {
  389. if (!Array.isArray(tempOptions.excludeIcons)) {
  390. throw 'property excludeIcons is not an array';
  391. }
  392. compileOptions.includeIcons = compileOptions.includeIcons.filter(function (icon) {
  393. return tempOptions.excludeIcons.indexOf(icon) === -1;
  394. });
  395. }
  396. if (typeof tempOptions.excludeOffIcons !== 'undefined' && tempOptions.excludeOffIcons) {
  397. // Exclude `*-off` icons
  398. compileOptions.includeIcons = compileOptions.includeIcons.filter(function (icon) {
  399. return !icon.endsWith('-off');
  400. });
  401. }
  402. if (typeof tempOptions.strokeWidth !== 'undefined') {
  403. if (
  404. typeof tempOptions.strokeWidth !== 'string' &&
  405. typeof tempOptions.strokeWidth !== 'number'
  406. ) {
  407. throw 'property strokeWidth is not a string or number';
  408. }
  409. compileOptions.strokeWidth = tempOptions.strokeWidth.toString();
  410. }
  411. if (typeof tempOptions.fontForge !== 'undefined') {
  412. if (typeof tempOptions.fontForge !== 'string') {
  413. throw 'property fontForge is not a string';
  414. }
  415. compileOptions.fontForge = tempOptions.fontForge;
  416. }
  417. } catch (error) {
  418. throw `Error reading compile-options.json: ${error}`;
  419. }
  420. }
  421. return compileOptions;
  422. };
  423. export const convertIconsToImages = async (dir, extension, size = 240) => {
  424. const icons = getAllIcons();
  425. await asyncForEach(Object.entries(icons), async function ([type, svgFiles]) {
  426. fs.mkdirSync(path.join(dir, `./${type}`), { recursive: true });
  427. await asyncForEach(svgFiles, async function (file, i) {
  428. const distPath = path.join(dir, `./${type}/${file.name}.${extension}`);
  429. process.stdout.write(
  430. `Building \`icons/${extension}\` ${type} ${i}/${svgFiles.length}: ${file.name.padEnd(42)}\r`,
  431. );
  432. await new Promise((resolve, reject) => {
  433. exec(`rsvg-convert -f ${extension} -h ${size} ${file.path} > ${distPath}`, (error) => {
  434. error ? reject() : resolve();
  435. });
  436. });
  437. });
  438. });
  439. };
  440. export const getMaxUnicode = () => {
  441. const files = globSync(path.join(ICONS_SRC_DIR, '**/*.svg'));
  442. let maxUnicode = 0;
  443. files.forEach(function (file) {
  444. const svgFile = fs.readFileSync(file).toString();
  445. svgFile.replace(/unicode: "([a-f0-9.]+)"/i, function (m, unicode) {
  446. const newUnicode = parseInt(unicode, 16);
  447. if (newUnicode) {
  448. maxUnicode = Math.max(maxUnicode, newUnicode);
  449. }
  450. });
  451. });
  452. console.log(`Max unicode: ${maxUnicode}`);
  453. return maxUnicode;
  454. };