Browse Source

Merge branch 'master' of https://github.com/tabler/tabler-icons into dev

 Conflicts:
	.gitignore
codecalm 2 years ago
parent
commit
6cc3ac23e7

+ 107 - 0
.build/build-icons.mjs

@@ -0,0 +1,107 @@
+import fs from 'fs-extra'
+import path from 'path'
+import { PACKAGES_DIR, readSvgs } from './helpers.mjs'
+import { stringify } from 'svgson'
+import prettier from 'prettier'
+
+import bundleSize from '@atomico/rollup-plugin-sizes'
+import { visualizer } from 'rollup-plugin-visualizer'
+import license from 'rollup-plugin-license'
+import esbuild from 'rollup-plugin-esbuild';
+
+
+/**
+ * Build icons
+ *
+ * @param name
+ * @param componentTemplate
+ * @param indexIconTemplate
+ * @param typeDefinitionsTemplate
+ * @param indexTypeTemplate
+ * @param ext
+ * @param pretty
+ */
+export const buildIcons = ({
+  name,
+  componentTemplate,
+  indexItemTemplate,
+  typeDefinitionsTemplate,
+  indexTypeTemplate,
+  extension = 'js',
+  pretty = true,
+  key = true
+}) => {
+  const DIST_DIR = path.resolve(PACKAGES_DIR, name),
+      svgFiles = readSvgs()
+
+  let index = []
+  let typings = []
+
+  svgFiles.forEach((svgFile, i) => {
+    const children = svgFile.obj.children
+        .map(({
+          name,
+          attributes
+        }, i) => {
+          if (key) {
+            attributes.key = `svg-${i}`
+          }
+
+          return [name, attributes]
+        })
+        .filter((i) => {
+          const [name, attributes] = i
+          return !attributes.d || attributes.d !== 'M0 0h24v24H0z'
+        })
+
+    // process.stdout.write(`Building ${i}/${svgFiles.length}: ${svgFile.name.padEnd(42)}\r`)
+
+    let component = componentTemplate({
+      name: svgFile.name,
+      namePascal: svgFile.namePascal,
+      children,
+      stringify,
+      svg: svgFile
+    })
+
+    const output = pretty ? prettier.format(component, {
+      singleQuote: true,
+      trailingComma: 'all',
+      parser: 'babel'
+    }) : component
+
+    let filePath = path.resolve(DIST_DIR, 'src/icons', `${svgFile.name}.${extension}`)
+    fs.writeFileSync(filePath, output, 'utf-8')
+
+    index.push(indexItemTemplate({
+      name: svgFile.name,
+      namePascal: svgFile.namePascal
+    }))
+
+    typings.push(indexTypeTemplate({
+      name: svgFile.name,
+      namePascal: svgFile.namePascal
+    }))
+  })
+
+  fs.writeFileSync(path.resolve(DIST_DIR, `./src/icons.js`), index.join('\n'), 'utf-8')
+
+  fs.ensureDirSync(path.resolve(DIST_DIR, `./dist/`))
+  fs.writeFileSync(path.resolve(DIST_DIR, `./dist/tabler-${name}.d.ts`), typeDefinitionsTemplate() + '\n' + typings.join('\n'), 'utf-8')
+}
+
+export const getRollupPlugins = (pkg, minify) => {
+  return [
+    esbuild({
+      minify,
+    }),
+    license({
+      banner: `${pkg.name} v${pkg.version} - ${pkg.license}`
+    }),
+    bundleSize(),
+    visualizer({
+      sourcemap: false,
+      filename: `stats/${pkg.name}${minify ? '-min' : ''}.html`
+    })
+  ].filter(Boolean)
+}

+ 24 - 0
.build/changelog-commit.mjs

