helpers.mjs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  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 { exit } from 'process'
  13. export const getCurrentDirPath = () => {
  14. return path.dirname(fileURLToPath(import.meta.url));
  15. }
  16. export const HOME_DIR = resolve(getCurrentDirPath(), '..')
  17. export const ICONS_SRC_DIR = resolve(HOME_DIR, 'src/_icons')
  18. export const ICONS_DIR = resolve(HOME_DIR, 'icons')
  19. export const PACKAGES_DIR = resolve(HOME_DIR, 'packages')
  20. export const getArgvs = () => {
  21. return minimist(process.argv.slice(2))
  22. }
  23. export const getPackageDir = (packageName) => {
  24. return `${PACKAGES_DIR}/${packageName}`
  25. }
  26. /**
  27. * Return project package.json
  28. * @returns {any}
  29. */
  30. export const getPackageJson = () => {
  31. return JSON.parse(fs.readFileSync(resolve(HOME_DIR, 'package.json'), 'utf-8'))
  32. }
  33. /**
  34. * Reads SVGs from directory
  35. *
  36. * @param directory
  37. * @returns {string[]}
  38. */
  39. export const readSvgDirectory = (directory) => {
  40. return fs.readdirSync(directory).filter((file) => path.extname(file) === '.svg')
  41. }
  42. export const readSvgs = () => {
  43. const svgFiles = readSvgDirectory(ICONS_DIR)
  44. const limit = process.env['ICONS_LIMIT'] || Infinity;
  45. return svgFiles
  46. .slice(0, limit)
  47. .map(svgFile => {
  48. const name = basename(svgFile, '.svg'),
  49. namePascal = toPascalCase(`icon ${name}`),
  50. contents = readSvg(svgFile, ICONS_DIR).trim(),
  51. path = resolve(ICONS_DIR, svgFile),
  52. obj = parseSync(contents.replace('<path stroke="none" d="M0 0h24v24H0z" fill="none"/>', ''));
  53. return {
  54. name,
  55. namePascal,
  56. contents,
  57. obj,
  58. path
  59. };
  60. });
  61. }
  62. export const readAliases = () => {
  63. const allAliases = JSON.parse(fs.readFileSync(resolve(HOME_DIR, 'aliases.json'), 'utf-8')),
  64. svgFilesList = readSvgs().map(icon => icon.name);
  65. let aliases = [];
  66. for (const [key, value] of Object.entries(allAliases)) {
  67. if (svgFilesList.includes(value)) {
  68. aliases[key] = value;
  69. }
  70. }
  71. return aliases
  72. }
  73. /**
  74. * Read SVG
  75. *
  76. * @param fileName
  77. * @param directory
  78. * @returns {string}
  79. */
  80. export const readSvg = (fileName, directory) => {
  81. return fs.readFileSync(path.join(directory, fileName), 'utf-8')
  82. }
  83. /**
  84. * Create directory if not exists
  85. * @param dir
  86. */
  87. export const createDirectory = (dir) => {
  88. if (!fs.existsSync(dir)) {
  89. fs.mkdirSync(dir);
  90. }
  91. };
  92. /**
  93. * Get SVG name
  94. * @param fileName
  95. * @returns {string}
  96. */
  97. export const getSvgName = (fileName) => {
  98. return path.basename(fileName, '.svg')
  99. }
  100. /**
  101. * Convert string to CamelCase
  102. * @param string
  103. * @returns {*}
  104. */
  105. export const toCamelCase = (string) => {
  106. return string.replace(/^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase())
  107. }
  108. export const toPascalCase = (string) => {
  109. const camelCase = toCamelCase(string);
  110. return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
  111. }
  112. export const addFloats = function (n1, n2) {
  113. return Math.round((parseFloat(n1) + parseFloat(n2)) * 1000) / 1000
  114. }
  115. export const optimizePath = function (path) {
  116. let transformed = svgpath(path).rel().round(3).toString()
  117. return svgParse(transformed).map(function (a) {
  118. return a.join(' ')
  119. }).join('')
  120. }
  121. export const optimizeSVG = (data) => {
  122. return optimize(data, {
  123. js2svg: {
  124. indent: 2,
  125. pretty: true
  126. },
  127. plugins: [
  128. {
  129. name: 'preset-default',
  130. params: {
  131. overrides: {
  132. mergePaths: false
  133. }
  134. }
  135. }]
  136. }).data
  137. }
  138. export function buildIconsObject(svgFiles, getSvg) {
  139. return svgFiles
  140. .map(svgFile => {
  141. const name = path.basename(svgFile, '.svg');
  142. const svg = getSvg(svgFile);
  143. const contents = getSvgContents(svg);
  144. return { name, contents };
  145. })
  146. .reduce((icons, icon) => {
  147. icons[icon.name] = icon.contents;
  148. return icons;
  149. }, {});
  150. }
  151. function getSvgContents(svg) {
  152. const $ = cheerio.load(svg);
  153. return minify($('svg').html(), { collapseWhitespace: true });
  154. }
  155. export const asyncForEach = async (array, callback) => {
  156. for (let index = 0; index < array.length; index++) {
  157. await callback(array[index], index, array)
  158. }
  159. }
  160. export const createScreenshot = async (filePath) => {
  161. await cp.exec(`rsvg-convert -x 2 -y 2 ${filePath} > ${filePath.replace('.svg', '.png')}`)
  162. await cp.exec(`rsvg-convert -x 4 -y 4 ${filePath} > ${filePath.replace('.svg', '@2x.png')}`)
  163. }
  164. export const generateIconsPreview = async function (files, destFile, {
  165. columnsCount = 19,
  166. paddingOuter = 7,
  167. color = '#354052',
  168. background = '#fff',
  169. png = true
  170. } = {}) {
  171. const padding = 20,
  172. iconSize = 24
  173. const iconsCount = files.length,
  174. rowsCount = Math.ceil(iconsCount / columnsCount),
  175. width = columnsCount * (iconSize + padding) + 2 * paddingOuter - padding,
  176. height = rowsCount * (iconSize + padding) + 2 * paddingOuter - padding
  177. let svgContentSymbols = '',
  178. svgContentIcons = '',
  179. x = paddingOuter,
  180. y = paddingOuter
  181. files.forEach(function (file, i) {
  182. let name = path.basename(file, '.svg')
  183. let svgFile = fs.readFileSync(file),
  184. svgFileContent = svgFile.toString()
  185. svgFileContent = svgFileContent.replace('<svg xmlns="http://www.w3.org/2000/svg"', `<symbol id="${name}"`)
  186. .replace(' width="24" height="24"', '')
  187. .replace('</svg>', '</symbol>')
  188. .replace(/\n\s+/g, '')
  189. svgContentSymbols += `\t${svgFileContent}\n`
  190. svgContentIcons += `\t<use xlink:href="#${name}" x="${x}" y="${y}" width="${iconSize}" height="${iconSize}" />\n`
  191. x += padding + iconSize
  192. if (i % columnsCount === columnsCount - 1) {
  193. x = paddingOuter
  194. y += padding + iconSize
  195. }
  196. })
  197. 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>`
  198. fs.writeFileSync(destFile, svgContent)
  199. if (png) {
  200. await createScreenshot(destFile)
  201. }
  202. }
  203. export const printChangelog = function (newIcons, modifiedIcons, renamedIcons, pretty = false) {
  204. if (newIcons.length > 0) {
  205. if (pretty) {
  206. console.log(`### ${newIcons.length} new icons:\n`)
  207. newIcons.forEach(function (icon, i) {
  208. console.log(`- \`${icon}\``)
  209. })
  210. } else {
  211. let str = ''
  212. str += `${newIcons.length} new icons: `
  213. newIcons.forEach(function (icon, i) {
  214. str += `\`${icon}\``
  215. if ((i + 1) <= newIcons.length - 1) {
  216. str += ', '
  217. }
  218. })
  219. console.log(str)
  220. }
  221. console.log('')
  222. }
  223. if (modifiedIcons.length > 0) {
  224. let str = ''
  225. str += `Fixed icons: `
  226. modifiedIcons.forEach(function (icon, i) {
  227. str += `\`${icon}\``
  228. if ((i + 1) <= modifiedIcons.length - 1) {
  229. str += ', '
  230. }
  231. })
  232. console.log(str)
  233. console.log('')
  234. }
  235. if (renamedIcons.length > 0) {
  236. console.log(`Renamed icons: `)
  237. renamedIcons.forEach(function (icon, i) {
  238. console.log(`- \`${icon[0]}\` renamed to \`${icon[1]}\``)
  239. })
  240. }
  241. }
  242. export const getCompileOptions = () => {
  243. const compileOptions = {
  244. includeIcons: [],
  245. strokeWidth: null,
  246. fontForge: 'fontforge'
  247. }
  248. if (fs.existsSync('../compile-options.json')) {
  249. try {
  250. const tempOptions = JSON.parse(fs.readFileSync('../compile-options.json').toString())
  251. if (typeof tempOptions !== 'object') {
  252. throw 'Compile options file does not contain an json object'
  253. }
  254. if (typeof tempOptions.includeIcons !== 'undefined') {
  255. if (!Array.isArray(tempOptions.includeIcons)) {
  256. throw 'property inludeIcons is not an array'
  257. }
  258. compileOptions.includeIcons = tempOptions.includeIcons
  259. }
  260. if (typeof tempOptions.includeCategories !== 'undefined') {
  261. if (typeof tempOptions.includeCategories === 'string') {
  262. tempOptions.includeCategories = tempOptions.includeCategories.split(' ')
  263. }
  264. if (!Array.isArray(tempOptions.includeCategories)) {
  265. throw 'property includeCategories is not an array or string'
  266. }
  267. const tags = Object.entries(require('./tags.json'))
  268. tempOptions.includeCategories.forEach(function (category) {
  269. category = category.charAt(0).toUpperCase() + category.slice(1)
  270. for (const [icon, data] of tags) {
  271. if (data.category === category && compileOptions.includeIcons.indexOf(icon) === -1) {
  272. compileOptions.includeIcons.push(icon)
  273. }
  274. }
  275. })
  276. }
  277. if (typeof tempOptions.excludeIcons !== 'undefined') {
  278. if (!Array.isArray(tempOptions.excludeIcons)) {
  279. throw 'property excludeIcons is not an array'
  280. }
  281. compileOptions.includeIcons = compileOptions.includeIcons.filter(function (icon) {
  282. return tempOptions.excludeIcons.indexOf(icon) === -1
  283. })
  284. }
  285. if (typeof tempOptions.excludeOffIcons !== 'undefined' && tempOptions.excludeOffIcons) {
  286. // Exclude `*-off` icons
  287. compileOptions.includeIcons = compileOptions.includeIcons.filter(function (icon) {
  288. return !icon.endsWith('-off')
  289. })
  290. }
  291. if (typeof tempOptions.strokeWidth !== 'undefined') {
  292. if (typeof tempOptions.strokeWidth !== 'string' && typeof tempOptions.strokeWidth !== 'number') {
  293. throw 'property strokeWidth is not a string or number'
  294. }
  295. compileOptions.strokeWidth = tempOptions.strokeWidth.toString()
  296. }
  297. if (typeof tempOptions.fontForge !== 'undefined') {
  298. if (typeof tempOptions.fontForge !== 'string') {
  299. throw 'property fontForge is not a string'
  300. }
  301. compileOptions.fontForge = tempOptions.fontForge
  302. }
  303. } catch (error) {
  304. throw `Error reading compile-options.json: ${error}`
  305. }
  306. }
  307. return compileOptions
  308. }