helpers.mjs 9.5 KB

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