@@ -0,0 +1,24 @@
+import cp from 'child_process'
+import { printChangelog } from './helpers.mjs'
+
+cp.exec('git status', function(err, ret) {
+  let newIcons = [], modifiedIcons = [], renamedIcons = []
+
+  ret.replace(/new file:\s+src\/_icons\/([a-z0-9-]+)\.svg/g, function(m, fileName) {
+    newIcons.push(fileName)
+  })
+
+  ret.replace(/modified:\s+src\/_icons\/([a-z0-9-]+)\.svg/g, function(m, fileName) {
+    modifiedIcons.push(fileName)
+  })
+
+  ret.replace(/renamed:\s+src\/_icons\/([a-z0-9-]+).svg -> src\/_icons\/([a-z0-9-]+).svg/g, function(m, fileNameBefore, fileNameAfter) {
+    renamedIcons.push([fileNameBefore, fileNameAfter])
+  })
+
+  modifiedIcons = modifiedIcons.filter(function(el) {
+    return newIcons.indexOf(el) < 0
+  })
+
+  printChangelog(newIcons, modifiedIcons, renamedIcons)
+})

+ 27 - 0
.build/changelog-image.mjs

@@ -0,0 +1,27 @@
+import { generateIconsPreview, getArgvs, getPackageJson, HOME_DIR } from './helpers.mjs'
+import * as fs from 'fs'
+
+const argv = getArgvs(),
+    p = getPackageJson()
+
+const version = argv['new-version'] || `${p.version}`
+
+if (version) {
+  const icons = JSON.parse(fs.readFileSync(`${HOME_DIR}/tags.json`))
+
+  const newIcons = Object
+      .entries(icons)
+      .filter(([name, value]) => {
+        return `${value.version}.0` === version
+      })
+      .map(([name, value]) => {
+        return `./icons/${name}.svg`
+      })
+
+  if (newIcons.length > 0) {
+    generateIconsPreview(newIcons, `.github/tabler-icons-${version}.svg`, {
+      columnsCount: 6,
+      paddingOuter: 24
+    })
+  }
+}

+ 31 - 0
.build/changelog.mjs

@@ -0,0 +1,31 @@
+import cp from 'child_process'
+import { getArgvs, getPackageJson, printChangelog } from './helpers.mjs'
+
+const p = getPackageJson(),
+    argv = getArgvs(),
+    version = argv['latest-version'] || `${p.version}`
+
+if (version) {
+  cp.exec(`git diff ${version} HEAD --name-status src/_icons`, function(err, ret) {
+
+    let newIcons = [], modifiedIcons = [], renamedIcons = []
+
+    ret.replace(/A\s+src\/_icons\/([a-z0-9-]+)\.svg/g, function(m, fileName) {
+      newIcons.push(fileName)
+    })
+
+    ret.replace(/M\s+src\/_icons\/([a-z0-9-]+)\.svg/g, function(m, fileName) {
+      modifiedIcons.push(fileName)
+    })
+
+    ret.replace(/R[0-9]+\s+src\/_icons\/([a-z0-9-]+)\.svg\s+src\/_icons\/([a-z0-9-]+).svg/g, function(m, fileNameBefore, fileNameAfter) {
+      renamedIcons.push([fileNameBefore, fileNameAfter])
+    })
+
+    modifiedIcons = modifiedIcons.filter(function(el) {
+      return newIcons.indexOf(el) < 0
+    })
+
+    printChangelog(newIcons, modifiedIcons, renamedIcons, true)
+  })
+}

+ 353 - 0
.build/helpers.mjs

