gulpfile.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. const gulp = require('gulp'),
  2. cp = require('child_process'),
  3. glob = require('glob'),
  4. fs = require('fs'),
  5. path = require('path'),
  6. p = require('./package.json'),
  7. zip = require('gulp-zip'),
  8. puppeteer = require('puppeteer'),
  9. outlineStroke = require('svg-outline-stroke'),
  10. iconfont = require('gulp-iconfont'),
  11. template = require('lodash.template'),
  12. sass = require('node-sass'),
  13. cleanCSS = require('clean-css'),
  14. argv = require('minimist')(process.argv.slice(2)),
  15. svgr = require('@svgr/core').default;
  16. async function asyncForEach(array, callback) {
  17. for (let index = 0; index < array.length; index++) {
  18. await callback(array[index], index, array);
  19. }
  20. }
  21. const svgToPng = async (filePath, destination) => {
  22. filePath = path.join(__dirname, filePath);
  23. const htmlFilePath = path.join("file:", filePath);
  24. const browser = await puppeteer.launch();
  25. const page = await browser.newPage();
  26. await page.setViewport({
  27. height: 24,
  28. width: 24,
  29. deviceScaleFactor: 10
  30. });
  31. await page.goto(htmlFilePath);
  32. await page.screenshot({
  33. path: path.join(__dirname, destination),
  34. omitBackground: true,
  35. fullPage: true
  36. });
  37. await browser.close();
  38. return page;
  39. };
  40. const createScreenshot = async (filePath) => {
  41. try {
  42. filePath = path.join(__dirname, filePath);
  43. const fileName = filePath.replace('.svg', '');
  44. const htmlFilePath = path.join("file:", filePath);
  45. const browser = await puppeteer.launch();
  46. const page = await browser.newPage();
  47. await page.setViewport({
  48. height: 10,
  49. width: 10,
  50. deviceScaleFactor: 2
  51. });
  52. await page.goto(htmlFilePath);
  53. await page.screenshot({
  54. path: `${fileName}.png`,
  55. omitBackground: false,
  56. fullPage: true
  57. });
  58. await browser.close();
  59. } catch (error) {
  60. console.error(error);
  61. throw Error(error);
  62. }
  63. };
  64. const printChangelog = function (newIcons, modifiedIcons, renamedIcons, pretty = false) {
  65. if (newIcons.length > 0) {
  66. if (pretty) {
  67. console.log(`### ${newIcons.length} new icons:`);
  68. newIcons.forEach(function (icon, i) {
  69. console.log(`- \`${icon}\``);
  70. });
  71. } else {
  72. let str = '';
  73. str += `${newIcons.length} new icons: `;
  74. newIcons.forEach(function (icon, i) {
  75. str += `\`${icon}\``;
  76. if ((i + 1) <= newIcons.length - 1) {
  77. str += ', '
  78. }
  79. });
  80. console.log(str);
  81. }
  82. console.log('');
  83. }
  84. if (modifiedIcons.length > 0) {
  85. let str = '';
  86. str += `Fixed icons: `;
  87. modifiedIcons.forEach(function (icon, i) {
  88. str += `\`${icon}\``;
  89. if ((i + 1) <= modifiedIcons.length - 1) {
  90. str += ', '
  91. }
  92. });
  93. console.log(str);
  94. console.log('');
  95. }
  96. if (renamedIcons.length > 0) {
  97. console.log(`Renamed icons: `);
  98. renamedIcons.forEach(function (icon, i) {
  99. console.log(`- \`${icon[0]}\` renamed to \`${icon[1]}\``);
  100. });
  101. }
  102. };
  103. const generateIconsPreview = function (files, destFile, cb, columnsCount = 17, paddingOuter = 7) {
  104. const padding = 26,
  105. iconSize = 24;
  106. const iconsCount = files.length,
  107. rowsCount = Math.ceil(iconsCount / columnsCount),
  108. width = columnsCount * (iconSize + padding) + 2 * paddingOuter - padding,
  109. height = rowsCount * (iconSize + padding) + 2 * paddingOuter - padding;
  110. let svgContentSymbols = '',
  111. svgContentIcons = '',
  112. x = paddingOuter,
  113. y = paddingOuter;
  114. files.forEach(function (file, i) {
  115. let name = path.basename(file, '.svg');
  116. let svgFile = fs.readFileSync(file),
  117. svgFileContent = svgFile.toString();
  118. svgFileContent = svgFileContent
  119. .replace('<svg xmlns="http://www.w3.org/2000/svg"', `<symbol id="${name}"`)
  120. .replace(' width="24" height="24"', '')
  121. .replace('</svg>', '</symbol>')
  122. .replace(/\n\s+/g, '');
  123. svgContentSymbols += `\t${svgFileContent}\n`;
  124. svgContentIcons += `\t<use xlink:href="#${name}" x="${x}" y="${y}" width="${iconSize}" height="${iconSize}" />\n`;
  125. x += padding + iconSize;
  126. if (i % columnsCount === columnsCount - 1) {
  127. x = paddingOuter;
  128. y += padding + iconSize;
  129. }
  130. });
  131. 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: #354052"><rect x="0" y="0" width="${width}" height="${height}" fill="#fff"></rect>\n${svgContentSymbols}\n${svgContentIcons}\n</svg>`;
  132. fs.writeFileSync(destFile, svgContent);
  133. createScreenshot(destFile);
  134. cb();
  135. };
  136. //*********************************************************************************************
  137. gulp.task('iconfont-prepare', function (cb) {
  138. cp.exec('mkdir -p icons-outlined/ && rm -fd ./icons-outlined/* && mkdir -p && rm -fd ./iconfont/*', function () {
  139. cb();
  140. });
  141. });
  142. gulp.task('iconfont-clean', function (cb) {
  143. cp.exec('rm -rf ./icons-outlined', function () {
  144. cb();
  145. });
  146. });
  147. gulp.task('iconfont-svg-outline', function (cb) {
  148. cp.exec('mkdir -p icons-outlined/ && rm -fd ./icons-outlined/*', async () => {
  149. let files = glob.sync("./icons/*.svg");
  150. let iconfontUnicode = {};
  151. if (fs.existsSync('./iconfont-unicode.json')) {
  152. iconfontUnicode = require('./iconfont-unicode');
  153. }
  154. await asyncForEach(files, async function (file) {
  155. const name = path.basename(file, '.svg'),
  156. unicode = iconfontUnicode[name];
  157. await console.log('Stroke for:', file, unicode);
  158. let strokedSVG = fs.readFileSync(file).toString();
  159. strokedSVG = strokedSVG
  160. .replace('width="24"', 'width="1000"')
  161. .replace('height="24"', 'height="1000"');
  162. await outlineStroke(strokedSVG, {
  163. optCurve: false,
  164. steps: 4,
  165. round: 0,
  166. centerHorizontally: true,
  167. fixedWidth: true,
  168. color: 'black'
  169. }).then(outlined => {
  170. if (unicode) {
  171. fs.writeFileSync(`icons-outlined/u${unicode.toUpperCase()}-${name}.svg`, outlined);
  172. } else {
  173. fs.writeFileSync(`icons-outlined/${name}.svg`, outlined);
  174. }
  175. }).catch(error => console.log(error));
  176. });
  177. cb();
  178. });
  179. });
  180. gulp.task('iconfont', function () {
  181. let maxUnicode = 59905;
  182. if (fs.existsSync('./iconfont-unicode.json')) {
  183. const iconfontUnicode = require('./iconfont-unicode');
  184. for (const name in iconfontUnicode) {
  185. const unicode = parseInt(iconfontUnicode[name], 16);
  186. maxUnicode = Math.max(maxUnicode, unicode);
  187. }
  188. }
  189. maxUnicode = maxUnicode + 1;
  190. return gulp.src(['icons-outlined/*.svg'])
  191. .pipe(iconfont({
  192. fontName: 'tabler-icons',
  193. prependUnicode: true,
  194. formats: ['ttf', 'eot', 'woff', 'woff2'],
  195. normalize: true,
  196. startUnicode: maxUnicode
  197. }))
  198. .on('glyphs', function (glyphs, options) {
  199. //glyphs json
  200. let glyphsObject = {};
  201. //sort glypht
  202. glyphs = glyphs.sort(function (a, b) {
  203. return ('' + a.name).localeCompare(b.name)
  204. });
  205. glyphs.forEach(function (glyph) {
  206. glyphsObject[glyph.name] = glyph.unicode[0].codePointAt(0).toString(16);
  207. });
  208. fs.writeFileSync(`iconfont-unicode.json`, JSON.stringify(glyphsObject));
  209. //css
  210. options['glyphs'] = glyphs;
  211. options['v'] = p.version;
  212. const compiled = template(fs.readFileSync('.build/iconfont.scss').toString());
  213. const result = compiled(options);
  214. fs.writeFileSync('iconfont/tabler-icons.scss', result);
  215. //html
  216. const compiledHtml = template(fs.readFileSync('.build/iconfont.html').toString());
  217. const resultHtml = compiledHtml(options);
  218. fs.writeFileSync('iconfont/tabler-icons.html', resultHtml);
  219. })
  220. .pipe(gulp.dest('iconfont/fonts'));
  221. });
  222. gulp.task('iconfont-css', function (cb) {
  223. sass.render({
  224. file: 'iconfont/tabler-icons.scss',
  225. outputStyle: 'expanded'
  226. }, function (err, result) {
  227. fs.writeFileSync('iconfont/tabler-icons.css', result.css);
  228. const cleanOutput = new cleanCSS({}).minify(result.css);
  229. fs.writeFileSync('iconfont/tabler-icons.min.css', cleanOutput.styles);
  230. cb();
  231. });
  232. });
  233. gulp.task('build-iconfont', gulp.series('iconfont-prepare', 'iconfont-svg-outline', 'iconfont', 'iconfont-css', 'iconfont-clean'));
  234. gulp.task('build-zip', function () {
  235. const version = p.version;
  236. return gulp.src('{icons/**/*,icons-png/**/*,icons-react/**/*,iconfont/**/*,tabler-sprite.svg,tabler-sprite-nostroke.svg}')
  237. .pipe(zip(`tabler-icons-${version}.zip`))
  238. .pipe(gulp.dest('packages'))
  239. });
  240. gulp.task('build-jekyll', function (cb) {
  241. cp.exec('bundle exec jekyll build', function () {
  242. cb();
  243. });
  244. });
  245. gulp.task('build-copy', function (cb) {
  246. cp.exec('mkdir -p icons/ && rm -fd ./icons/* && cp ./_site/icons/* ./icons && cp ./_site/tags.json .', function () {
  247. cb();
  248. });
  249. });
  250. gulp.task('clean-png', function (cb) {
  251. cp.exec('rm -fd ./icons-png/*', function () {
  252. cb();
  253. });
  254. });
  255. gulp.task('icons-sprite', function (cb) {
  256. glob("_site/icons/*.svg", {}, function (er, files) {
  257. let svgContent = '';
  258. files.forEach(function (file, i) {
  259. let name = path.basename(file, '.svg'),
  260. svgFile = fs.readFileSync(file),
  261. svgFileContent = svgFile.toString();
  262. svgFileContent = svgFileContent
  263. .replace(/<svg[^>]+>/g, '')
  264. .replace(/<\/svg>/g, '')
  265. .replace(/\n+/g, '')
  266. .replace(/>\s+</g, '><')
  267. .trim();
  268. svgContent += `<symbol id="tabler-${name}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${svgFileContent}</symbol>`
  269. });
  270. let svg = `<svg xmlns="http://www.w3.org/2000/svg"><defs>${svgContent}</defs></svg>`;
  271. fs.writeFileSync('tabler-sprite.svg', svg);
  272. fs.writeFileSync('tabler-sprite-nostroke.svg', svg.replace(/stroke-width="2"\s/g, ''));
  273. cb();
  274. });
  275. });
  276. gulp.task('icons-preview', function (cb) {
  277. glob("icons/*.svg", {}, function (er, files) {
  278. generateIconsPreview(files, '.github/icons.svg', cb);
  279. });
  280. });
  281. gulp.task('icons-stroke', gulp.series('build-jekyll', function (cb) {
  282. const icon = "disabled",
  283. strokes = ['.5', '1', '1.5', '2', '2.75'],
  284. svgFileContent = fs.readFileSync(`icons/${icon}.svg`).toString(),
  285. padding = 16,
  286. paddingOuter = 3,
  287. iconSize = 32,
  288. width = 914,
  289. height = iconSize + paddingOuter * 2;
  290. let svgContentSymbols = '',
  291. svgContentIcons = '',
  292. x = paddingOuter;
  293. strokes.forEach(function (stroke) {
  294. let svgFileContentStroked = svgFileContent
  295. .replace('<svg xmlns="http://www.w3.org/2000/svg"', `<symbol id="icon-${stroke}"`)
  296. .replace(' width="24" height="24"', '')
  297. .replace(' stroke-width="2"', ` stroke-width="${stroke}"`)
  298. .replace('</svg>', '</symbol>')
  299. .replace(/\n\s+/g, '');
  300. svgContentSymbols += `\t${svgFileContentStroked}\n`;
  301. svgContentIcons += `\t<use xlink:href="#icon-${stroke}" x="${x}" y="${paddingOuter}" width="${iconSize}" height="${iconSize}" />\n`;
  302. x += padding + iconSize;
  303. });
  304. 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: #354052"><rect x="0" y="0" width="${width}" height="${height}" fill="#fff"></rect>\n${svgContentSymbols}\n${svgContentIcons}\n</svg>`;
  305. fs.writeFileSync('.github/icons-stroke.svg', svgContent);
  306. createScreenshot('.github/icons-stroke.svg');
  307. cb();
  308. }));
  309. gulp.task('optimize', function (cb) {
  310. const addFloats = function (n1, n2) {
  311. return Math.round((parseFloat(n1) + parseFloat(n2)) * 1000) / 1000
  312. };
  313. glob("src/_icons/*.svg", {}, function (er, files) {
  314. files.forEach(function (file, i) {
  315. let svgFile = fs.readFileSync(file),
  316. svgFileContent = svgFile.toString();
  317. svgFileContent = svgFileContent
  318. .replace(/><\/(polyline|line|rect|circle|path)>/g, '/>')
  319. .replace(/rx="([^"]+)"\s+ry="\1"/g, 'rx="$1"')
  320. .replace(/\s?\/>/g, ' />')
  321. .replace(/\n\s*<(line|circle|path|polyline|rect)/g, "\n <$1")
  322. .replace(/polyline points="([0-9.]+)\s([0-9.]+)\s([0-9.]+)\s([0-9.]+)"/g, 'line x1="$1" y1="$2" x2="$3" y2="$4"')
  323. .replace(/d="m/g, 'd="M')
  324. .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')
  325. .replace(/\n\n+/g, "\n")
  326. .replace(/<path d="M([0-9.]*) ([0-9.]*)l\s?([-0-9.]*) ([-0-9.]*)"/g, function (f, r1, r2, r3, r4) {
  327. return `<line x1="${r1}" y1="${r2}" x2="${addFloats(r1, r3)}" y2="${addFloats(r2, r4)}"`;
  328. })
  329. .replace(/<path d="M([0-9.]*) ([0-9.]*)v\s?([-0-9.]*)"/g, function (f, r1, r2, r3) {
  330. return `<line x1="${r1}" y1="${r2}" x2="${r1}" y2="${addFloats(r2, r3)}"`;
  331. })
  332. .replace(/<path d="M([0-9.]*) ([0-9.]*)h\s?([-0-9.]*)"/g, function (f, r1, r2, r3) {
  333. return `<line x1="${r1}" y1="${r2}" x2="${addFloats(r1, r3)}" y2="${r2}"`;
  334. })
  335. .replace(/<path d="([^"]+)"/g, function (f, r1) {
  336. r1 = r1
  337. .replace(/ -0\./g, " -.")
  338. .replace(/ 0\./g, " .")
  339. .replace(/\s([a-z])/gi, "$1")
  340. .replace(/([a-z])\s/gi, "$1");
  341. return `<path d="${r1}"`;
  342. })
  343. ;
  344. //
  345. //
  346. if (svgFile.toString() !== svgFileContent) {
  347. fs.writeFileSync(file, svgFileContent);
  348. }
  349. });
  350. cb();
  351. });
  352. });
  353. gulp.task('changelog-commit', function (cb) {
  354. cp.exec('git status', function (err, ret) {
  355. let newIcons = [], modifiedIcons = [], renamedIcons = [];
  356. ret.replace(/new file:\s+src\/_icons\/([a-z0-9-]+)\.svg/g, function (m, fileName) {
  357. newIcons.push(fileName);
  358. });
  359. ret.replace(/modified:\s+src\/_icons\/([a-z0-9-]+)\.svg/g, function (m, fileName) {
  360. modifiedIcons.push(fileName);
  361. });
  362. ret.replace(/renamed:\s+src\/_icons\/([a-z0-9-]+).svg -> src\/_icons\/([a-z0-9-]+).svg/g, function (m, fileNameBefore, fileNameAfter) {
  363. renamedIcons.push([fileNameBefore, fileNameAfter]);
  364. });
  365. modifiedIcons = modifiedIcons.filter(function (el) {
  366. return newIcons.indexOf(el) < 0;
  367. });
  368. printChangelog(newIcons, modifiedIcons, renamedIcons);
  369. cb();
  370. });
  371. });
  372. gulp.task('changelog', function (cb) {
  373. const version = argv['latest-tag'] || `v${p.version}`;
  374. if (version) {
  375. cp.exec(`git diff ${version} HEAD --name-status`, function (err, ret) {
  376. let newIcons = [], modifiedIcons = [], renamedIcons = [];
  377. ret.replace(/A\s+src\/_icons\/([a-z0-9-]+)\.svg/g, function (m, fileName) {
  378. newIcons.push(fileName);
  379. });
  380. ret.replace(/M\s+src\/_icons\/([a-z0-9-]+)\.svg/g, function (m, fileName) {
  381. modifiedIcons.push(fileName);
  382. });
  383. ret.replace(/R[0-9]+\s+src\/_icons\/([a-z0-9-]+)\.svg\s+src\/_icons\/([a-z0-9-]+).svg/g, function (m, fileNameBefore, fileNameAfter) {
  384. renamedIcons.push([fileNameBefore, fileNameAfter]);
  385. });
  386. modifiedIcons = modifiedIcons.filter(function (el) {
  387. return newIcons.indexOf(el) < 0;
  388. });
  389. printChangelog(newIcons, modifiedIcons, renamedIcons, true);
  390. cb();
  391. });
  392. }
  393. });
  394. gulp.task('changelog-image', function (cb) {
  395. const version = argv['latest-version'] || `${p.version}`,
  396. newVersion = argv['new-version'] || `${p.version}`;
  397. if (version) {
  398. cp.exec(`git diff v${version} HEAD --name-status`, function (err, ret) {
  399. let newIcons = [];
  400. ret.replace(/[A]\s+src\/_icons\/([a-z0-9-]+)\.svg/g, function (m, fileName) {
  401. newIcons.push(fileName);
  402. });
  403. newIcons = newIcons.map(function (icon) {
  404. return `./icons/${icon}.svg`;
  405. });
  406. if (newIcons.length > 0) {
  407. generateIconsPreview(newIcons, `.github/tabler-icons-${newVersion}.svg`, cb, 6, 24);
  408. } else {
  409. cb();
  410. }
  411. });
  412. } else {
  413. cb();
  414. }
  415. });
  416. gulp.task('svg-to-png', gulp.series('build-jekyll', 'clean-png', async (cb) => {
  417. let files = glob.sync("./icons/*.svg");
  418. await asyncForEach(files, async function (file, i) {
  419. let name = path.basename(file, '.svg');
  420. console.log('name', name);
  421. await svgToPng(file, `icons-png/${name}.png`);
  422. });
  423. cb();
  424. }));
  425. gulp.task('clean-react', function (cb) {
  426. cp.exec('rm -fd ./icons-react/* && mkdir icons-react/icons-js', function () {
  427. cb();
  428. });
  429. });
  430. gulp.task('svg-to-react', gulp.series('clean-react', async function (cb) {
  431. let files = glob.sync("./icons/*.svg");
  432. const camelize = function (str) {
  433. str = str.replace(/-/g, ' ');
  434. return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
  435. return word.toUpperCase();
  436. }).replace(/\s+/g, '');
  437. };
  438. const componentName = function (file) {
  439. file = path.basename(file, '.svg');
  440. file = camelize(`Icon ${file}`);
  441. return file;
  442. };
  443. const optimizeSvgCode = function(svgCode) {
  444. return svgCode.replace('<path stroke="none" d="M0 0h24v24H0z"/>', '');
  445. };
  446. let indexCode = '',
  447. indexDCode = `import { FC, SVGAttributes } from 'react';\n\ninterface TablerIconProps extends SVGAttributes<SVGElement> { color?: string; size?: string | number; stroke?: string | number; }\n\ntype TablerIcon = FC<TablerIconProps>;\n\n`;
  448. await asyncForEach(files, async function (file) {
  449. const svgCode = optimizeSvgCode(fs.readFileSync(file).toString()),
  450. fileName = path.basename(file, '.svg') + '.js',
  451. iconComponentName = componentName(file);
  452. svgr(svgCode, {
  453. icon: false,
  454. svgProps: { width: '{size}', height: '{size}', strokeWidth: '{stroke}', stroke: '{color}' },
  455. template: require('./.build/svgr-template')
  456. }, { componentName: iconComponentName }).then(jsCode => {
  457. fs.writeFileSync('icons-react/icons-js/' + fileName, jsCode);
  458. indexCode += `export { default as ${iconComponentName} } from './icons-js/${fileName}';\n`;
  459. indexDCode += `export const ${iconComponentName}: TablerIcon;\n`;
  460. });
  461. fs.writeFileSync('icons-react/index.js', indexCode);
  462. fs.writeFileSync('icons-react/index.d.ts', indexDCode);
  463. });
  464. cb();
  465. }));
  466. gulp.task('build', gulp.series('optimize', 'build-jekyll', 'build-copy', 'icons-sprite', 'svg-to-react', 'icons-preview', 'svg-to-png', 'build-iconfont', 'changelog-image', 'build-zip'));