helpers.mjs 14 KB

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