@@ -0,0 +1,353 @@
+import fs from 'fs'
+import path, { resolve, basename } from 'path'
+import { fileURLToPath } from 'url'
+import svgParse from 'parse-svg-path'
+import svgpath from 'svgpath'
+import cheerio from 'cheerio';
+import { minify } from 'html-minifier';
+import { parseSync } from 'svgson'
+import { optimize } from 'svgo'
+import cp from 'child_process'
+import minimist from 'minimist'
+
+export const getCurrentDirPath = () => {
+  return path.dirname(fileURLToPath(import.meta.url));
+}
+
+export const HOME_DIR = resolve(getCurrentDirPath(), '..')
+
+export const ICONS_SRC_DIR = resolve(HOME_DIR, 'src/_icons')
+export const ICONS_DIR = resolve(HOME_DIR, 'icons')
+export const PACKAGES_DIR = resolve(HOME_DIR, 'packages')
+
+export const getArgvs = () => {
+  return minimist(process.argv.slice(2))
+}
+
+export const getPackageDir = (packageName) => {
+  return `${PACKAGES_DIR}/${packageName}`
+}
+
+/**
+ * Return project package.json
+ * @returns {any}
+ */
+export const getPackageJson = () => {
+  return JSON.parse(fs.readFileSync(resolve(HOME_DIR, 'package.json'), 'utf-8'))
+}
+
+/**
+ * Reads SVGs from directory
+ *
+ * @param directory
+ * @returns {string[]}
+ */
+export const readSvgDirectory = (directory) => {
+  return fs.readdirSync(directory).filter((file) => path.extname(file) === '.svg')
+}
+
+export const readSvgs = () => {
+  const svgFiles = readSvgDirectory(ICONS_DIR)
+
+  return svgFiles.map(svgFile => {
+    const name = basename(svgFile, '.svg'),
+        namePascal = toPascalCase(`icon ${name}`),
+        contents = readSvg(svgFile, ICONS_DIR).trim(),
+        path = resolve(ICONS_DIR, svgFile),
+        obj = parseSync(contents.replace('<path stroke="none" d="M0 0h24v24H0z" fill="none"/>', ''));
+
+    return {
+      name,
+      namePascal,
+      contents,
+      obj,
+      path
+    };
+  });
+}
+
+/**
+ * Read SVG
+ *
+ * @param fileName
+ * @param directory
+ * @returns {string}
+ */
+export const readSvg = (fileName, directory) => {
+  return fs.readFileSync(path.join(directory, fileName), 'utf-8')
+}
+
+/**
+ * Create directory if not exists
+ * @param dir
+ */
+export const createDirectory = (dir) => {
+  if (!fs.existsSync(dir)) {
+    fs.mkdirSync(dir);
+  }
+};
+
+/**
+ * Get SVG name
+ * @param fileName
+ * @returns {string}
+ */
+export const getSvgName = (fileName) => {
+  return path.basename(fileName, '.svg')
+}
+
+/**
+ * Convert string to CamelCase
+ * @param string
+ * @returns {*}
+ */
+export const toCamelCase = (string) => {
+  return string.replace(/^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase())
+}
+
+export const toPascalCase = (string) => {
+  const camelCase = toCamelCase(string);
+
+  return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
+}
+
+
+
+export const addFloats = function(n1, n2) {
+  return Math.round((parseFloat(n1) + parseFloat(n2)) * 1000) / 1000
+}
+
+export const optimizePath = function(path) {
+  let transformed = svgpath(path).rel().round(3).toString()
+
+  return svgParse(transformed).map(function(a) {
+    return a.join(' ')
+  }).join(' ')
+}
+
+export const optimizeSVG = (data) => {
+  return optimize(data, {
+    js2svg: {
+      indent: 2,
+      pretty: true
+    },
+    plugins: [
+      {
+        name: 'preset-default',
+        params: {
+          overrides: {
+            mergePaths: false
+          }
+        }
+      }]
+  }).data
+}
+
+export function buildIconsObject(svgFiles, getSvg) {
+  return svgFiles
+      .map(svgFile => {
+        const name = path.basename(svgFile, '.svg');
+        const svg = getSvg(svgFile);
+        const contents = getSvgContents(svg);
+        return { name, contents };
+      })
+      .reduce((icons, icon) => {
+        icons[icon.name] = icon.contents;
+        return icons;
+      }, {});
+}
+
+function getSvgContents(svg) {
+  const $ = cheerio.load(svg);
+  return minify($('svg').html(), { collapseWhitespace: true });
+}
+
+export const asyncForEach = async (array, callback) => {
+  for (let index = 0; index < array.length; index++) {
+    await callback(array[index], index, array)
+  }
+}
+
+export const createScreenshot = async (filePath) => {
+  await cp.exec(`rsvg-convert -x 2 -y 2 ${filePath} > ${filePath.replace('.svg', '.png')}`)
+  await cp.exec(`rsvg-convert -x 4 -y 4 ${filePath} > ${filePath.replace('.svg', '@2x.png')}`)
+}
+
+export const generateIconsPreview = async function(files, destFile, {
+  columnsCount = 19,
+  paddingOuter = 7,
+  color = '#354052',
+  background = '#fff'
+} = {}) {
+
+  const padding = 20,
+      iconSize = 24
+
+  const iconsCount = files.length,
+      rowsCount = Math.ceil(iconsCount / columnsCount),
+      width = columnsCount * (iconSize + padding) + 2 * paddingOuter - padding,
+      height = rowsCount * (iconSize + padding) + 2 * paddingOuter - padding
+
+  let svgContentSymbols = '',
+      svgContentIcons = '',
+      x = paddingOuter,
+      y = paddingOuter
+
+  files.forEach(function(file, i) {
+    let name = path.basename(file, '.svg')
+
+    let svgFile = fs.readFileSync(file),
+        svgFileContent = svgFile.toString()
+
+    svgFileContent = svgFileContent.replace('<svg xmlns="http://www.w3.org/2000/svg"', `<symbol id="${name}"`)
+        .replace(' width="24" height="24"', '')
+        .replace('</svg>', '</symbol>')
+        .replace(/\n\s+/g, '')
+
+    svgContentSymbols += `\t${svgFileContent}\n`
+    svgContentIcons += `\t<use xlink:href="#${name}" x="${x}" y="${y}" width="${iconSize}" height="${iconSize}" />\n`
+
+    x += padding + iconSize
+
+    if (i % columnsCount === columnsCount - 1) {
+      x = paddingOuter
+      y += padding + iconSize
+    }
+  })
+
+  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>`
+
+  fs.writeFileSync(destFile, svgContent)
+  await createScreenshot(destFile)
+}
+
+
+export const printChangelog = function(newIcons, modifiedIcons, renamedIcons, pretty = false) {
+  if (newIcons.length > 0) {
+    if (pretty) {
+      console.log(`### ${newIcons.length} new icons:\n`)
+
+      newIcons.forEach(function(icon, i) {
+        console.log(`- \`${icon}\``)
+      })
+    } else {
+      let str = ''
+      str += `${newIcons.length} new icons: `
+
+      newIcons.forEach(function(icon, i) {
+        str += `\`${icon}\``
+
+        if ((i + 1) <= newIcons.length - 1) {
+          str += ', '
+        }
+      })
+
+      console.log(str)
+    }
+
+    console.log('')
+  }
+
+  if (modifiedIcons.length > 0) {
+    let str = ''
+    str += `Fixed icons: `
+
+    modifiedIcons.forEach(function(icon, i) {
+      str += `\`${icon}\``
+
+      if ((i + 1) <= modifiedIcons.length - 1) {
+        str += ', '
+      }
+    })
+
+    console.log(str)
+    console.log('')
+  }
+
+  if (renamedIcons.length > 0) {
+    console.log(`Renamed icons: `)
+
+    renamedIcons.forEach(function(icon, i) {
+      console.log(`- \`${icon[0]}\` renamed to \`${icon[1]}\``)
+    })
+  }
+}
+
+
+export const getCompileOptions = () => {
+  const compileOptions = {
+    includeIcons: [],
+    strokeWidth: null,
+    fontForge: 'fontforge'
+  }
+
+  if (fs.existsSync('../compile-options.json')) {
+    try {
+      const tempOptions = require('../compile-options.json')
+
+      if (typeof tempOptions !== 'object') {
+        throw 'Compile options file does not contain an json object'
+      }
+
+      if (typeof tempOptions.includeIcons !== 'undefined') {
+        if (!Array.isArray(tempOptions.includeIcons)) {
+          throw 'property inludeIcons is not an array'
+        }
+        compileOptions.includeIcons = tempOptions.includeIcons
+      }
+
+      if (typeof tempOptions.includeCategories !== 'undefined') {
+        if (typeof tempOptions.includeCategories === 'string') {
+          tempOptions.includeCategories = tempOptions.includeCategories.split(' ')
+        }
+        if (!Array.isArray(tempOptions.includeCategories)) {
+          throw 'property includeCategories is not an array or string'
+        }
+        const tags = Object.entries(require('./tags.json'))
+        tempOptions.includeCategories.forEach(function(category) {
+          category = category.charAt(0).toUpperCase() + category.slice(1)
+          for (const [icon, data] of tags) {
+            if (data.category === category && compileOptions.includeIcons.indexOf(icon) === -1) {
+              compileOptions.includeIcons.push(icon)
+            }
+          }
+        })
+      }
+
+      if (typeof tempOptions.excludeIcons !== 'undefined') {
+        if (!Array.isArray(tempOptions.excludeIcons)) {
+          throw 'property excludeIcons is not an array'
+        }
+        compileOptions.includeIcons = compileOptions.includeIcons.filter(function(icon) {
+          return tempOptions.excludeIcons.indexOf(icon) === -1
+        })
+      }
+
+      if (typeof tempOptions.excludeOffIcons !== 'undefined' && tempOptions.excludeOffIcons) {
+        // Exclude `*-off` icons
+        compileOptions.includeIcons = compileOptions.includeIcons.filter(function(icon) {
+          return !icon.endsWith('-off')
+        })
+      }
+
+      if (typeof tempOptions.strokeWidth !== 'undefined') {
+        if (typeof tempOptions.strokeWidth !== 'string' && typeof tempOptions.strokeWidth !== 'number') {
+          throw 'property strokeWidth is not a string or number'
+        }
+        compileOptions.strokeWidth = tempOptions.strokeWidth.toString()
+      }
+
+      if (typeof tempOptions.fontForge !== 'undefined') {
+        if (typeof tempOptions.fontForge !== 'string') {
+          throw 'property fontForge is not a string'
+        }
+        compileOptions.fontForge = tempOptions.fontForge
+      }
+
+    } catch (error) {
+      throw `Error reading compile-options.json: ${error}`
+    }
+  }
+
+  return compileOptions
+}

+ 38 - 0
.build/import-categories.mjs

@@ -0,0 +1,38 @@
+import glob from 'glob'
+import fs from 'fs'
+import { resolve, join, basename } from 'path'
+import { ICONS_SRC_DIR } from './helpers.mjs'
+
+
+glob.sync(join(ICONS_SRC_DIR, '*-off.svg')).forEach(function(file, i) {
+  const fileOriginal = file.replace(/\-off.svg$/, '.svg')
+
+  if (fs.existsSync(fileOriginal)) {
+    const dataOriginal = fs.readFileSync(fileOriginal).toString()
+
+    const categoryOriginal = dataOriginal.match(/category: ([a-zA-Z-]+)/),
+        tagsOriginal = dataOriginal.match(/tags: (\[.*?\])/)
+
+    if (categoryOriginal || tagsOriginal) {
+
+      let data = fs.readFileSync(file).toString()
+      data = data.replace(/(---[\s\S]+?---)/, function(m, headerContent) {
+        console.log('categoryOriginal', fileOriginal, categoryOriginal && categoryOriginal[1], tagsOriginal && tagsOriginal[1])
+
+        if (categoryOriginal) {
+          headerContent = headerContent.replace(/category: .*\n/, '')
+          headerContent = headerContent.replace(/---/, `---\ncategory: ${categoryOriginal[1]}`)
+        }
+
+        if (tagsOriginal) {
+          headerContent = headerContent.replace(/tags: .*\n/, '')
+          headerContent = headerContent.replace(/---/, `---\ntags: ${tagsOriginal[1]}`)
+        }
+
+        return headerContent
+      })
+
+      fs.writeFileSync(file, data)
+    }
+  }
+})

+ 30 - 0
.build/import-tags.mjs

@@ -0,0 +1,30 @@
+import fs from 'fs'
+import csv from 'csv-parser'
+import { join } from 'path'
+import { HOME_DIR } from './helpers.mjs'
+
+
+fs.createReadStream(join(HOME_DIR, '_import.tsv')).pipe(csv({
+  headers: false,
+  separator: '\t'
+})).on('data', (row) => {
+  console.log(row[1], row[2])
+
+  const filename = join(HOME_DIR, `src/_icons/${row[1]}.svg`)
+
+  if(row[2].length) {
+    let data = fs.readFileSync(filename).toString()
+    data = data.replace(/(---[\s\S]+?---)/, function(m, headerContent) {
+
+      headerContent = headerContent.replace(/tags: .*\n/, '')
+      headerContent = headerContent.replace(/---/, `---\ntags: [${row[2]}]`)
+
+      return headerContent
+    })
+
+    fs.writeFileSync(filename, data)
+  }
+
+}).on('end', () => {
+  console.log('CSV file successfully processed')
+})

+ 60 - 0
.build/import.mjs

@@ -0,0 +1,60 @@
+import fs from 'fs'
+import glob from 'glob'
+import { resolve, basename } from 'path'
+import { HOME_DIR, optimizeSVG } from './helpers.mjs'
+
+
+const files = glob.sync(resolve(HOME_DIR, './new/*.svg'))
+
+files.forEach(function(file, i) {
+  let fileData = fs.readFileSync(file).toString(),
+      filename = basename(file, '.svg')
+
+  console.log(filename)
+
+  fileData = optimizeSVG(fileData)
+
+  if (fileData.match(/transform="/)) {
+    throw new Error(`File ${file} has \`transform\` in code!!`)
+  }
+
+  if (filename.match(/\s/)) {
+    throw new Error(`File ${file} has space in name!!`)
+  }
+
+  fileData = fileData.replace(/---/g, '')
+      .replace(/fill="none"/g, '')
+      .replace(/fill="#D8D8D8"/gi, '')
+      .replace(/fill-rule="evenodd"/g, '')
+      .replace(/stroke-linecap="round"/g, '')
+      .replace(/stroke-linejoin="round"/g, '')
+      .replace(/viewBox="0 0 24 24"/g, '')
+      .replace(/stroke="#000000"/g, '')
+      .replace(/stroke="#000"/g, '')
+      .replace(/stroke-width="2"/g, '')
+      .replace(/width="24"/g, '')
+      .replace(/width="24px"/g, '')
+      .replace(/height="24"/g, '')
+      .replace(/height="24px"/g, '')
+      .replace(/xmlns="http:\/\/www.w3.org\/2000\/svg"/g, '')
+      .replace(/<path d="M0 0h24v24H0z"\/>"/g, '')
+      .replace(/<path stroke="red" stroke-width=".1" d="[^"]+"\s?\/>/g, '')
+      .replace(/<path[^>]*stroke="red"[^>]*\/>/gs, '')
+      .replace(/<circle[^>]*stroke="red"[^>]*\/>/gs, '')
+      .replace(/<g[^>]*stroke="red"[^>]*>.*?<\/g>/gs, '')
+
+  fileData = optimizeSVG(fileData)
+
+  fileData = fileData.replace(/<svg>/g, '---\n---\n<svg>')
+
+  if (fs.existsSync(`./src/_icons/${filename}.svg`)) {
+    const newFileData = fs.readFileSync(`./src/_icons/${filename}.svg`).toString()
+    const m = newFileData.match(/(---.*---)/gms)
+
+    if (m) {
+      fileData = fileData.replace('---\n---', m[0])
+    }
+  }
+
+  fs.writeFileSync(`./src/_icons/${filename}.svg`, fileData)
+})

+ 98 - 0
.build/optimize.mjs

@@ -0,0 +1,98 @@
+import glob from 'glob'
+import { readFileSync, writeFileSync } from 'fs'
+import { join, basename } from 'path'
+import { optimizePath, ICONS_SRC_DIR } from './helpers.mjs'
+
+
+glob(join(ICONS_SRC_DIR, '*.svg'), {}, function(er, files) {
+
+  files.forEach(function(file, i) {
+    console.log(`Optimize ${basename(file)}`);
+
+    let svgFile = readFileSync(file),
+        svgFileContent = svgFile.toString()
+
+    svgFileContent = svgFileContent.replace(/><\/(polyline|line|rect|circle|path|ellipse)>/g, '/>')
+        .replace(/rx="([^"]+)"\s+ry="\1"/g, 'rx="$1"')
+        .replace(/<path stroke="red" stroke-width="\.1"([^>]+)?\/>/g, '')
+        .replace(/\s?\/>/g, ' />')
+        .replace(/\n\s*<(line|circle|path|polyline|rect|ellipse)/g, '\n  <$1')
+        // .replace(/polyline points="([0-9.]+)\s([0-9.]+)\s([0-9.]+)\s([0-9.]+)"/g, 'line x1="$1" y1="$2" x2="$3" y2="$4"')
+        .replace(/<line x1="([^"]+)" y1="([^"]+)" x2="([^"]+)" y2="([^"]+)"\s*\/>/g, function(f, x1, y1, x2, y2) {
+          return `<path d="M${x1} ${y1}L${x2} ${y2}" />`
+        })
+        .replace(/<circle cx="([^"]+)" cy="([^"]+)" r="([^"]+)"\s+\/>/g, function(f, cx, cy, r) {
+          return `<path d="M ${cx} ${cy}m -${r} 0a ${r} ${r} 0 1 0 ${r * 2} 0a ${r} ${r} 0 1 0 ${r * -2} 0" />`
+        })
+        .replace(/<ellipse cx="([^"]+)" cy="([^"]+)" rx="([^"]+)"\s+\/>/g, function(f, cx, cy, rx) {
+          return `<ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="${rx}" />`
+        })
+        .replace(/<ellipse cx="([^"]+)" cy="([^"]+)" rx="([^"]+)" ry="([^"]+)"\s+\/>/g, function(f, cx, cy, rx, ry) {
+          return `<path d="M${cx} ${cy}m -${rx} 0a${rx} ${ry} 0 1 0 ${rx * 2} 0a ${rx} ${ry} 0 1 0 -${rx * 2} 0" />`
+        })
+        .replace(/<rect width="([^"]+)" height="([^"]+)" x="([^"]+)" y="([^"]+)" rx="([^"]+)"\s+\/>/g, function(f, width, height, x, y, rx) {
+          return `<rect x="${x}" y="${y}" width="${height}" height="${height}" rx="${rx}" />`
+        })
+        .replace(/<rect x="([^"]+)" y="([^"]+)" rx="([^"]+)" width="([^"]+)" height="([^"]+)"\s+\/>/g, function(f, x, y, rx, width, height) {
+          return `<rect x="${x}" y="${y}" width="${height}" height="${height}" rx="${rx}" />`
+        })
+        .replace(/<rect x="([^"]+)" y="([^"]+)" width="([^"]+)" height="([^"]+)" rx="([^"]+)"\s+\/>/g, function(f, x, y, width, height, rx) {
+          return `<path d="M ${x} ${y}m 0 ${rx}a${rx} ${rx} 0 0 1 ${rx} ${-rx}h${width - rx * 2}a${rx} ${rx} 0 0 1 ${rx} ${rx}v${height - rx *
+          2}a${rx} ${rx} 0 0 1 ${-rx} ${rx}h${-width + rx * 2}a${rx} ${rx} 0 0 1 ${-rx} ${-rx}Z" />`
+        })
+        .replace(/<rect x="([^"]+)" y="([^"]+)" width="([^"]+)" height="([^"]+)"\s+\/>/g, function(f, x, y, width, height) {
+          return `<path d="M ${x} ${y}h${width}v${height}h${-width}Z" />`
+        })
+        .replace(/<polyline points="([^"]+)\s?"\s+\/>/g, function(f, points) {
+          const path = points.split(' ').reduce(
+              (accumulator, currentValue, currentIndex) => `${accumulator}${currentIndex % 2 === 0 ? (currentIndex === 0 ? 'M' : 'L') : ''}${currentValue} `,
+              ''
+          )
+          return `<path d="${path}" />`
+        })
+        .replace(/<path\s+d="([^"]+)"/g, function(f, d) {
+
+          const d2 = d
+              .replace(/([0-9]+)+\.00[1-6]/g, (f, m) => `${m}`)
+              .replace(/([0-9]+)+\.99[4-9]/g, (f, m) => `${parseInt(m) + 1}`)
+              .replace(/\.99[4-9]/g, (f, m) => `1`)
+              .replace(/-\.00[1-6]/g, (f, m) => `0`)
+              .replace(/\.00[1-6]/g, (f, m) => `0`)
+              .replace(/m0 0/g, (f, m) => ``)
+
+          return `<path d="${d2}"`
+        })
+        // .replace(/(?<=M[^"]+)"\s+\/>[\n\s\t]+<path d="M(?=([^"]+)"\s+\/>)/g, function() {
+        //    return `M`
+        // })
+        .replace(/<path d="([^"]+)"/g, function(f, r1) {
+          r1 = optimizePath(r1)
+
+          return `<path d="${r1}"`
+        })
+        .replace(/d="m/g, 'd="M')
+        .replace(/([Aa])\s?([0-9.]+)[\s,]([0-9.]+)[\s,]([0-9.]+)[\s,]?([0-1])[\s,]?([0-1])[\s,]?(-?[0-9.]+)[\s,]?(-?[0-9.]+)/gi, '$1$2 $3 $4 $5 $6 $7 $8')
+        .replace(/\n\s+\n+/g, '\n')
+        .replace(/<path d="M([0-9.]*) ([0-9.]*)l\s?([-0-9.]*) ([-0-9.]*)"/g, function(f, r1, r2, r3, r4) {
+          return `<line x1="${r1}" y1="${r2}" x2="${addFloats(r1, r3)}" y2="${addFloats(r2, r4)}"`
+        })
+        .replace(/<path d="M([0-9.]*) ([0-9.]*)v\s?([-0-9.]*)"/g, function(f, r1, r2, r3) {
+          return `<line x1="${r1}" y1="${r2}" x2="${r1}" y2="${addFloats(r2, r3)}"`
+        })
+        .replace(/<path d="M([0-9.]*) ([0-9.]*)h\s?([-0-9.]*)"/g, function(f, r1, r2, r3) {
+          return `<line x1="${r1}" y1="${r2}" x2="${addFloats(r1, r3)}" y2="${r2}"`
+        })
+        .replace(/<path d="([^"]+)"/g, function(f, r1) {
+          r1 = r1.replace(/ -0\./g, ' -.').replace(/ 0\./g, ' .').replace(/\s([a-z])/gi, '$1').replace(/([a-z])\s/gi, '$1')
+          return `<path d="${r1}"`
+        })
+
+    if (!svgFileContent.match(/<svg>[\n\t\s]*<path d="([^"]+)"( fill="currentColor")? \/>[\n\t\s]*<\/svg>/)) {
+      console.log(`Fix ${file}!`);
+    }
+
+    if (svgFile.toString() !== svgFileContent) {
+      writeFileSync(file, svgFileContent)
+    }
+  })
+})

+ 10 - 0
.build/preview-icons.mjs

@@ -0,0 +1,10 @@
+import glob from 'glob'
+import { generateIconsPreview } from './helpers.mjs'
+
+glob('icons/*.svg', {}, async function(er, files) {
+  await generateIconsPreview(files, '.github/icons.svg')
+  await generateIconsPreview(files, '.github/icons-dark.svg', {
+    color: '#ffffff',
+    background: 'transparent'
+  })
+})

Some files were not shown because too many files changed in this diff