From 7df7eb9ca70fb8b97aa6268364c119731f3fcce1 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Thu, 25 Apr 2024 11:07:04 -0300 Subject: [PATCH 01/25] =?UTF-8?q?feat:=20=E2=9C=A8=20clone=20and=20recolor?= =?UTF-8?q?ize=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 150 ++++++++- package.json | 80 ++++- package.nls.es.json | 8 + package.nls.json | 8 + src/icons/generator/clones/clonesGenerator.ts | 40 +++ src/icons/generator/clones/fileClone.ts | 80 +++++ src/icons/generator/clones/folderClone.ts | 75 +++++ .../generator/clones/utils/color/colors.ts | 99 ++++++ .../clones/utils/color/material-palette.ts | 292 ++++++++++++++++++ src/icons/generator/clones/utils/paths.ts | 149 +++++++++ src/icons/generator/clones/utils/svg.ts | 93 ++++++ src/models/iconConfiguration.ts | 4 + src/models/icons/iconJsonOptions.ts | 18 ++ 13 files changed, 1090 insertions(+), 6 deletions(-) create mode 100644 src/icons/generator/clones/clonesGenerator.ts create mode 100644 src/icons/generator/clones/fileClone.ts create mode 100644 src/icons/generator/clones/folderClone.ts create mode 100644 src/icons/generator/clones/utils/color/colors.ts create mode 100644 src/icons/generator/clones/utils/color/material-palette.ts create mode 100644 src/icons/generator/clones/utils/paths.ts create mode 100644 src/icons/generator/clones/utils/svg.ts diff --git a/package-lock.json b/package-lock.json index 9367a45e93..32c7fc6663 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,12 @@ "name": "material-icon-theme", "version": "5.1.0", "dependencies": { - "lodash.merge": "4.6.2" + "chroma-js": "^2.4.2", + "lodash.merge": "4.6.2", + "svgson": "^5.3.1" }, "devDependencies": { + "@types/chroma-js": "^2.4.4", "@types/glob": "^7.2.0", "@types/lodash.merge": "^4.6.7", "@types/mocha": "^9.1.1", @@ -250,6 +253,12 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "node_modules/@types/chroma-js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.4.tgz", + "integrity": "sha512-/DTccpHTaKomqussrn+ciEvfW4k6NAHzNzs/sts1TCqg333qNxOhy8TNIoQCmbGG3Tl8KdEhkGAssb1n3mTXiQ==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.2.tgz", @@ -1177,8 +1186,7 @@ "node_modules/chroma-js": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", - "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==", - "dev": true + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" }, "node_modules/chrome-trace-event": { "version": "1.0.3", @@ -1385,6 +1393,29 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "node_modules/deep-rename-keys": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/deep-rename-keys/-/deep-rename-keys-0.2.1.tgz", + "integrity": "sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A==", + "dependencies": { + "kind-of": "^3.0.2", + "rename-keys": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-rename-keys/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1851,6 +1882,11 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -2484,6 +2520,11 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-core-module": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", @@ -3468,6 +3509,14 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/rename-keys": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rename-keys/-/rename-keys-1.2.0.tgz", + "integrity": "sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3876,6 +3925,15 @@ "node": ">= 10" } }, + "node_modules/svgson": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/svgson/-/svgson-5.3.1.tgz", + "integrity": "sha512-qdPgvUNWb40gWktBJnbJRelWcPzkLed/ShhnRsjbayXz8OtdPOzbil9jtiZdrYvSDumAz/VNQr6JaNfPx/gvPA==", + "dependencies": { + "deep-rename-keys": "^0.2.1", + "xml-reader": "2.4.3" + } + }, "node_modules/tapable": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", @@ -4456,6 +4514,23 @@ } } }, + "node_modules/xml-lexer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/xml-lexer/-/xml-lexer-0.2.2.tgz", + "integrity": "sha512-G0i98epIwiUEiKmMcavmVdhtymW+pCAohMRgybyIME9ygfVu8QheIi+YoQh3ngiThsT0SQzJT4R0sKDEv8Ou0w==", + "dependencies": { + "eventemitter3": "^2.0.0" + } + }, + "node_modules/xml-reader": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/xml-reader/-/xml-reader-2.4.3.tgz", + "integrity": "sha512-xWldrIxjeAMAu6+HSf9t50ot1uL5M+BtOidRCWHXIeewvSeIpscWCsp4Zxjk8kHHhdqFBrfK8U0EJeCcnyQ/gA==", + "dependencies": { + "eventemitter3": "^2.0.0", + "xml-lexer": "^0.2.2" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -4712,6 +4787,12 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "@types/chroma-js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.4.tgz", + "integrity": "sha512-/DTccpHTaKomqussrn+ciEvfW4k6NAHzNzs/sts1TCqg333qNxOhy8TNIoQCmbGG3Tl8KdEhkGAssb1n3mTXiQ==", + "dev": true + }, "@types/eslint": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.2.tgz", @@ -5407,8 +5488,7 @@ "chroma-js": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", - "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==", - "dev": true + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" }, "chrome-trace-event": { "version": "1.0.3", @@ -5571,6 +5651,25 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "deep-rename-keys": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/deep-rename-keys/-/deep-rename-keys-0.2.1.tgz", + "integrity": "sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A==", + "requires": { + "kind-of": "^3.0.2", + "rename-keys": "^1.1.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5922,6 +6021,11 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==" + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -6379,6 +6483,11 @@ "binary-extensions": "^2.0.0" } }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "is-core-module": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", @@ -7105,6 +7214,11 @@ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, + "rename-keys": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rename-keys/-/rename-keys-1.2.0.tgz", + "integrity": "sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==" + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -7392,6 +7506,15 @@ } } }, + "svgson": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/svgson/-/svgson-5.3.1.tgz", + "integrity": "sha512-qdPgvUNWb40gWktBJnbJRelWcPzkLed/ShhnRsjbayXz8OtdPOzbil9jtiZdrYvSDumAz/VNQr6JaNfPx/gvPA==", + "requires": { + "deep-rename-keys": "^0.2.1", + "xml-reader": "2.4.3" + } + }, "tapable": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", @@ -7820,6 +7943,23 @@ "dev": true, "requires": {} }, + "xml-lexer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/xml-lexer/-/xml-lexer-0.2.2.tgz", + "integrity": "sha512-G0i98epIwiUEiKmMcavmVdhtymW+pCAohMRgybyIME9ygfVu8QheIi+YoQh3ngiThsT0SQzJT4R0sKDEv8Ou0w==", + "requires": { + "eventemitter3": "^2.0.0" + } + }, + "xml-reader": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/xml-reader/-/xml-reader-2.4.3.tgz", + "integrity": "sha512-xWldrIxjeAMAu6+HSf9t50ot1uL5M+BtOidRCWHXIeewvSeIpscWCsp4Zxjk8kHHhdqFBrfK8U0EJeCcnyQ/gA==", + "requires": { + "eventemitter3": "^2.0.0", + "xml-lexer": "^0.2.2" + } + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index ff6d590b47..2c7396a8ee 100644 --- a/package.json +++ b/package.json @@ -189,6 +189,81 @@ "default": {}, "description": "%configuration.languages.associations%" }, + "material-icon-theme.files.customClones": { + "type": "array", + "default": [], + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "%configuration.customClones.name%" + }, + "base": { + "type": "string", + "description": "%configuration.customClones.base%" + }, + "color": { + "type": "string", + "description": "%configuration.customClones.color%" + }, + "lightColor": { + "type": "string", + "description": "%configuration.customClones.lightColor%" + }, + "fileNames": { + "type": "array", + "default": [], + "description": "%configuration.customClones.fileNames%", + "items": { + "type": "string" + } + }, + "fileExtensions": { + "type": "array", + "default": [], + "description": "%configuration.customClones.fileExtensions%", + "items": { + "type": "string" + } + } + } + }, + "description": "%configuration.customClones%" + }, + "material-icon-theme.folders.customClones": { + "type": "array", + "default": [], + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "%configuration.customClones.name%" + }, + "base": { + "type": "string", + "description": "%configuration.customClones.base%" + }, + "color": { + "type": "string", + "description": "%configuration.customClones.color%" + }, + "lightColor": { + "type": "string", + "description": "%configuration.customClones.lightColor%" + }, + "folderNames": { + "type": "array", + "description": "%configuration.customClones.folderNames%", + "items": { + "type": "string" + } + } + } + }, + "description": "%configuration.customClones%" + }, "material-icon-theme.folders.theme": { "type": "string", "default": "specific", @@ -239,9 +314,12 @@ } }, "dependencies": { - "lodash.merge": "4.6.2" + "chroma-js": "^2.4.2", + "lodash.merge": "4.6.2", + "svgson": "^5.3.1" }, "devDependencies": { + "@types/chroma-js": "^2.4.4", "@types/glob": "^7.2.0", "@types/lodash.merge": "^4.6.7", "@types/mocha": "^9.1.1", diff --git a/package.nls.es.json b/package.nls.es.json index ccd16dee33..b80f4790c6 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -13,6 +13,14 @@ "configuration.files.associations": "Configurar asociaciones personalizadas de iconos de archivos.", "configuration.folders.associations": "Configurar asociaciones personalizadas de iconos de carpetas.", "configuration.languages.associations": "Configurar asociaciones personalizadas de iconos de idioma.", + "configuration.customClones": "Clonar cualquier icono existente y crear uno nuevo con colores y asociaciones personalizadas", + "configuration.customClones.base": "Icono usado como base para crear el icono clonado personalizado", + "configuration.customClones.name": "Nombre del icono personalizado", + "configuration.customClones.color": "Color usado como base para recolorear el icono", + "configuration.customClones.lightColor": "Color usado como base para recolorear el icono cuando el tema es claro", + "configuration.customClones.fileNames": "Nombres de archivo para asociar con el icono personalizado", + "configuration.customClones.fileExtensions": "Extensiones de archivo para asociar con el icono personalizado", + "configuration.customClones.folderNames": "Nombres de carpeta para asociar con el icono personalizado", "configuration.activeIconPack": "Seleccionar un paquete de iconos que permita iconos específicos.", "configuration.activeIconPack.angular": "Iconos de Angular.", "configuration.activeIconPack.angular_ngrx": "Iconos de Angular y ngrx.", diff --git a/package.nls.json b/package.nls.json index 389bbba92c..f5d10776ee 100644 --- a/package.nls.json +++ b/package.nls.json @@ -13,6 +13,14 @@ "configuration.files.associations": "Set custom file icon associations.", "configuration.folders.associations": "Set custom folder icon associations.", "configuration.languages.associations": "Set custom language icon associations.", + "configuration.customClones": "Clone any existing icon and create a new one with custom colors and associations", + "configuration.customClones.base": "Icon used as a base to create the custom cloned icon", + "configuration.customClones.name": "Name of the custom icon", + "configuration.customClones.color": "Color used as a base to recolor the icon", + "configuration.customClones.lightColor": "Color used as a base to recolor the icon when the theme is light", + "configuration.customClones.fileNames": "File names to associate with the custom icon", + "configuration.customClones.fileExtensions": "File extensions to associate with the custom icon", + "configuration.customClones.folderNames": "Folder names to associate with the custom icon", "configuration.activeIconPack": "Select an icon pack that enables specific icons.", "configuration.activeIconPack.angular": "Icons for Angular.", "configuration.activeIconPack.angular_ngrx": "Icons for Angular and ngrx.", diff --git a/src/icons/generator/clones/clonesGenerator.ts b/src/icons/generator/clones/clonesGenerator.ts new file mode 100644 index 0000000000..95527c10df --- /dev/null +++ b/src/icons/generator/clones/clonesGenerator.ts @@ -0,0 +1,40 @@ +import { IconConfiguration, IconJsonOptions } from '../../../models'; +import { clearCloneFolder } from './utils/paths'; +import merge from 'lodash.merge'; +import { getFileConfigHash } from '../../../helpers/fileConfig'; +import { cloneFolderIcon } from './folderClone'; +import { cloneFileIcon } from './fileClone'; + +/** + * This function applies custom colors to the svg icon, respecting + * the different tones of the original icon based on a provided + * "base" color. + */ +export function customClonesIcons( + config: IconConfiguration, + options: IconJsonOptions +): IconConfiguration { + clearCloneFolder(hasCustomClones(options)); + + let clonedIconsConfig: IconConfiguration = new IconConfiguration(); + const hash = getFileConfigHash(options); + + options.folders?.customClones?.forEach((clone) => { + const cloneIcon = cloneFolderIcon(clone, config, hash); + clonedIconsConfig = merge(clonedIconsConfig, cloneIcon); + }); + + options.files?.customClones?.forEach((clone) => { + const cloneIcon = cloneFileIcon(clone, config, hash); + clonedIconsConfig = merge(clonedIconsConfig, cloneIcon); + }); + + return clonedIconsConfig; +} + +export function hasCustomClones(options: IconJsonOptions): boolean { + return ( + (options.folders?.customClones?.length ?? 0) > 0 || + (options.files?.customClones?.length ?? 0) > 0 + ); +} diff --git a/src/icons/generator/clones/fileClone.ts b/src/icons/generator/clones/fileClone.ts new file mode 100644 index 0000000000..89ba455646 --- /dev/null +++ b/src/icons/generator/clones/fileClone.ts @@ -0,0 +1,80 @@ +import { basename } from 'path'; +import { FileIconClone, IconConfiguration } from '../../../models'; +import { + IconPath, + getFileIconBasePaths, + getFileIconClonePath, + FileIconType, +} from './utils/paths'; +import { iconFolderPath } from '../constants'; +import { cloneIcon } from './utils/svg'; +import { writeFileSync } from 'fs'; + +export function cloneFileIcon( + cloneOpts: FileIconClone, + config: IconConfiguration, + hash: string +): IconConfiguration { + const basePaths = getFileIconBasePaths(cloneOpts, config); + if (!basePaths) { + return {}; + } + + return createFileIconClones(cloneOpts, basePaths, hash); +} + +function createFileIconClones( + cloneOpts: FileIconClone, + basePaths: IconPath[], + hash: string +): IconConfiguration { + const config: IconConfiguration = new IconConfiguration(); + + basePaths.forEach((base) => { + try { + const filePath = getFileIconClonePath(base, cloneOpts, hash); + const iconPathConfig = `${iconFolderPath}clones/${basename( + filePath.path + )}`; + const content = cloneIcon(base.path, hash, cloneOpts); + const iconName = basename(filePath.path, '.svg'); + + try { + writeFileSync(filePath.path, content); + } catch (error) { + console.error(error); + return; + } + + config.iconDefinitions![iconName] = { + iconPath: iconPathConfig, + }; + + cloneOpts.fileNames?.forEach((fileName) => { + switch (filePath.type) { + case FileIconType.Base: + config.fileNames![fileName] = iconName; + break; + case FileIconType.Light: + config.light!.fileNames![fileName] = iconName; + break; + } + }); + + cloneOpts.fileExtensions?.forEach((fileExtension) => { + switch (filePath.type) { + case FileIconType.Base: + config.fileExtensions![fileExtension] = iconName; + break; + case FileIconType.Light: + config.light!.fileExtensions![fileExtension] = iconName; + break; + } + }); + } catch (error) { + console.error(error); + } + }); + + return config; +} diff --git a/src/icons/generator/clones/folderClone.ts b/src/icons/generator/clones/folderClone.ts new file mode 100644 index 0000000000..017f6ea2aa --- /dev/null +++ b/src/icons/generator/clones/folderClone.ts @@ -0,0 +1,75 @@ +import { basename } from 'path'; +import { FolderIconClone, IconConfiguration } from '../../../models'; +import { + IconPath, + FolderIconType, + getFolderIconBasePaths, + getFolderIconClonePath, +} from './utils/paths'; +import { iconFolderPath } from '../constants'; +import { cloneIcon } from './utils/svg'; +import { writeFileSync } from 'fs'; + +export function cloneFolderIcon( + cloneOpts: FolderIconClone, + config: IconConfiguration, + hash: string +): IconConfiguration { + const basePaths = getFolderIconBasePaths(cloneOpts, config); + if (!basePaths) { + return {}; + } + + return createFolderClones(cloneOpts, basePaths, hash); +} + +function createFolderClones( + cloneOpts: FolderIconClone, + basePaths: IconPath[], + hash: string +): IconConfiguration { + const config: IconConfiguration = new IconConfiguration(); + + basePaths.forEach((base) => { + try { + const filePath = getFolderIconClonePath(base, cloneOpts, hash); + const iconPathConfig = `${iconFolderPath}clones/${basename( + filePath.path + )}`; + const content = cloneIcon(base.path, hash, cloneOpts); + const iconName = basename(filePath.path, '.svg'); + + try { + writeFileSync(filePath.path, content); + } catch (error) { + console.error(error); + return; + } + + config.iconDefinitions![iconName] = { + iconPath: iconPathConfig, + }; + + cloneOpts.folderNames?.forEach((folderName) => { + switch (filePath.type) { + case FolderIconType.Base: + config.folderNames![folderName] = iconName; + break; + case FolderIconType.Open: + config.folderNamesExpanded![folderName] = iconName; + break; + case FolderIconType.Light: + config.light!.folderNames![folderName] = iconName; + break; + case FolderIconType.LightOpen: + config.light!.folderNamesExpanded![folderName] = iconName; + break; + } + }); + } catch (error) { + console.error(error); + } + }); + + return config; +} diff --git a/src/icons/generator/clones/utils/color/colors.ts b/src/icons/generator/clones/utils/color/colors.ts new file mode 100644 index 0000000000..3e89263be8 --- /dev/null +++ b/src/icons/generator/clones/utils/color/colors.ts @@ -0,0 +1,99 @@ +import { INode } from 'svgson'; +import { getStyle, traverse } from '../svg'; +import chroma, { valid } from 'chroma-js'; +import { + closerMaterialColorTo, + getMaterialColorByKey, +} from './material-palette'; + +export function getColorList(node: INode) { + const colors = new Set(); + + traverse(node, (node) => { + // check colors in style attribute + const style = getStyle(node); + if (style) { + if (style.fill && isValidColor(style.fill)) { + colors.add(style.fill); + } + + if (style.stroke && isValidColor(style.stroke)) { + colors.add(style.stroke); + } + } + + // check colors in attributes + if (node.attributes) { + if (node.attributes.fill && isValidColor(node.attributes.fill)) { + colors.add(node.attributes.fill); + } + + if (node.attributes.stroke && isValidColor(node.attributes.stroke)) { + colors.add(node.attributes.stroke); + } + + if ( + node.attributes['stop-color'] && + isValidColor(node.attributes['stop-color']) + ) { + colors.add(node.attributes['stop-color']); + } + } + }); + + return colors; +} + +function orderDarkToLight(colors: Set) { + const colorArray = Array.from(colors); + return colorArray.sort((a, b) => { + const colorA = chroma(a); + const colorB = chroma(b); + + // determine which one is darker based on saturation and lightness + const aDarkness = colorA.get('hsl.l') * colorA.get('hsl.s'); + const bDarkness = colorB.get('hsl.l') * colorB.get('hsl.s'); + + return aDarkness - bDarkness; + }); +} + +export function isValidColor(hexColor: string | undefined): boolean { + if (hexColor === undefined) { + return false; + } + return valid(hexColor); +} + +/** + * receives a base color and a list of colors, orders the list of colors from dark to light + * and replaces the darkest color (first in the list) with the base color, then grabs the hue + * of the base color and applies it to the rest of the colors in the list. + */ +export function replacementMap(baseColor: string, colors: Set) { + if (!isValidColor(baseColor)) { + // try to get it from the material palette by key + const matCol = getMaterialColorByKey(baseColor); + if (matCol === undefined) { + throw new Error(`Invalid color: ${baseColor}`); + } + + baseColor = matCol; + } + + const orderedColors = orderDarkToLight(colors); + const baseColorChroma = chroma(baseColor); + const baseHue = baseColorChroma.get('hsl.h'); + + const replacement = new Map(); + replacement.set(orderedColors[0], baseColor); + + for (let i = 1; i < orderedColors.length; i++) { + const color = chroma(orderedColors[i]); + const newColor = color.set('hsl.h', baseHue).hex(); + const matCol = closerMaterialColorTo(newColor); + replacement.set(orderedColors[i], matCol); + } + + return replacement; +} diff --git a/src/icons/generator/clones/utils/color/material-palette.ts b/src/icons/generator/clones/utils/color/material-palette.ts new file mode 100644 index 0000000000..1398a8ec33 --- /dev/null +++ b/src/icons/generator/clones/utils/color/material-palette.ts @@ -0,0 +1,292 @@ +import chroma, { deltaE } from 'chroma-js'; +import { isValidColor } from './colors'; + +export const materialPalette = { + white: '#FFFFFF', + black: '#000000', + 'red-50': '#FFEBEE', + 'red-100': '#FFCDD2', + 'red-200': '#EF9A9A', + 'red-300': '#E57373', + 'red-400': '#EF5350', + 'red-500': '#F44336', + 'red-600': '#E53935', + 'red-700': '#D32F2F', + 'red-800': '#C62828', + 'red-900': '#B71C1C', + 'red-A100': '#FF8A80', + 'red-A200': '#FF5252', + 'red-A400': '#FF1744', + 'red-A700': '#D50000', + 'pink-50': '#FCE4EC', + 'pink-100': '#F8BBD0', + 'pink-200': '#F48FB1', + 'pink-300': '#F06292', + 'pink-400': '#EC407A', + 'pink-500': '#E91E63', + 'pink-600': '#D81B60', + 'pink-700': '#C2185B', + 'pink-800': '#AD1457', + 'pink-900': '#880E4F', + 'pink-A100': '#FF80AB', + 'pink-A200': '#FF4081', + 'pink-A400': '#F50057', + 'pink-A700': '#C51162', + 'purple-50': '#F3E5F5', + 'purple-100': '#E1BEE7', + 'purple-200': '#CE93D8', + 'purple-300': '#BA68C8', + 'purple-400': '#AB47BC', + 'purple-500': '#9C27B0', + 'purple-600': '#8E24AA', + 'purple-700': '#7B1FA2', + 'purple-800': '#6A1B9A', + 'purple-900': '#4A148C', + 'purple-A100': '#EA80FC', + 'purple-A200': '#E040FB', + 'purple-A400': '#D500F9', + 'purple-A700': '#AA00FF', + 'deep-purple-50': '#EDE7F6', + 'deep-purple-100': '#D1C4E9', + 'deep-purple-200': '#B39DDB', + 'deep-purple-300': '#9575CD', + 'deep-purple-400': '#7E57C2', + 'deep-purple-500': '#673AB7', + 'deep-purple-600': '#5E35B1', + 'deep-purple-700': '#512DA8', + 'deep-purple-800': '#4527A0', + 'deep-purple-900': '#311B92', + 'deep-purple-A100': '#B388FF', + 'deep-purple-A200': '#7C4DFF', + 'deep-purple-A400': '#651FFF', + 'deep-purple-A700': '#6200EA', + 'indigo-50': '#E8EAF6', + 'indigo-100': '#C5CAE9', + 'indigo-200': '#9FA8DA', + 'indigo-300': '#7986CB', + 'indigo-400': '#5C6BC0', + 'indigo-500': '#3F51B5', + 'indigo-600': '#3949AB', + 'indigo-700': '#303F9F', + 'indigo-800': '#283593', + 'indigo-900': '#1A237E', + 'indigo-A100': '#8C9EFF', + 'indigo-A200': '#536DFE', + 'indigo-A400': '#3D5AFE', + 'indigo-A700': '#304FFE', + 'blue-50': '#E3F2FD', + 'blue-100': '#BBDEFB', + 'blue-200': '#90CAF9', + 'blue-300': '#64B5F6', + 'blue-400': '#42A5F5', + 'blue-500': '#2196F3', + 'blue-600': '#1E88E5', + 'blue-700': '#1976D2', + 'blue-800': '#1565C0', + 'blue-900': '#0D47A1', + 'blue-A100': '#82B1FF', + 'blue-A200': '#448AFF', + 'blue-A400': '#2979FF', + 'blue-A700': '#2962FF', + 'light-blue-50': '#E1F5FE', + 'light-blue-100': '#B3E5FC', + 'light-blue-200': '#81D4FA', + 'light-blue-300': '#4FC3F7', + 'light-blue-400': '#29B6F6', + 'light-blue-500': '#03A9F4', + 'light-blue-600': '#039BE5', + 'light-blue-700': '#0288D1', + 'light-blue-800': '#0277BD', + 'light-blue-900': '#01579B', + 'light-blue-A100': '#80D8FF', + 'light-blue-A200': '#40C4FF', + 'light-blue-A400': '#00B0FF', + 'light-blue-A700': '#0091EA', + 'cyan-50': '#E0F7FA', + 'cyan-100': '#B2EBF2', + 'cyan-200': '#80DEEA', + 'cyan-300': '#4DD0E1', + 'cyan-400': '#26C6DA', + 'cyan-500': '#00BCD4', + 'cyan-600': '#00ACC1', + 'cyan-700': '#0097A7', + 'cyan-800': '#00838F', + 'cyan-900': '#006064', + 'cyan-A100': '#84FFFF', + 'cyan-A200': '#18FFFF', + 'cyan-A400': '#00E5FF', + 'cyan-A700': '#00B8D4', + 'teal-50': '#E0F2F1', + 'teal-100': '#B2DFDB', + 'teal-200': '#80CBC4', + 'teal-300': '#4DB6AC', + 'teal-400': '#26A69A', + 'teal-500': '#009688', + 'teal-600': '#00897B', + 'teal-700': '#00796B', + 'teal-800': '#00695C', + 'teal-900': '#004D40', + 'teal-A100': '#A7FFEB', + 'teal-A200': '#64FFDA', + 'teal-A400': '#1DE9B6', + 'teal-A700': '#00BFA5', + 'green-50': '#E8F5E9', + 'green-100': '#C8E6C9', + 'green-200': '#A5D6A7', + 'green-300': '#81C784', + 'green-400': '#66BB6A', + 'green-500': '#4CAF50', + 'green-600': '#43A047', + 'green-700': '#388E3C', + 'green-800': '#2E7D32', + 'green-900': '#1B5E20', + 'green-A100': '#B9F6CA', + 'green-A200': '#69F0AE', + 'green-A400': '#00E676', + 'green-A700': '#00C853', + 'light-green-50': '#F1F8E9', + 'light-green-100': '#DCEDC8', + 'light-green-200': '#C5E1A5', + 'light-green-300': '#AED581', + 'light-green-400': '#9CCC65', + 'light-green-500': '#8BC34A', + 'light-green-600': '#7CB342', + 'light-green-700': '#689F38', + 'light-green-800': '#558B2F', + 'light-green-900': '#33691E', + 'light-green-A100': '#CCFF90', + 'light-green-A200': '#B2FF59', + 'light-green-A400': '#76FF03', + 'light-green-A700': '#64DD17', + 'lime-50': '#F9FBE7', + 'lime-100': '#F0F4C3', + 'lime-200': '#E6EE9C', + 'lime-300': '#DCE775', + 'lime-400': '#D4E157', + 'lime-500': '#CDDC39', + 'lime-600': '#C0CA33', + 'lime-700': '#AFB42B', + 'lime-800': '#9E9D24', + 'lime-900': '#827717', + 'lime-A100': '#F4FF81', + 'lime-A200': '#EEFF41', + 'lime-A400': '#C6FF00', + 'lime-A700': '#AEEA00', + 'yellow-50': '#FFFDE7', + 'yellow-100': '#FFF9C4', + 'yellow-200': '#FFF59D', + 'yellow-300': '#FFF176', + 'yellow-400': '#FFEE58', + 'yellow-500': '#FFEB3B', + 'yellow-600': '#FDD835', + 'yellow-700': '#FBC02D', + 'yellow-800': '#F9A825', + 'yellow-900': '#F57F17', + 'yellow-A100': '#FFFF8D', + 'yellow-A200': '#FFFF00', + 'yellow-A400': '#FFEA00', + 'yellow-A700': '#FFD600', + 'amber-50': '#FFF8E1', + 'amber-100': '#FFECB3', + 'amber-200': '#FFE082', + 'amber-300': '#FFD54F', + 'amber-400': '#FFCA28', + 'amber-500': '#FFC107', + 'amber-600': '#FFB300', + 'amber-700': '#FFA000', + 'amber-800': '#FF8F00', + 'amber-900': '#FF6F00', + 'amber-A100': '#FFE57F', + 'amber-A200': '#FFD740', + 'amber-A400': '#FFC400', + 'amber-A700': '#FFAB00', + 'orange-50': '#FFF3E0', + 'orange-100': '#FFE0B2', + 'orange-200': '#FFCC80', + 'orange-300': '#FFB74D', + 'orange-400': '#FFA726', + 'orange-500': '#FF9800', + 'orange-600': '#FB8C00', + 'orange-700': '#F57C00', + 'orange-800': '#EF6C00', + 'orange-900': '#E65100', + 'orange-A100': '#FFD180', + 'orange-A200': '#FFAB40', + 'orange-A400': '#FF9100', + 'orange-A700': '#FF6D00', + 'deep-orange-50': '#FBE9E7', + 'deep-orange-100': '#FFCCBC', + 'deep-orange-200': '#FFAB91', + 'deep-orange-300': '#FF8A65', + 'deep-orange-400': '#FF7043', + 'deep-orange-500': '#FF5722', + 'deep-orange-600': '#F4511E', + 'deep-orange-700': '#E64A19', + 'deep-orange-800': '#D84315', + 'deep-orange-900': '#BF360C', + 'deep-orange-A100': '#FF9E80', + 'deep-orange-A200': '#FF6E40', + 'deep-orange-A400': '#FF3D00', + 'deep-orange-A700': '#DD2C00', + 'brown-50': '#EFEBE9', + 'brown-100': '#D7CCC8', + 'brown-200': '#BCAAA4', + 'brown-300': '#A1887F', + 'brown-400': '#8D6E63', + 'brown-500': '#795548', + 'brown-600': '#6D4C41', + 'brown-700': '#5D4037', + 'brown-800': '#4E342E', + 'brown-900': '#3E2723', + 'gray-50': '#FAFAFA', + 'gray-100': '#F5F5F5', + 'gray-200': '#EEEEEE', + 'gray-300': '#E0E0E0', + 'gray-400': '#BDBDBD', + 'gray-500': '#9E9E9E', + 'gray-600': '#757575', + 'gray-700': '#616161', + 'gray-800': '#424242', + 'gray-900': '#212121', + 'blue-gray-50': '#ECEFF1', + 'blue-gray-100': '#CFD8DC', + 'blue-gray-200': '#B0BEC5', + 'blue-gray-300': '#90A4AE', + 'blue-gray-400': '#78909C', + 'blue-gray-500': '#607D8B', + 'blue-gray-600': '#546E7A', + 'blue-gray-700': '#455A64', + 'blue-gray-800': '#37474F', + 'blue-gray-900': '#263238', +}; + +/** + * Gets the material color from the material palette + * @param key the key of the material color e.g. 'blue-grey-500' + */ +export function getMaterialColorByKey(key: string): string | undefined { + if (key in materialPalette) { + return materialPalette[key as keyof typeof materialPalette]; + } + + return undefined; +} + +export function closerMaterialColorTo(color: string): string { + const palette = Object.values(materialPalette); + + if (!isValidColor(color)) { + throw new Error(`The given color "${color}" is not valid!`); + } + + color = chroma(color).hex(); + + const distances = palette + .map((paletteColor) => ({ + distance: deltaE(paletteColor, color), + color: paletteColor, + })) + .sort((a, b) => a.distance - b.distance); + + return distances[0].color; +} diff --git a/src/icons/generator/clones/utils/paths.ts b/src/icons/generator/clones/utils/paths.ts new file mode 100644 index 0000000000..e5b169d9bb --- /dev/null +++ b/src/icons/generator/clones/utils/paths.ts @@ -0,0 +1,149 @@ +import { basename, dirname, join } from 'path'; +import { + FileIconClone, + FolderIconClone, + IconConfiguration, +} from '../../../../models'; +import { existsSync, mkdirSync, rmSync } from 'fs'; + +export enum FolderIconType { + Base, + Open, + Light, + LightOpen, +} + +export enum FileIconType { + Base, + Light, +} + +export interface IconPath { + path: string; + type: T; +} + +function resolvePath(path: string): string { + if (basename(__dirname) === 'dist') { + return join(__dirname, String(path)); + } else { + // executed via script + return join(__dirname, '..', '..', '..', String(path)); + } +} + +export function getFileIconBasePaths( + cloneOpts: FileIconClone, + config: IconConfiguration +): IconPath[] | undefined { + const paths = []; + const base = config.iconDefinitions?.[`${cloneOpts.base}`]?.iconPath; + const light = config.iconDefinitions?.[`${cloneOpts.base}_light`]?.iconPath; + + if (base) { + base && paths.push({ type: FileIconType.Base, path: resolvePath(base) }); + light && paths.push({ type: FileIconType.Light, path: resolvePath(light) }); + return paths; + } +} + +export function getFileIconClonePath( + base: IconPath, + cloneOpts: FileIconClone, + hash: string +): IconPath { + const sufix = base.type === FileIconType.Light ? '_light' : ''; + + const clonePath = join( + dirname(base.path), + 'clones', + `${cloneOpts.name}${sufix}${hash}.svg` + ); + + return { + type: base.type, + path: clonePath, + }; +} + +export function getFolderIconBasePaths( + cloneOpts: FolderIconClone, + config: IconConfiguration +): IconPath[] | undefined { + const paths = []; + const folderBase = + cloneOpts.base === 'folder' ? 'folder' : `folder-${cloneOpts.base}`; + + const base = config.iconDefinitions?.[`${folderBase}`]?.iconPath; + const open = config.iconDefinitions?.[`${folderBase}-open`]?.iconPath; + const light = config.iconDefinitions?.[`${folderBase}_light`]?.iconPath; + const lightOpen = + config.iconDefinitions?.[`${folderBase}-open_light`]?.iconPath; + + if (base && open) { + base && paths.push({ type: FolderIconType.Base, path: resolvePath(base) }); + open && paths.push({ type: FolderIconType.Open, path: resolvePath(open) }); + + if (light) { + paths.push({ type: FolderIconType.Light, path: resolvePath(light) }); + } + + if (lightOpen) { + paths.push({ + type: FolderIconType.LightOpen, + path: resolvePath(lightOpen), + }); + } + + return paths; + } +} + +export function getFolderIconClonePath( + base: IconPath, + cloneOpts: FolderIconClone, + hash: string +): IconPath { + let sufix = ''; + + switch (base.type) { + case FolderIconType.Base: + break; + case FolderIconType.Open: + sufix = '-open'; + break; + case FolderIconType.Light: + sufix = '_light'; + break; + case FolderIconType.LightOpen: + sufix = '-open_light'; + break; + } + + const clonePath = join( + dirname(base.path), + 'clones', + `folder-${cloneOpts.name}${sufix}${hash}.svg` + ); + + return { + type: base.type, + path: clonePath, + }; +} + +/** + * removes the clones folder if it exists + * and creates a new one + */ +export function clearCloneFolder(keep: boolean = true): void { + const clonesFolderPath = resolvePath('./../icons/clones'); + + if (existsSync(clonesFolderPath)) { + rmSync(clonesFolderPath, { recursive: true }); + } + + if (keep) { + mkdirSync(clonesFolderPath); + } +} diff --git a/src/icons/generator/clones/utils/svg.ts b/src/icons/generator/clones/utils/svg.ts new file mode 100644 index 0000000000..a7f2c28386 --- /dev/null +++ b/src/icons/generator/clones/utils/svg.ts @@ -0,0 +1,93 @@ +import { readFileSync } from 'fs'; +import { INode, parseSync, stringify } from 'svgson'; +import { CustomClone } from '../../../../models'; +import { getColorList, replacementMap } from './color/colors'; + +export function traverse(node: INode, callback: (node: INode) => void) { + callback(node); + if (node.children) { + node.children.forEach((child) => traverse(child, callback)); + } +} + +export function readIcon(path: string, hash: string): string { + try { + return readFileSync(path, 'utf8'); + } catch (error) { + const unhashedPath = path.replace(hash, ''); + return readFileSync(unhashedPath, 'utf8'); + } +} + +export function cloneIcon( + path: string, + hash: string, + cloneOpts: CustomClone +): string { + const baseContent = readIcon(path, hash); + const svg = parseSync(baseContent); + const replacements = replacementMap(cloneOpts.color, getColorList(svg)); + replaceColors(svg, replacements); + return stringify(svg); +} + +export function getStyle(node: INode) { + if (node && node.attributes && node.attributes.style) { + return parseStyle(node.attributes.style); + } + return {}; +} + +function parseStyle(css: string) { + const rules = css.split(';'); + const result: Record = {}; + rules.forEach((rule) => { + const [key, value] = rule.split(':'); + result[key.trim()] = value.trim(); + }); + return result; +} + +export function stringifyStyle(css: Record) { + return Object.entries(css) + .map(([key, value]) => `${key}:${value}`) + .join(';'); +} + +export function replaceColors(node: INode, replacements: Map) { + traverse(node, (node) => { + // replace colors in style attribute + const style = getStyle(node); + if (style) { + if (style.fill && replacements.has(style.fill)) { + style.fill = replacements.get(style.fill)!; + node.attributes.style = stringifyStyle(style); + } + + if (style.stroke && replacements.has(style.stroke)) { + style.stroke = replacements.get(style.stroke)!; + node.attributes.style = stringifyStyle(style); + } + } + + // replace colors in attributes + if (node.attributes) { + if (node.attributes.fill && replacements.has(node.attributes.fill)) { + node.attributes.fill = replacements.get(node.attributes.fill)!; + } + + if (node.attributes.stroke && replacements.has(node.attributes.stroke)) { + node.attributes.stroke = replacements.get(node.attributes.stroke)!; + } + + if ( + node.attributes['stop-color'] && + replacements.has(node.attributes['stop-color']) + ) { + node.attributes['stop-color'] = replacements.get( + node.attributes['stop-color'] + )!; + } + } + }); +} diff --git a/src/models/iconConfiguration.ts b/src/models/iconConfiguration.ts index 00ea4bab9a..c50de1b79e 100644 --- a/src/models/iconConfiguration.ts +++ b/src/models/iconConfiguration.ts @@ -27,10 +27,14 @@ export class IconConfiguration { this.light = { fileExtensions: {}, fileNames: {}, + folderNames: {}, + folderNamesExpanded: {}, }; this.highContrast = { fileExtensions: {}, fileNames: {}, + folderNames: {}, + folderNamesExpanded: {}, }; this.options = {}; } diff --git a/src/models/icons/iconJsonOptions.ts b/src/models/icons/iconJsonOptions.ts index 5acef6e340..b14505565a 100644 --- a/src/models/icons/iconJsonOptions.ts +++ b/src/models/icons/iconJsonOptions.ts @@ -7,10 +7,12 @@ export interface IconJsonOptions { theme?: string; color?: string; associations?: IconAssociations; + customClones?: FolderIconClone[]; }; files?: { color?: string; associations?: IconAssociations; + customClones?: FileIconClone[]; }; languages?: { associations?: IconAssociations; @@ -20,3 +22,19 @@ export interface IconJsonOptions { export interface IconAssociations { [pattern: string]: string; } + +export interface CustomClone { + name: string; + base: string; + color: string; + lightColor?: string; +} + +export interface FileIconClone extends CustomClone { + fileExtensions?: string[]; + fileNames?: string[]; +} + +export interface FolderIconClone extends CustomClone { + folderNames: string[]; +} From bc6abd79ae77c200994c2713ae6f4cb17accca89 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Thu, 25 Apr 2024 11:25:04 -0300 Subject: [PATCH 02/25] =?UTF-8?q?feat:=20=E2=9C=A8=20integrate=20icon=20cl?= =?UTF-8?q?oning=20with=20the=20the=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src/helpers/fileConfig.ts | 4 +++- src/icons/generator/iconOpacity.ts | 5 ++++- src/icons/generator/iconSaturation.ts | 5 ++++- src/icons/generator/jsonGenerator.ts | 7 ++++++- 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2f4f1fe2c0..aac90f073c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ icons/folder.svg icons/folder-open.svg icons/folder-root.svg icons/folder-root-open.svg +icons/clones src/scripts/preview/*.html src/scripts/contributors/*.html diff --git a/src/helpers/fileConfig.ts b/src/helpers/fileConfig.ts index 9d77f0a37a..6f53ed5dff 100644 --- a/src/helpers/fileConfig.ts +++ b/src/helpers/fileConfig.ts @@ -13,7 +13,9 @@ export const getFileConfigHash = (options: IconJsonOptions): string => { options.saturation !== defaults.saturation || options.opacity !== defaults.opacity || options.folders?.color !== defaults.folders.color || - options.files?.color !== defaults.files.color + options.files?.color !== defaults.files.color || + (options.files?.customClones?.length ?? 0) > 0 || + (options.folders?.customClones?.length ?? 0) > 0 ) { fileConfigString += `~${getHash(JSON.stringify(options))}`; } diff --git a/src/icons/generator/iconOpacity.ts b/src/icons/generator/iconOpacity.ts index 3fbadcffe8..1615bbef51 100644 --- a/src/icons/generator/iconOpacity.ts +++ b/src/icons/generator/iconOpacity.ts @@ -1,4 +1,4 @@ -import { readdirSync, readFileSync, writeFileSync } from 'fs'; +import { lstatSync, readdirSync, readFileSync, writeFileSync } from 'fs'; import { basename, join } from 'path'; import { getCustomIconPaths } from '../../helpers/customIcons'; import { IconJsonOptions } from '../../models'; @@ -89,6 +89,9 @@ const adjustOpacity = ( ): ((value: string, index: number, array: string[]) => void) => { return (iconFileName) => { const svgFilePath = join(iconPath, iconFileName); + if (!lstatSync(svgFilePath).isFile()) { + return; + } // Read SVG file const svg = readFileSync(svgFilePath, 'utf-8'); diff --git a/src/icons/generator/iconSaturation.ts b/src/icons/generator/iconSaturation.ts index a8ab6255c2..35199c62f7 100644 --- a/src/icons/generator/iconSaturation.ts +++ b/src/icons/generator/iconSaturation.ts @@ -1,4 +1,4 @@ -import { readdirSync, readFileSync, writeFileSync } from 'fs'; +import { lstatSync, readdirSync, readFileSync, writeFileSync } from 'fs'; import { basename, join } from 'path'; import { getCustomIconPaths } from '../../helpers/customIcons'; import { IconJsonOptions } from '../../models'; @@ -111,6 +111,9 @@ const adjustSaturation = ( ): ((value: string, index: number, array: string[]) => void) => { return (iconFileName) => { const svgFilePath = join(iconsPath, iconFileName); + if (!lstatSync(svgFilePath).isFile()) { + return; + } // Read SVG file const svg = readFileSync(svgFilePath, 'utf-8'); diff --git a/src/icons/generator/jsonGenerator.ts b/src/icons/generator/jsonGenerator.ts index 5408dead58..7e7c5aceae 100644 --- a/src/icons/generator/jsonGenerator.ts +++ b/src/icons/generator/jsonGenerator.ts @@ -26,6 +26,7 @@ import { validateOpacityValue, validateSaturationValue, } from './index'; +import { customClonesIcons } from './clones/clonesGenerator'; /** * Generate the complete icon configuration object that can be written as JSON file. @@ -73,7 +74,7 @@ export const createIconFile = ( getDefaultIconOptions(), updatedJSONConfig ); - const json = generateIconConfigurationObject(options); + let json = generateIconConfigurationObject(options); // make sure that the folder color, opacity and saturation values are entered correctly if ( @@ -131,6 +132,10 @@ export const createIconFile = ( setIconSaturation(options); } renameIconFiles(iconJsonPath, options); + + // generate custom cloned icons after opacity and saturation have + // been set so that those changes are also applied to the clones + json = merge({}, json, customClonesIcons(json, options)); } catch (error) { throw new Error('Failed to update icons: ' + error); } From d931fc4a59e0e73ccfe24df985a4588ff91accec Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Thu, 25 Apr 2024 12:13:57 -0300 Subject: [PATCH 03/25] =?UTF-8?q?chore:=20=F0=9F=A7=B9=20update=20vscode-t?= =?UTF-8?q?est=20dependency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vscode-test was renamed to @vscode/test-electron and the former package was unable to run tests on windows. this commit removes vscode-test and updates it to the last version of @vscode/test-electron --- package-lock.json | 582 ++++++++++++---------------------------------- package.json | 2 +- 2 files changed, 155 insertions(+), 429 deletions(-) diff --git a/package-lock.json b/package-lock.json index 32c7fc6663..7da6726389 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@types/vscode": "~1.51.0", "@typescript-eslint/eslint-plugin": "^5.26.0", "@typescript-eslint/parser": "^5.26.0", + "@vscode/test-electron": "^2.3.9", "axios": "^1.4.0", "changelog-machine": "^1.0.2", "eslint": "^8.16.0", @@ -37,7 +38,6 @@ "ts-loader": "^9.3.0", "ts-node": "^10.8.0", "typescript": "^4.7.2", - "vscode-test": "^1.6.1", "webpack": "^5.71.1", "webpack-cli": "^4.9.2" }, @@ -549,6 +549,21 @@ "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, + "node_modules/@vscode/test-electron": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.9.tgz", + "integrity": "sha512-z3eiChaCQXMqBnk2aHHSEkobmC2VRalFQN0ApOAtydL172zXGxTwGrRtviT5HnUB+Q+G3vtEYFtuQkYqBzYgMA==", + "dev": true, + "dependencies": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "jszip": "^3.10.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -920,25 +935,6 @@ } ] }, - "node_modules/big-integer": { - "version": "1.6.48", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", - "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, - "dependencies": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -959,12 +955,6 @@ "readable-stream": "^3.4.0" } }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "dev": true - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -1061,24 +1051,6 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "node_modules/buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true, - "engines": { - "node": ">=0.2.0" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1110,15 +1082,6 @@ "url": "https://opencollective.com/browserslist" } }, - "node_modules/chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, - "dependencies": { - "traverse": ">=0.3.0 <0.4" - } - }, "node_modules/chalk": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", @@ -1519,45 +1482,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.2" - } - }, - "node_modules/duplexer2/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/duplexer2/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/duplexer2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/electron-to-chromium": { "version": "1.3.736", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.736.tgz", @@ -2165,53 +2089,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/fstream/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fstream/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -2442,6 +2319,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2713,6 +2596,48 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -2735,11 +2660,14 @@ "node": ">= 0.8.0" } }, - "node_modules/listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "dependencies": { + "immediate": "~3.0.5" + } }, "node_modules/loader-runner": { "version": "4.2.0", @@ -2886,18 +2814,6 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, - "node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -3188,6 +3104,12 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4043,12 +3965,6 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", "dev": true }, - "node_modules/traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true - }, "node_modules/ts-loader": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.3.0.tgz", @@ -4188,54 +4104,6 @@ "through": "^2.3.8" } }, - "node_modules/unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, - "node_modules/unzipper/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/unzipper/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/unzipper/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4263,21 +4131,6 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, - "node_modules/vscode-test": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.6.1.tgz", - "integrity": "sha512-086q88T2ca1k95mUzffvbzb7esqQNvJgiwY4h29ukPhFo8u+vXOOmelUoU5EQUHs3Of8+JuQ3oGdbVCqaxuTXA==", - "dev": true, - "dependencies": { - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" - }, - "engines": { - "node": ">=8.9.3" - } - }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -4994,6 +4847,18 @@ "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", "dev": true }, + "@vscode/test-electron": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.9.tgz", + "integrity": "sha512-z3eiChaCQXMqBnk2aHHSEkobmC2VRalFQN0ApOAtydL172zXGxTwGrRtviT5HnUB+Q+G3vtEYFtuQkYqBzYgMA==", + "dev": true, + "requires": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "jszip": "^3.10.1", + "semver": "^7.5.2" + } + }, "@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -5300,22 +5165,6 @@ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true }, - "big-integer": { - "version": "1.6.48", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", - "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==", - "dev": true - }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -5333,12 +5182,6 @@ "readable-stream": "^3.4.0" } }, - "bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "dev": true - }, "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -5405,18 +5248,6 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true - }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true - }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -5435,15 +5266,6 @@ "integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==", "dev": true }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, - "requires": { - "traverse": ">=0.3.0 <0.4" - } - }, "chalk": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", @@ -5743,47 +5565,6 @@ "domhandler": "^4.2.0" } }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, "electron-to-chromium": { "version": "1.3.736", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.736.tgz", @@ -6225,43 +6006,6 @@ "dev": true, "optional": true }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -6426,6 +6170,12 @@ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -6627,6 +6377,50 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -6643,11 +6437,14 @@ "type-check": "~0.4.0" } }, - "listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "requires": { + "immediate": "~3.0.5" + } }, "loader-runner": { "version": "4.2.0", @@ -6758,15 +6555,6 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, "mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -6984,6 +6772,12 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7599,12 +7393,6 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", "dev": true }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true - }, "ts-loader": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.3.0.tgz", @@ -7692,56 +7480,6 @@ "through": "^2.3.8" } }, - "unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "dev": true, - "requires": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -7769,18 +7507,6 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, - "vscode-test": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.6.1.tgz", - "integrity": "sha512-086q88T2ca1k95mUzffvbzb7esqQNvJgiwY4h29ukPhFo8u+vXOOmelUoU5EQUHs3Of8+JuQ3oGdbVCqaxuTXA==", - "dev": true, - "requires": { - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "rimraf": "^3.0.2", - "unzipper": "^0.10.11" - } - }, "watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index 2c7396a8ee..192d11d399 100644 --- a/package.json +++ b/package.json @@ -328,6 +328,7 @@ "@types/vscode": "~1.51.0", "@typescript-eslint/eslint-plugin": "^5.26.0", "@typescript-eslint/parser": "^5.26.0", + "@vscode/test-electron": "^2.3.9", "axios": "^1.4.0", "changelog-machine": "^1.0.2", "eslint": "^8.16.0", @@ -343,7 +344,6 @@ "ts-loader": "^9.3.0", "ts-node": "^10.8.0", "typescript": "^4.7.2", - "vscode-test": "^1.6.1", "webpack": "^5.71.1", "webpack-cli": "^4.9.2" } From c1fafe67c40362dab7e19b2bfa7f93b89cca8ef0 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Thu, 25 Apr 2024 12:20:52 -0300 Subject: [PATCH 04/25] =?UTF-8?q?test:=20=F0=9F=A7=AA=20fix=20failing=20te?= =?UTF-8?q?sts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/icons/generator/clones/fileClone.ts | 4 ++-- src/icons/generator/clones/folderClone.ts | 4 ++-- .../generator/clones/utils/{svg.ts => cloning.ts} | 14 +++++++++++++- src/icons/generator/clones/utils/color/colors.ts | 2 +- src/models/iconConfiguration.ts | 4 ---- src/test/runTest.ts | 2 +- 6 files changed, 19 insertions(+), 11 deletions(-) rename src/icons/generator/clones/utils/{svg.ts => cloning.ts} (90%) diff --git a/src/icons/generator/clones/fileClone.ts b/src/icons/generator/clones/fileClone.ts index 89ba455646..bac941eecb 100644 --- a/src/icons/generator/clones/fileClone.ts +++ b/src/icons/generator/clones/fileClone.ts @@ -7,7 +7,7 @@ import { FileIconType, } from './utils/paths'; import { iconFolderPath } from '../constants'; -import { cloneIcon } from './utils/svg'; +import { cloneIcon, createCloneConfig } from './utils/cloning'; import { writeFileSync } from 'fs'; export function cloneFileIcon( @@ -28,7 +28,7 @@ function createFileIconClones( basePaths: IconPath[], hash: string ): IconConfiguration { - const config: IconConfiguration = new IconConfiguration(); + const config = createCloneConfig(); basePaths.forEach((base) => { try { diff --git a/src/icons/generator/clones/folderClone.ts b/src/icons/generator/clones/folderClone.ts index 017f6ea2aa..8f7430f7e4 100644 --- a/src/icons/generator/clones/folderClone.ts +++ b/src/icons/generator/clones/folderClone.ts @@ -7,7 +7,7 @@ import { getFolderIconClonePath, } from './utils/paths'; import { iconFolderPath } from '../constants'; -import { cloneIcon } from './utils/svg'; +import { cloneIcon, createCloneConfig } from './utils/cloning'; import { writeFileSync } from 'fs'; export function cloneFolderIcon( @@ -28,7 +28,7 @@ function createFolderClones( basePaths: IconPath[], hash: string ): IconConfiguration { - const config: IconConfiguration = new IconConfiguration(); + const config = createCloneConfig(); basePaths.forEach((base) => { try { diff --git a/src/icons/generator/clones/utils/svg.ts b/src/icons/generator/clones/utils/cloning.ts similarity index 90% rename from src/icons/generator/clones/utils/svg.ts rename to src/icons/generator/clones/utils/cloning.ts index a7f2c28386..7cbf06792d 100644 --- a/src/icons/generator/clones/utils/svg.ts +++ b/src/icons/generator/clones/utils/cloning.ts @@ -1,6 +1,6 @@ import { readFileSync } from 'fs'; import { INode, parseSync, stringify } from 'svgson'; -import { CustomClone } from '../../../../models'; +import { CustomClone, IconConfiguration } from '../../../../models'; import { getColorList, replacementMap } from './color/colors'; export function traverse(node: INode, callback: (node: INode) => void) { @@ -91,3 +91,15 @@ export function replaceColors(node: INode, replacements: Map) { } }); } + +export function createCloneConfig() { + const config = new IconConfiguration(); + config.light = { + fileExtensions: {}, + fileNames: {}, + folderNames: {}, + folderNamesExpanded: {}, + }; + + return config; +} diff --git a/src/icons/generator/clones/utils/color/colors.ts b/src/icons/generator/clones/utils/color/colors.ts index 3e89263be8..9c892444f4 100644 --- a/src/icons/generator/clones/utils/color/colors.ts +++ b/src/icons/generator/clones/utils/color/colors.ts @@ -1,5 +1,5 @@ import { INode } from 'svgson'; -import { getStyle, traverse } from '../svg'; +import { getStyle, traverse } from '../cloning'; import chroma, { valid } from 'chroma-js'; import { closerMaterialColorTo, diff --git a/src/models/iconConfiguration.ts b/src/models/iconConfiguration.ts index c50de1b79e..00ea4bab9a 100644 --- a/src/models/iconConfiguration.ts +++ b/src/models/iconConfiguration.ts @@ -27,14 +27,10 @@ export class IconConfiguration { this.light = { fileExtensions: {}, fileNames: {}, - folderNames: {}, - folderNamesExpanded: {}, }; this.highContrast = { fileExtensions: {}, fileNames: {}, - folderNames: {}, - folderNamesExpanded: {}, }; this.options = {}; } diff --git a/src/test/runTest.ts b/src/test/runTest.ts index f8a69a0b1e..69491bb706 100644 --- a/src/test/runTest.ts +++ b/src/test/runTest.ts @@ -1,5 +1,5 @@ import { resolve } from 'path'; -import { runTests } from 'vscode-test'; +import { runTests } from '@vscode/test-electron'; const main = async () => { try { From f4634a588677e5bf2f2f07c39d624241b74ba1e7 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Thu, 25 Apr 2024 15:24:31 -0300 Subject: [PATCH 05/25] =?UTF-8?q?refactor:=20=F0=9F=94=A8=20improve=20in-c?= =?UTF-8?q?ode=20docs=20&=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/icons/generator/clones/clonesGenerator.ts | 5 ++- src/icons/generator/clones/fileClone.ts | 17 +++++++++ src/icons/generator/clones/folderClone.ts | 32 +++++++++++++---- src/icons/generator/clones/utils/cloning.ts | 11 ++++++ .../generator/clones/utils/color/colors.ts | 21 +++++++---- .../clones/utils/color/material-palette.ts | 5 +++ src/icons/generator/clones/utils/paths.ts | 35 +++++++++++++------ 7 files changed, 99 insertions(+), 27 deletions(-) diff --git a/src/icons/generator/clones/clonesGenerator.ts b/src/icons/generator/clones/clonesGenerator.ts index 95527c10df..eb6ed99653 100644 --- a/src/icons/generator/clones/clonesGenerator.ts +++ b/src/icons/generator/clones/clonesGenerator.ts @@ -6,9 +6,8 @@ import { cloneFolderIcon } from './folderClone'; import { cloneFileIcon } from './fileClone'; /** - * This function applies custom colors to the svg icon, respecting - * the different tones of the original icon based on a provided - * "base" color. + * Creates custom icons by cloning already existing icons and changing + * their colors, allowing users create their own variations. */ export function customClonesIcons( config: IconConfiguration, diff --git a/src/icons/generator/clones/fileClone.ts b/src/icons/generator/clones/fileClone.ts index bac941eecb..3ab0746bf0 100644 --- a/src/icons/generator/clones/fileClone.ts +++ b/src/icons/generator/clones/fileClone.ts @@ -10,6 +10,13 @@ import { iconFolderPath } from '../constants'; import { cloneIcon, createCloneConfig } from './utils/cloning'; import { writeFileSync } from 'fs'; +/** + * Generates a clone of a file icon. + * @param cloneOpts options and configurations on how to clone the file icon + * @param config global icon configuration (used to get the base icon) + * @param hash current hash being applied to the icons + * @returns a partial icon configuration for the new file icon + */ export function cloneFileIcon( cloneOpts: FileIconClone, config: IconConfiguration, @@ -23,6 +30,11 @@ export function cloneFileIcon( return createFileIconClones(cloneOpts, basePaths, hash); } +/** + * for each base icon, creates its clone, recolorizes it and writes it to the disk + * + * @returns partial icon configuration for the cloned file icons + */ function createFileIconClones( cloneOpts: FileIconClone, basePaths: IconPath[], @@ -36,20 +48,25 @@ function createFileIconClones( const iconPathConfig = `${iconFolderPath}clones/${basename( filePath.path )}`; + + // generates the new icon content const content = cloneIcon(base.path, hash, cloneOpts); const iconName = basename(filePath.path, '.svg'); try { + // create the .svg file for the cloned icon writeFileSync(filePath.path, content); } catch (error) { console.error(error); return; } + // set the icon path for the cloned icon in the configuration config.iconDefinitions![iconName] = { iconPath: iconPathConfig, }; + // set associations for the cloned icon in the configuration cloneOpts.fileNames?.forEach((fileName) => { switch (filePath.type) { case FileIconType.Base: diff --git a/src/icons/generator/clones/folderClone.ts b/src/icons/generator/clones/folderClone.ts index 8f7430f7e4..3349adef82 100644 --- a/src/icons/generator/clones/folderClone.ts +++ b/src/icons/generator/clones/folderClone.ts @@ -10,11 +10,19 @@ import { iconFolderPath } from '../constants'; import { cloneIcon, createCloneConfig } from './utils/cloning'; import { writeFileSync } from 'fs'; +/** + * Generates a clone of a folder icon. + * @param cloneOpts options and configurations on how to clone the folder icon + * @param config global icon configuration (used to get the base icon) + * @param hash current hash being applied to the icons + * @returns a partial icon configuration for the new folder icon + */ export function cloneFolderIcon( cloneOpts: FolderIconClone, config: IconConfiguration, hash: string ): IconConfiguration { + // get the paths of the base icons const basePaths = getFolderIconBasePaths(cloneOpts, config); if (!basePaths) { return {}; @@ -23,6 +31,11 @@ export function cloneFolderIcon( return createFolderClones(cloneOpts, basePaths, hash); } +/** + * for each base icon, creates its clone, recolorizes it and writes it to the disk + * + * @returns partial icon configuration for the cloned folder icons + */ function createFolderClones( cloneOpts: FolderIconClone, basePaths: IconPath[], @@ -32,26 +45,31 @@ function createFolderClones( basePaths.forEach((base) => { try { - const filePath = getFolderIconClonePath(base, cloneOpts, hash); - const iconPathConfig = `${iconFolderPath}clones/${basename( - filePath.path + const clonePath = getFolderIconClonePath(base, cloneOpts, hash); + const clonePathConfig = `${iconFolderPath}clones/${basename( + clonePath.path )}`; + + // generates the new icon content const content = cloneIcon(base.path, hash, cloneOpts); - const iconName = basename(filePath.path, '.svg'); + const iconName = basename(clonePath.path, '.svg'); try { - writeFileSync(filePath.path, content); + // create the .svg file for the cloned icon + writeFileSync(clonePath.path, content); } catch (error) { console.error(error); return; } + // sets the icon path for the cloned icon in the configuration config.iconDefinitions![iconName] = { - iconPath: iconPathConfig, + iconPath: clonePathConfig, }; + // sets the associated folder names for the cloned icon cloneOpts.folderNames?.forEach((folderName) => { - switch (filePath.type) { + switch (clonePath.type) { case FolderIconType.Base: config.folderNames![folderName] = iconName; break; diff --git a/src/icons/generator/clones/utils/cloning.ts b/src/icons/generator/clones/utils/cloning.ts index 7cbf06792d..65d1af925f 100644 --- a/src/icons/generator/clones/utils/cloning.ts +++ b/src/icons/generator/clones/utils/cloning.ts @@ -3,6 +3,10 @@ import { INode, parseSync, stringify } from 'svgson'; import { CustomClone, IconConfiguration } from '../../../../models'; import { getColorList, replacementMap } from './color/colors'; +/** + * Recursively walks through an SVG node tree and its children, + * calling a callback on each node. + */ export function traverse(node: INode, callback: (node: INode) => void) { callback(node); if (node.children) { @@ -10,6 +14,7 @@ export function traverse(node: INode, callback: (node: INode) => void) { } } +/** Reads an icon from the file system and returns its content. */ export function readIcon(path: string, hash: string): string { try { return readFileSync(path, 'utf8'); @@ -19,6 +24,7 @@ export function readIcon(path: string, hash: string): string { } } +/** Clones an icon and changes its colors according to the clone options. */ export function cloneIcon( path: string, hash: string, @@ -31,6 +37,7 @@ export function cloneIcon( return stringify(svg); } +/** Gets the style attribute of an SVG node if it exists. */ export function getStyle(node: INode) { if (node && node.attributes && node.attributes.style) { return parseStyle(node.attributes.style); @@ -38,6 +45,7 @@ export function getStyle(node: INode) { return {}; } +/** Parses the style attribute of an SVG node. */ function parseStyle(css: string) { const rules = css.split(';'); const result: Record = {}; @@ -48,12 +56,14 @@ function parseStyle(css: string) { return result; } +/** Converts object to css style string. */ export function stringifyStyle(css: Record) { return Object.entries(css) .map(([key, value]) => `${key}:${value}`) .join(';'); } +/** Replaces colors in an SVG node using a replacement map. */ export function replaceColors(node: INode, replacements: Map) { traverse(node, (node) => { // replace colors in style attribute @@ -92,6 +102,7 @@ export function replaceColors(node: INode, replacements: Map) { }); } +/** Creates a clone configuration with empty light object. */ export function createCloneConfig() { const config = new IconConfiguration(); config.light = { diff --git a/src/icons/generator/clones/utils/color/colors.ts b/src/icons/generator/clones/utils/color/colors.ts index 9c892444f4..ecd677f63e 100644 --- a/src/icons/generator/clones/utils/color/colors.ts +++ b/src/icons/generator/clones/utils/color/colors.ts @@ -6,6 +6,7 @@ import { getMaterialColorByKey, } from './material-palette'; +/** Get all the colors used in the SVG node as a `Set` list. **/ export function getColorList(node: INode) { const colors = new Set(); @@ -22,7 +23,7 @@ export function getColorList(node: INode) { } } - // check colors in attributes + // check colors in svg attributes if (node.attributes) { if (node.attributes.fill && isValidColor(node.attributes.fill)) { colors.add(node.attributes.fill); @@ -44,6 +45,7 @@ export function getColorList(node: INode) { return colors; } +/** given a set of colors, orders them from dark to light. **/ function orderDarkToLight(colors: Set) { const colorArray = Array.from(colors); return colorArray.sort((a, b) => { @@ -58,17 +60,22 @@ function orderDarkToLight(colors: Set) { }); } -export function isValidColor(hexColor: string | undefined): boolean { - if (hexColor === undefined) { +/** checks if a string is a valid color. **/ +export function isValidColor(color: string | undefined): boolean { + if (color === undefined) { return false; } - return valid(hexColor); + return valid(color); } /** - * receives a base color and a list of colors, orders the list of colors from dark to light - * and replaces the darkest color (first in the list) with the base color, then grabs the hue - * of the base color and applies it to the rest of the colors in the list. + * Creates a map of color replacements based on the base color and + * the list of colors. + * + * Orders the list of colors from dark to light and replaces the darkest + * color with the base color. Then uses the hue of the base color and + * the material palette to find the most appropriate color for the rest + * in the list. */ export function replacementMap(baseColor: string, colors: Set) { if (!isValidColor(baseColor)) { diff --git a/src/icons/generator/clones/utils/color/material-palette.ts b/src/icons/generator/clones/utils/color/material-palette.ts index 1398a8ec33..f5c4db24b4 100644 --- a/src/icons/generator/clones/utils/color/material-palette.ts +++ b/src/icons/generator/clones/utils/color/material-palette.ts @@ -272,6 +272,10 @@ export function getMaterialColorByKey(key: string): string | undefined { return undefined; } +/** + * Given a color, returns the closest material color from the + * material palette. + */ export function closerMaterialColorTo(color: string): string { const palette = Object.values(materialPalette); @@ -283,6 +287,7 @@ export function closerMaterialColorTo(color: string): string { const distances = palette .map((paletteColor) => ({ + // calculate the distance between the color and the palette color distance: deltaE(paletteColor, color), color: paletteColor, })) diff --git a/src/icons/generator/clones/utils/paths.ts b/src/icons/generator/clones/utils/paths.ts index e5b169d9bb..28e5be5f16 100644 --- a/src/icons/generator/clones/utils/paths.ts +++ b/src/icons/generator/clones/utils/paths.ts @@ -5,6 +5,7 @@ import { IconConfiguration, } from '../../../../models'; import { existsSync, mkdirSync, rmSync } from 'fs'; +import { lightColorFileEnding, openedFolder } from '../../constants'; export enum FolderIconType { Base, @@ -23,6 +24,7 @@ export interface IconPath { type: T; } +/** resolves the path of the icon depending of the caller */ function resolvePath(path: string): string { if (basename(__dirname) === 'dist') { return join(__dirname, String(path)); @@ -32,27 +34,31 @@ function resolvePath(path: string): string { } } +/** returns the paths of the base file icons to be cloned (base, light) */ export function getFileIconBasePaths( cloneOpts: FileIconClone, config: IconConfiguration ): IconPath[] | undefined { const paths = []; const base = config.iconDefinitions?.[`${cloneOpts.base}`]?.iconPath; - const light = config.iconDefinitions?.[`${cloneOpts.base}_light`]?.iconPath; + const light = + config.iconDefinitions?.[`${cloneOpts.base}${lightColorFileEnding}`] + ?.iconPath; if (base) { - base && paths.push({ type: FileIconType.Base, path: resolvePath(base) }); + paths.push({ type: FileIconType.Base, path: resolvePath(base) }); light && paths.push({ type: FileIconType.Light, path: resolvePath(light) }); return paths; } } +/** creates and returns the path of the cloned file icon */ export function getFileIconClonePath( base: IconPath, cloneOpts: FileIconClone, hash: string ): IconPath { - const sufix = base.type === FileIconType.Light ? '_light' : ''; + const sufix = base.type === FileIconType.Light ? lightColorFileEnding : ''; const clonePath = join( dirname(base.path), @@ -66,6 +72,10 @@ export function getFileIconClonePath( }; } +/** + * returns the paths of the base folder icons to be cloned + * (base, open, light, light-open) + */ export function getFolderIconBasePaths( cloneOpts: FolderIconClone, config: IconConfiguration @@ -75,10 +85,14 @@ export function getFolderIconBasePaths( cloneOpts.base === 'folder' ? 'folder' : `folder-${cloneOpts.base}`; const base = config.iconDefinitions?.[`${folderBase}`]?.iconPath; - const open = config.iconDefinitions?.[`${folderBase}-open`]?.iconPath; - const light = config.iconDefinitions?.[`${folderBase}_light`]?.iconPath; + const open = + config.iconDefinitions?.[`${folderBase}${openedFolder}`]?.iconPath; + const light = + config.iconDefinitions?.[`${folderBase}${lightColorFileEnding}`]?.iconPath; const lightOpen = - config.iconDefinitions?.[`${folderBase}-open_light`]?.iconPath; + config.iconDefinitions?.[ + `${folderBase}${openedFolder}${lightColorFileEnding}` + ]?.iconPath; if (base && open) { base && paths.push({ type: FolderIconType.Base, path: resolvePath(base) }); @@ -99,6 +113,7 @@ export function getFolderIconBasePaths( } } +/** creates and returns the path of the cloned folder icon */ export function getFolderIconClonePath( base: IconPath, cloneOpts: FolderIconClone, @@ -110,13 +125,13 @@ export function getFolderIconClonePath( case FolderIconType.Base: break; case FolderIconType.Open: - sufix = '-open'; + sufix = openedFolder; break; case FolderIconType.Light: - sufix = '_light'; + sufix = lightColorFileEnding; break; case FolderIconType.LightOpen: - sufix = '-open_light'; + sufix = `${openedFolder}${lightColorFileEnding}`; break; } @@ -134,7 +149,7 @@ export function getFolderIconClonePath( /** * removes the clones folder if it exists - * and creates a new one + * and creates a new one if `keep` is true */ export function clearCloneFolder(keep: boolean = true): void { const clonesFolderPath = resolvePath('./../icons/clones'); From 9e1ba796ca6cb1f9c5836ce1adb25c6d351a9604 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Thu, 25 Apr 2024 15:52:09 -0300 Subject: [PATCH 06/25] =?UTF-8?q?feat:=20=E2=9C=A8=20config=20to=20create?= =?UTF-8?q?=20light=20variants=20of=20the=20icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/icons/generator/clones/fileClone.ts | 7 ++++++- src/icons/generator/clones/folderClone.ts | 10 +++++++++- src/icons/generator/clones/utils/cloning.ts | 10 +++------- src/icons/generator/clones/utils/paths.ts | 17 ++++++++++++++--- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/icons/generator/clones/fileClone.ts b/src/icons/generator/clones/fileClone.ts index 3ab0746bf0..0e012b4e59 100644 --- a/src/icons/generator/clones/fileClone.ts +++ b/src/icons/generator/clones/fileClone.ts @@ -49,8 +49,13 @@ function createFileIconClones( filePath.path )}`; + const baseColor = + base.type === FileIconType.Base + ? cloneOpts.color + : cloneOpts.lightColor ?? cloneOpts.color; + // generates the new icon content - const content = cloneIcon(base.path, hash, cloneOpts); + const content = cloneIcon(base.path, hash, baseColor); const iconName = basename(filePath.path, '.svg'); try { diff --git a/src/icons/generator/clones/folderClone.ts b/src/icons/generator/clones/folderClone.ts index 3349adef82..3cdf4e7946 100644 --- a/src/icons/generator/clones/folderClone.ts +++ b/src/icons/generator/clones/folderClone.ts @@ -50,8 +50,12 @@ function createFolderClones( clonePath.path )}`; + const baseColor = isDarkThemeIcon(clonePath) + ? cloneOpts.color + : cloneOpts.lightColor ?? cloneOpts.color; + // generates the new icon content - const content = cloneIcon(base.path, hash, cloneOpts); + const content = cloneIcon(base.path, hash, baseColor); const iconName = basename(clonePath.path, '.svg'); try { @@ -91,3 +95,7 @@ function createFolderClones( return config; } + +function isDarkThemeIcon(path: IconPath): boolean { + return path.type === FolderIconType.Base || path.type === FolderIconType.Open; +} diff --git a/src/icons/generator/clones/utils/cloning.ts b/src/icons/generator/clones/utils/cloning.ts index 65d1af925f..d67c2dc8b0 100644 --- a/src/icons/generator/clones/utils/cloning.ts +++ b/src/icons/generator/clones/utils/cloning.ts @@ -1,6 +1,6 @@ import { readFileSync } from 'fs'; import { INode, parseSync, stringify } from 'svgson'; -import { CustomClone, IconConfiguration } from '../../../../models'; +import { IconConfiguration } from '../../../../models'; import { getColorList, replacementMap } from './color/colors'; /** @@ -25,14 +25,10 @@ export function readIcon(path: string, hash: string): string { } /** Clones an icon and changes its colors according to the clone options. */ -export function cloneIcon( - path: string, - hash: string, - cloneOpts: CustomClone -): string { +export function cloneIcon(path: string, hash: string, color: string): string { const baseContent = readIcon(path, hash); const svg = parseSync(baseContent); - const replacements = replacementMap(cloneOpts.color, getColorList(svg)); + const replacements = replacementMap(color, getColorList(svg)); replaceColors(svg, replacements); return stringify(svg); } diff --git a/src/icons/generator/clones/utils/paths.ts b/src/icons/generator/clones/utils/paths.ts index 28e5be5f16..ba5419b963 100644 --- a/src/icons/generator/clones/utils/paths.ts +++ b/src/icons/generator/clones/utils/paths.ts @@ -41,10 +41,15 @@ export function getFileIconBasePaths( ): IconPath[] | undefined { const paths = []; const base = config.iconDefinitions?.[`${cloneOpts.base}`]?.iconPath; - const light = + let light = config.iconDefinitions?.[`${cloneOpts.base}${lightColorFileEnding}`] ?.iconPath; + if (cloneOpts.lightColor && !light) { + // the original icon does not have a light version, so we re-use the base + light = base; + } + if (base) { paths.push({ type: FileIconType.Base, path: resolvePath(base) }); light && paths.push({ type: FileIconType.Light, path: resolvePath(light) }); @@ -87,9 +92,9 @@ export function getFolderIconBasePaths( const base = config.iconDefinitions?.[`${folderBase}`]?.iconPath; const open = config.iconDefinitions?.[`${folderBase}${openedFolder}`]?.iconPath; - const light = + let light = config.iconDefinitions?.[`${folderBase}${lightColorFileEnding}`]?.iconPath; - const lightOpen = + let lightOpen = config.iconDefinitions?.[ `${folderBase}${openedFolder}${lightColorFileEnding}` ]?.iconPath; @@ -98,6 +103,12 @@ export function getFolderIconBasePaths( base && paths.push({ type: FolderIconType.Base, path: resolvePath(base) }); open && paths.push({ type: FolderIconType.Open, path: resolvePath(open) }); + if (cloneOpts.lightColor && (!light || !lightOpen)) { + // the original icon does not have a light version, so we re-use the base icons + light = base; + lightOpen = open; + } + if (light) { paths.push({ type: FolderIconType.Light, path: resolvePath(light) }); } From 8522d4d48a1f063965381a39bd683847e828d373 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Thu, 25 Apr 2024 20:41:06 -0300 Subject: [PATCH 07/25] =?UTF-8?q?feat:=20=E2=9C=A8=20improve=20recolorizat?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit improves recolorization logic, keeping originally darker colors dark and lighter colors light even after recolor --- .../generator/clones/utils/color/colors.ts | 47 ++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/src/icons/generator/clones/utils/color/colors.ts b/src/icons/generator/clones/utils/color/colors.ts index ecd677f63e..1e450a54a8 100644 --- a/src/icons/generator/clones/utils/color/colors.ts +++ b/src/icons/generator/clones/utils/color/colors.ts @@ -1,6 +1,6 @@ import { INode } from 'svgson'; import { getStyle, traverse } from '../cloning'; -import chroma, { valid } from 'chroma-js'; +import chroma, { Color, valid } from 'chroma-js'; import { closerMaterialColorTo, getMaterialColorByKey, @@ -49,17 +49,24 @@ export function getColorList(node: INode) { function orderDarkToLight(colors: Set) { const colorArray = Array.from(colors); return colorArray.sort((a, b) => { - const colorA = chroma(a); - const colorB = chroma(b); - - // determine which one is darker based on saturation and lightness - const aDarkness = colorA.get('hsl.l') * colorA.get('hsl.s'); - const bDarkness = colorB.get('hsl.l') * colorB.get('hsl.s'); - - return aDarkness - bDarkness; + // sort by lightness + const lA = chroma(a).get('hsl.l'); + const lB = chroma(b).get('hsl.l'); + + if (lA < lB) { + return -1; + } else if (lA > lB) { + return 1; + } else { + return 0; + } }); } +/** Lightens a color by a given percentage. **/ +const lighten = (color: Color, hslPercent: number) => + color.set('hsl.l', color.get('hsl.l') + hslPercent); + /** checks if a string is a valid color. **/ export function isValidColor(color: string | undefined): boolean { if (color === undefined) { @@ -91,14 +98,30 @@ export function replacementMap(baseColor: string, colors: Set) { const orderedColors = orderDarkToLight(colors); const baseColorChroma = chroma(baseColor); const baseHue = baseColorChroma.get('hsl.h'); - const replacement = new Map(); replacement.set(orderedColors[0], baseColor); + // keep track of the latest color to determine if the next color + // should be lightened or not. + let latestColor = baseColorChroma; + for (let i = 1; i < orderedColors.length; i++) { const color = chroma(orderedColors[i]); - const newColor = color.set('hsl.h', baseHue).hex(); - const matCol = closerMaterialColorTo(newColor); + let newColor = color.set('hsl.h', baseHue); + + // the idea is to keep the paths with the same relative darkness + // as the original icon, but with different hues. So if the + // new color results in a darker color (as we are looping from + // dark to light), we set the lightness to the latest color and + // then lighten it a bit so that it's brighter than the latest one. + if (newColor.luminance() < latestColor.luminance()) { + newColor = newColor.set('hsl.l', latestColor.get('hsl.l')); + newColor = lighten(newColor, 0.1); + } + + const matCol = closerMaterialColorTo(newColor.hex()); + latestColor = chroma(matCol); + replacement.set(orderedColors[i], matCol); } From 8d399c42f379af9898a166f850c426bf5c631d58 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Thu, 25 Apr 2024 20:43:12 -0300 Subject: [PATCH 08/25] =?UTF-8?q?feat:=20=E2=9C=A8=20edge=20cases:=20suppo?= =?UTF-8?q?rt=20ignoring=20recolorizing=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adds support for a custom svg attribute `mit-no-recolor` that, when set to true, will keep the original color of the svg node on which is applied --- src/icons/generator/clones/utils/cloning.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/icons/generator/clones/utils/cloning.ts b/src/icons/generator/clones/utils/cloning.ts index d67c2dc8b0..6ca355fc32 100644 --- a/src/icons/generator/clones/utils/cloning.ts +++ b/src/icons/generator/clones/utils/cloning.ts @@ -8,9 +8,12 @@ import { getColorList, replacementMap } from './color/colors'; * calling a callback on each node. */ export function traverse(node: INode, callback: (node: INode) => void) { - callback(node); - if (node.children) { - node.children.forEach((child) => traverse(child, callback)); + if (node.attributes['mit-no-recolor'] !== 'true') { + callback(node); + + if (node.children) { + node.children.forEach((child) => traverse(child, callback)); + } } } From 8445f2fe776d6055ed0c0e047680f9fd80ce8839 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Thu, 25 Apr 2024 20:44:27 -0300 Subject: [PATCH 09/25] =?UTF-8?q?feat:=20=E2=9C=A8=20do=20not=20recolor=20?= =?UTF-8?q?some=20paths=20in=20some=20edge-case=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- icons/azure-pipelines.svg | 2 -- icons/blink_light.svg | 2 +- icons/browserlist_light.svg | 2 +- icons/folder-gitlab-open.svg | 2 +- icons/folder-gitlab.svg | 2 +- icons/folder-intellij-open.svg | 2 +- icons/folder-intellij-open_light.svg | 2 +- icons/folder-intellij.svg | 2 +- icons/folder-intellij_light.svg | 2 +- icons/folder-sublime-open.svg | 2 +- icons/folder-sublime.svg | 2 +- icons/folder-vuex-store-open.svg | 2 +- icons/folder-vuex-store.svg | 2 +- icons/fusebox.svg | 2 +- icons/go_gopher.svg | 2 +- icons/openapi_light.svg | 2 +- icons/rubocop.svg | 2 +- icons/rubocop_light.svg | 2 +- icons/spwn.svg | 3 +-- icons/tsconfig.svg | 2 +- icons/vue-config.svg | 2 +- 21 files changed, 20 insertions(+), 23 deletions(-) diff --git a/icons/azure-pipelines.svg b/icons/azure-pipelines.svg index c39954cfdb..9c41b59810 100644 --- a/icons/azure-pipelines.svg +++ b/icons/azure-pipelines.svg @@ -3,10 +3,8 @@ - - diff --git a/icons/blink_light.svg b/icons/blink_light.svg index f58d602866..58c253301e 100644 --- a/icons/blink_light.svg +++ b/icons/blink_light.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/icons/browserlist_light.svg b/icons/browserlist_light.svg index 762866ca39..63bf6b4b84 100644 --- a/icons/browserlist_light.svg +++ b/icons/browserlist_light.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/icons/folder-gitlab-open.svg b/icons/folder-gitlab-open.svg index da0c559e32..ccd481973b 100644 --- a/icons/folder-gitlab-open.svg +++ b/icons/folder-gitlab-open.svg @@ -1,6 +1,6 @@ - + diff --git a/icons/folder-gitlab.svg b/icons/folder-gitlab.svg index ce26697dc5..b07e5d8350 100644 --- a/icons/folder-gitlab.svg +++ b/icons/folder-gitlab.svg @@ -1,6 +1,6 @@ - + diff --git a/icons/folder-intellij-open.svg b/icons/folder-intellij-open.svg index a2fa93082d..4149260d22 100644 --- a/icons/folder-intellij-open.svg +++ b/icons/folder-intellij-open.svg @@ -1,5 +1,5 @@ - + diff --git a/icons/folder-intellij-open_light.svg b/icons/folder-intellij-open_light.svg index 168a38bcd0..7e5ae0e814 100644 --- a/icons/folder-intellij-open_light.svg +++ b/icons/folder-intellij-open_light.svg @@ -1,5 +1,5 @@ - + diff --git a/icons/folder-intellij.svg b/icons/folder-intellij.svg index 7956aa7fb6..bdfe3e18c9 100644 --- a/icons/folder-intellij.svg +++ b/icons/folder-intellij.svg @@ -1,5 +1,5 @@ - + diff --git a/icons/folder-intellij_light.svg b/icons/folder-intellij_light.svg index e940482b85..9b34936d56 100644 --- a/icons/folder-intellij_light.svg +++ b/icons/folder-intellij_light.svg @@ -1,5 +1,5 @@ - + diff --git a/icons/folder-sublime-open.svg b/icons/folder-sublime-open.svg index e8e9fd3372..67fb725324 100644 --- a/icons/folder-sublime-open.svg +++ b/icons/folder-sublime-open.svg @@ -1,4 +1,4 @@ - + diff --git a/icons/folder-sublime.svg b/icons/folder-sublime.svg index b00981a85f..6194f97d82 100644 --- a/icons/folder-sublime.svg +++ b/icons/folder-sublime.svg @@ -1,4 +1,4 @@ - + diff --git a/icons/folder-vuex-store-open.svg b/icons/folder-vuex-store-open.svg index 4925535b96..6ab3ba837b 100644 --- a/icons/folder-vuex-store-open.svg +++ b/icons/folder-vuex-store-open.svg @@ -1,6 +1,6 @@ - + diff --git a/icons/folder-vuex-store.svg b/icons/folder-vuex-store.svg index 1ce110005a..a11b263d46 100644 --- a/icons/folder-vuex-store.svg +++ b/icons/folder-vuex-store.svg @@ -1,6 +1,6 @@ - + diff --git a/icons/fusebox.svg b/icons/fusebox.svg index 1ea89f4d3a..74073cdba4 100644 --- a/icons/fusebox.svg +++ b/icons/fusebox.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/icons/go_gopher.svg b/icons/go_gopher.svg index 42be334e6a..800931ebe6 100644 --- a/icons/go_gopher.svg +++ b/icons/go_gopher.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/icons/openapi_light.svg b/icons/openapi_light.svg index eb3e934fe5..51e71774e4 100644 --- a/icons/openapi_light.svg +++ b/icons/openapi_light.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/icons/rubocop.svg b/icons/rubocop.svg index 817936fe80..3ba2915e1a 100644 --- a/icons/rubocop.svg +++ b/icons/rubocop.svg @@ -2,6 +2,6 @@ - + diff --git a/icons/rubocop_light.svg b/icons/rubocop_light.svg index 2befc83c64..5f31981e60 100644 --- a/icons/rubocop_light.svg +++ b/icons/rubocop_light.svg @@ -2,6 +2,6 @@ - + diff --git a/icons/spwn.svg b/icons/spwn.svg index 15c9619652..d2fbaee287 100644 --- a/icons/spwn.svg +++ b/icons/spwn.svg @@ -1,2 +1 @@ - - + \ No newline at end of file diff --git a/icons/tsconfig.svg b/icons/tsconfig.svg index 31f4b2755f..90c903a397 100644 --- a/icons/tsconfig.svg +++ b/icons/tsconfig.svg @@ -1,4 +1,4 @@ - + diff --git a/icons/vue-config.svg b/icons/vue-config.svg index cc06c39a46..b459750819 100644 --- a/icons/vue-config.svg +++ b/icons/vue-config.svg @@ -1,5 +1,5 @@ - + From 77d76a9e8283f92a0fc50d2ca7506cb287eff5d5 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Thu, 25 Apr 2024 20:59:55 -0300 Subject: [PATCH 10/25] =?UTF-8?q?feat:=20=E2=9C=A8=20jsconfig=20edge-case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- icons/jsconfig.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/icons/jsconfig.svg b/icons/jsconfig.svg index 41cdc378ee..53b872eb9f 100644 --- a/icons/jsconfig.svg +++ b/icons/jsconfig.svg @@ -1,4 +1,4 @@ - + From 15ba192915c3ec26bca595cee137c35e7d6f11bd Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Fri, 26 Apr 2024 13:37:41 -0300 Subject: [PATCH 11/25] =?UTF-8?q?refactor:=20=F0=9F=94=A8=20simplify=20clo?= =?UTF-8?q?ning=20process?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/icons/generator/clones/clonesGenerator.ts | 108 +++++++- src/icons/generator/clones/fileClone.ts | 102 ------- src/icons/generator/clones/folderClone.ts | 101 ------- .../generator/clones/utils/clone-data.ts | 251 ++++++++++++++++++ src/icons/generator/clones/utils/paths.ts | 175 ------------ 5 files changed, 351 insertions(+), 386 deletions(-) delete mode 100644 src/icons/generator/clones/fileClone.ts delete mode 100644 src/icons/generator/clones/folderClone.ts create mode 100644 src/icons/generator/clones/utils/clone-data.ts delete mode 100644 src/icons/generator/clones/utils/paths.ts diff --git a/src/icons/generator/clones/clonesGenerator.ts b/src/icons/generator/clones/clonesGenerator.ts index eb6ed99653..6e81b8e27f 100644 --- a/src/icons/generator/clones/clonesGenerator.ts +++ b/src/icons/generator/clones/clonesGenerator.ts @@ -1,9 +1,19 @@ -import { IconConfiguration, IconJsonOptions } from '../../../models'; -import { clearCloneFolder } from './utils/paths'; +import { + FileIconClone, + FolderIconClone, + IconConfiguration, + IconJsonOptions, +} from '../../../models'; +import { + Variant, + clearCloneFolder, + getCloneData, + isFolder, +} from './utils/clone-data'; import merge from 'lodash.merge'; import { getFileConfigHash } from '../../../helpers/fileConfig'; -import { cloneFolderIcon } from './folderClone'; -import { cloneFileIcon } from './fileClone'; +import { cloneIcon, createCloneConfig } from './utils/cloning'; +import { writeFileSync } from 'fs'; /** * Creates custom icons by cloning already existing icons and changing @@ -18,22 +28,104 @@ export function customClonesIcons( let clonedIconsConfig: IconConfiguration = new IconConfiguration(); const hash = getFileConfigHash(options); + // create folder clones as specified by the user in the options options.folders?.customClones?.forEach((clone) => { - const cloneIcon = cloneFolderIcon(clone, config, hash); - clonedIconsConfig = merge(clonedIconsConfig, cloneIcon); + const cloneCfg = createIconClone(clone, config, hash); + clonedIconsConfig = merge(clonedIconsConfig, cloneCfg); }); + // create file clones as specified by the user in the options options.files?.customClones?.forEach((clone) => { - const cloneIcon = cloneFileIcon(clone, config, hash); - clonedIconsConfig = merge(clonedIconsConfig, cloneIcon); + const cloneCfg = createIconClone(clone, config, hash); + clonedIconsConfig = merge(clonedIconsConfig, cloneCfg); }); return clonedIconsConfig; } +/** Checks if there are any custom clones to be created */ export function hasCustomClones(options: IconJsonOptions): boolean { return ( (options.folders?.customClones?.length ?? 0) > 0 || (options.files?.customClones?.length ?? 0) > 0 ); } + +/** + * Generates a clone of an icon. + * @param cloneOpts options and configurations on how to clone the icon + * @param config global icon configuration (used to get the base icon) + * @param hash current hash being applied to the icons + * @returns a partial icon configuration for the new icon + */ +export function createIconClone( + cloneOpts: FolderIconClone | FileIconClone, + config: IconConfiguration, + hash: string +): IconConfiguration { + // get clones to be created + const clones = getCloneData(cloneOpts, config, hash); + if (!clones) { + return {}; + } + + const clonesConfig = createCloneConfig(); + + clones.forEach((clone) => { + try { + // generates the new icon content (svg) + const content = cloneIcon(clone.base.path, hash, clone.color); + + try { + // write the new .svg file to the disk + writeFileSync(clone.path, content); + } catch (error) { + console.error(error); + return; + } + + // sets the icon path for the cloned icon in the configuration + clonesConfig.iconDefinitions![clone.name] = { + iconPath: clone.inConfigPath, + }; + + if (isFolder(cloneOpts)) { + // sets the associated folder names for the cloned icon + cloneOpts.folderNames?.forEach((folderName) => { + const folderNamesCfg = + clone.variant === Variant.Base + ? clonesConfig.folderNames! + : clone.variant === Variant.Open + ? clonesConfig.folderNamesExpanded! + : clone.variant === Variant.Light + ? clonesConfig.light!.folderNames! + : clonesConfig.light!.folderNamesExpanded!; + folderNamesCfg[folderName] = clone.name; + }); + } else { + // set associations for the cloned file icon in the configuration + cloneOpts.fileNames?.forEach((fileName) => { + const fileNamesCfg = + clone.variant === Variant.Base + ? clonesConfig.fileNames! + : clonesConfig.light!.fileNames!; + + fileNamesCfg[fileName] = clone.name; + }); + + cloneOpts.fileExtensions?.forEach((fileExtension) => { + const fileExtensionsCfg = + clone.variant === Variant.Base + ? clonesConfig.fileExtensions! + : clonesConfig.light!.fileExtensions!; + + fileExtensionsCfg[fileExtension] = clone.name; + }); + } + } catch (error) { + console.error(error); + } + }); + + return clonesConfig; +} diff --git a/src/icons/generator/clones/fileClone.ts b/src/icons/generator/clones/fileClone.ts deleted file mode 100644 index 0e012b4e59..0000000000 --- a/src/icons/generator/clones/fileClone.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { basename } from 'path'; -import { FileIconClone, IconConfiguration } from '../../../models'; -import { - IconPath, - getFileIconBasePaths, - getFileIconClonePath, - FileIconType, -} from './utils/paths'; -import { iconFolderPath } from '../constants'; -import { cloneIcon, createCloneConfig } from './utils/cloning'; -import { writeFileSync } from 'fs'; - -/** - * Generates a clone of a file icon. - * @param cloneOpts options and configurations on how to clone the file icon - * @param config global icon configuration (used to get the base icon) - * @param hash current hash being applied to the icons - * @returns a partial icon configuration for the new file icon - */ -export function cloneFileIcon( - cloneOpts: FileIconClone, - config: IconConfiguration, - hash: string -): IconConfiguration { - const basePaths = getFileIconBasePaths(cloneOpts, config); - if (!basePaths) { - return {}; - } - - return createFileIconClones(cloneOpts, basePaths, hash); -} - -/** - * for each base icon, creates its clone, recolorizes it and writes it to the disk - * - * @returns partial icon configuration for the cloned file icons - */ -function createFileIconClones( - cloneOpts: FileIconClone, - basePaths: IconPath[], - hash: string -): IconConfiguration { - const config = createCloneConfig(); - - basePaths.forEach((base) => { - try { - const filePath = getFileIconClonePath(base, cloneOpts, hash); - const iconPathConfig = `${iconFolderPath}clones/${basename( - filePath.path - )}`; - - const baseColor = - base.type === FileIconType.Base - ? cloneOpts.color - : cloneOpts.lightColor ?? cloneOpts.color; - - // generates the new icon content - const content = cloneIcon(base.path, hash, baseColor); - const iconName = basename(filePath.path, '.svg'); - - try { - // create the .svg file for the cloned icon - writeFileSync(filePath.path, content); - } catch (error) { - console.error(error); - return; - } - - // set the icon path for the cloned icon in the configuration - config.iconDefinitions![iconName] = { - iconPath: iconPathConfig, - }; - - // set associations for the cloned icon in the configuration - cloneOpts.fileNames?.forEach((fileName) => { - switch (filePath.type) { - case FileIconType.Base: - config.fileNames![fileName] = iconName; - break; - case FileIconType.Light: - config.light!.fileNames![fileName] = iconName; - break; - } - }); - - cloneOpts.fileExtensions?.forEach((fileExtension) => { - switch (filePath.type) { - case FileIconType.Base: - config.fileExtensions![fileExtension] = iconName; - break; - case FileIconType.Light: - config.light!.fileExtensions![fileExtension] = iconName; - break; - } - }); - } catch (error) { - console.error(error); - } - }); - - return config; -} diff --git a/src/icons/generator/clones/folderClone.ts b/src/icons/generator/clones/folderClone.ts deleted file mode 100644 index 3cdf4e7946..0000000000 --- a/src/icons/generator/clones/folderClone.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { basename } from 'path'; -import { FolderIconClone, IconConfiguration } from '../../../models'; -import { - IconPath, - FolderIconType, - getFolderIconBasePaths, - getFolderIconClonePath, -} from './utils/paths'; -import { iconFolderPath } from '../constants'; -import { cloneIcon, createCloneConfig } from './utils/cloning'; -import { writeFileSync } from 'fs'; - -/** - * Generates a clone of a folder icon. - * @param cloneOpts options and configurations on how to clone the folder icon - * @param config global icon configuration (used to get the base icon) - * @param hash current hash being applied to the icons - * @returns a partial icon configuration for the new folder icon - */ -export function cloneFolderIcon( - cloneOpts: FolderIconClone, - config: IconConfiguration, - hash: string -): IconConfiguration { - // get the paths of the base icons - const basePaths = getFolderIconBasePaths(cloneOpts, config); - if (!basePaths) { - return {}; - } - - return createFolderClones(cloneOpts, basePaths, hash); -} - -/** - * for each base icon, creates its clone, recolorizes it and writes it to the disk - * - * @returns partial icon configuration for the cloned folder icons - */ -function createFolderClones( - cloneOpts: FolderIconClone, - basePaths: IconPath[], - hash: string -): IconConfiguration { - const config = createCloneConfig(); - - basePaths.forEach((base) => { - try { - const clonePath = getFolderIconClonePath(base, cloneOpts, hash); - const clonePathConfig = `${iconFolderPath}clones/${basename( - clonePath.path - )}`; - - const baseColor = isDarkThemeIcon(clonePath) - ? cloneOpts.color - : cloneOpts.lightColor ?? cloneOpts.color; - - // generates the new icon content - const content = cloneIcon(base.path, hash, baseColor); - const iconName = basename(clonePath.path, '.svg'); - - try { - // create the .svg file for the cloned icon - writeFileSync(clonePath.path, content); - } catch (error) { - console.error(error); - return; - } - - // sets the icon path for the cloned icon in the configuration - config.iconDefinitions![iconName] = { - iconPath: clonePathConfig, - }; - - // sets the associated folder names for the cloned icon - cloneOpts.folderNames?.forEach((folderName) => { - switch (clonePath.type) { - case FolderIconType.Base: - config.folderNames![folderName] = iconName; - break; - case FolderIconType.Open: - config.folderNamesExpanded![folderName] = iconName; - break; - case FolderIconType.Light: - config.light!.folderNames![folderName] = iconName; - break; - case FolderIconType.LightOpen: - config.light!.folderNamesExpanded![folderName] = iconName; - break; - } - }); - } catch (error) { - console.error(error); - } - }); - - return config; -} - -function isDarkThemeIcon(path: IconPath): boolean { - return path.type === FolderIconType.Base || path.type === FolderIconType.Open; -} diff --git a/src/icons/generator/clones/utils/clone-data.ts b/src/icons/generator/clones/utils/clone-data.ts new file mode 100644 index 0000000000..c6fdce390c --- /dev/null +++ b/src/icons/generator/clones/utils/clone-data.ts @@ -0,0 +1,251 @@ +import { basename, dirname, join } from 'path'; +import { + CustomClone, + FileIconClone, + FolderIconClone, + IconConfiguration, +} from '../../../../models'; +import { existsSync, mkdirSync, rmSync } from 'fs'; +import { + iconFolderPath, + lightColorFileEnding, + openedFolder, +} from '../../constants'; + +export enum Variant { + Base, + Open, + Light, + LightOpen, +} + +export enum Type { + Folder, + File, +} + +export interface IconData { + type: Type; + path: string; + variant: Variant; +} + +export interface CloneData extends IconData { + name: string; + color: string; + inConfigPath: string; + base: IconData; +} + +/** resolves the path of the icon depending on the caller */ +function resolvePath(path: string): string { + if (basename(__dirname) === 'dist') { + return join(__dirname, String(path)); + } else { + // executed via script + return join(__dirname, '..', '..', '..', String(path)); + } +} + +/** checks if a `CustomClone` configuration is a `FolderIconClone` */ +export const isFolder = (clone: CustomClone): clone is FolderIconClone => { + return clone && (clone as FolderIconClone).folderNames !== undefined; +}; + +/** checks if the icon is a dark variant */ +const isDark = (daa: IconData) => + daa.variant === Variant.Base || daa.variant === Variant.Open; + +/** + * get cloning information from configuration + * @param cloneOpts the clone configuration + * @param config the current configuration of the extension + * @param hash the current hash being applied to the icons + */ +export function getCloneData( + cloneOpts: CustomClone, + config: IconConfiguration, + hash: string +): CloneData[] | undefined { + const baseIcon = isFolder(cloneOpts) + ? getFolderIconBaseData(cloneOpts, config) + : getFileIconBaseData(cloneOpts, config); + + if (baseIcon) { + return baseIcon.map((base) => { + const cloneIcon = isFolder(cloneOpts) + ? getFolderIconCloneData(base, cloneOpts, hash) + : getFileIconCloneData(base, cloneOpts, hash); + + return { + name: getIconName(cloneOpts.name, base), + color: isDark(base) + ? cloneOpts.color + : cloneOpts.lightColor ?? cloneOpts.color, + inConfigPath: `${iconFolderPath}clones/${basename(cloneIcon.path)}`, + base, + ...cloneIcon, + }; + }); + } +} + +/** returns path, type and variant for the base file icons to be cloned */ +function getFileIconBaseData( + cloneOpts: FileIconClone, + config: IconConfiguration +): IconData[] | undefined { + const icons = []; + const base = config.iconDefinitions?.[`${cloneOpts.base}`]?.iconPath; + let light = + config.iconDefinitions?.[`${cloneOpts.base}${lightColorFileEnding}`] + ?.iconPath; + + if (cloneOpts.lightColor && !light) { + // the original icon does not have a light version, so we re-use the base + light = base; + } + + if (base) { + icons.push({ + type: Type.File, + variant: Variant.Base, + path: resolvePath(base), + }); + light && + icons.push({ + type: Type.Folder, + variant: Variant.Light, + path: resolvePath(light), + }); + return icons; + } +} + +/** creates and returns the path of the cloned file icon */ +function getFileIconCloneData( + base: IconData, + cloneOpts: FileIconClone, + hash: string +): IconData { + const name = getIconName(cloneOpts.name, base); + const clonePath = join(dirname(base.path), 'clones', `${name}${hash}.svg`); + + return { + variant: base.variant, + type: base.type, + path: clonePath, + }; +} + +/** returns path, type and variant for the base folder icons to be cloned */ +function getFolderIconBaseData( + cloneOpts: FolderIconClone, + config: IconConfiguration +): IconData[] | undefined { + const icons = []; + const folderBase = + cloneOpts.base === 'folder' ? 'folder' : `folder-${cloneOpts.base}`; + + const base = config.iconDefinitions?.[`${folderBase}`]?.iconPath; + const open = + config.iconDefinitions?.[`${folderBase}${openedFolder}`]?.iconPath; + let light = + config.iconDefinitions?.[`${folderBase}${lightColorFileEnding}`]?.iconPath; + let lightOpen = + config.iconDefinitions?.[ + `${folderBase}${openedFolder}${lightColorFileEnding}` + ]?.iconPath; + + if (base && open) { + base && + icons.push({ + type: Type.Folder, + variant: Variant.Base, + path: resolvePath(base), + }); + open && + icons.push({ + type: Type.Folder, + variant: Variant.Open, + path: resolvePath(open), + }); + + if (cloneOpts.lightColor && (!light || !lightOpen)) { + // the original icon does not have a light version, so we re-use the base icons + light = base; + lightOpen = open; + } + + if (light) { + icons.push({ + type: Type.Folder, + variant: Variant.Light, + path: resolvePath(light), + }); + } + + if (lightOpen) { + icons.push({ + type: Type.Folder, + variant: Variant.LightOpen, + path: resolvePath(lightOpen), + }); + } + + return icons; + } +} + +/** creates and returns the path of the cloned folder icon */ +function getFolderIconCloneData( + base: IconData, + cloneOpts: FolderIconClone, + hash: string +): IconData { + const name = getIconName(cloneOpts.name, base); + const path = join(dirname(base.path), 'clones', `${name}${hash}.svg`); + return { type: base.type, variant: base.variant, path }; +} + +/** + * removes the clones folder if it exists + * and creates a new one if `keep` is true + */ +export function clearCloneFolder(keep: boolean = true): void { + const clonesFolderPath = resolvePath('./../icons/clones'); + + if (existsSync(clonesFolderPath)) { + rmSync(clonesFolderPath, { recursive: true }); + } + + if (keep) { + mkdirSync(clonesFolderPath); + } +} + +function getIconName(baseName: string, data: IconData): string { + let prefix = ''; + let sufix = ''; + + if (data.type === Type.Folder) { + prefix = baseName === 'folder' ? '' : `folder-`; + switch (data.variant) { + case Variant.Base: + break; + case Variant.Open: + sufix = openedFolder; + break; + case Variant.Light: + sufix = lightColorFileEnding; + break; + case Variant.LightOpen: + sufix = `${openedFolder}${lightColorFileEnding}`; + break; + } + } else { + sufix = data.variant === Variant.Light ? lightColorFileEnding : ''; + } + + return `${prefix}${baseName}${sufix}`; +} diff --git a/src/icons/generator/clones/utils/paths.ts b/src/icons/generator/clones/utils/paths.ts deleted file mode 100644 index ba5419b963..0000000000 --- a/src/icons/generator/clones/utils/paths.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { basename, dirname, join } from 'path'; -import { - FileIconClone, - FolderIconClone, - IconConfiguration, -} from '../../../../models'; -import { existsSync, mkdirSync, rmSync } from 'fs'; -import { lightColorFileEnding, openedFolder } from '../../constants'; - -export enum FolderIconType { - Base, - Open, - Light, - LightOpen, -} - -export enum FileIconType { - Base, - Light, -} - -export interface IconPath { - path: string; - type: T; -} - -/** resolves the path of the icon depending of the caller */ -function resolvePath(path: string): string { - if (basename(__dirname) === 'dist') { - return join(__dirname, String(path)); - } else { - // executed via script - return join(__dirname, '..', '..', '..', String(path)); - } -} - -/** returns the paths of the base file icons to be cloned (base, light) */ -export function getFileIconBasePaths( - cloneOpts: FileIconClone, - config: IconConfiguration -): IconPath[] | undefined { - const paths = []; - const base = config.iconDefinitions?.[`${cloneOpts.base}`]?.iconPath; - let light = - config.iconDefinitions?.[`${cloneOpts.base}${lightColorFileEnding}`] - ?.iconPath; - - if (cloneOpts.lightColor && !light) { - // the original icon does not have a light version, so we re-use the base - light = base; - } - - if (base) { - paths.push({ type: FileIconType.Base, path: resolvePath(base) }); - light && paths.push({ type: FileIconType.Light, path: resolvePath(light) }); - return paths; - } -} - -/** creates and returns the path of the cloned file icon */ -export function getFileIconClonePath( - base: IconPath, - cloneOpts: FileIconClone, - hash: string -): IconPath { - const sufix = base.type === FileIconType.Light ? lightColorFileEnding : ''; - - const clonePath = join( - dirname(base.path), - 'clones', - `${cloneOpts.name}${sufix}${hash}.svg` - ); - - return { - type: base.type, - path: clonePath, - }; -} - -/** - * returns the paths of the base folder icons to be cloned - * (base, open, light, light-open) - */ -export function getFolderIconBasePaths( - cloneOpts: FolderIconClone, - config: IconConfiguration -): IconPath[] | undefined { - const paths = []; - const folderBase = - cloneOpts.base === 'folder' ? 'folder' : `folder-${cloneOpts.base}`; - - const base = config.iconDefinitions?.[`${folderBase}`]?.iconPath; - const open = - config.iconDefinitions?.[`${folderBase}${openedFolder}`]?.iconPath; - let light = - config.iconDefinitions?.[`${folderBase}${lightColorFileEnding}`]?.iconPath; - let lightOpen = - config.iconDefinitions?.[ - `${folderBase}${openedFolder}${lightColorFileEnding}` - ]?.iconPath; - - if (base && open) { - base && paths.push({ type: FolderIconType.Base, path: resolvePath(base) }); - open && paths.push({ type: FolderIconType.Open, path: resolvePath(open) }); - - if (cloneOpts.lightColor && (!light || !lightOpen)) { - // the original icon does not have a light version, so we re-use the base icons - light = base; - lightOpen = open; - } - - if (light) { - paths.push({ type: FolderIconType.Light, path: resolvePath(light) }); - } - - if (lightOpen) { - paths.push({ - type: FolderIconType.LightOpen, - path: resolvePath(lightOpen), - }); - } - - return paths; - } -} - -/** creates and returns the path of the cloned folder icon */ -export function getFolderIconClonePath( - base: IconPath, - cloneOpts: FolderIconClone, - hash: string -): IconPath { - let sufix = ''; - - switch (base.type) { - case FolderIconType.Base: - break; - case FolderIconType.Open: - sufix = openedFolder; - break; - case FolderIconType.Light: - sufix = lightColorFileEnding; - break; - case FolderIconType.LightOpen: - sufix = `${openedFolder}${lightColorFileEnding}`; - break; - } - - const clonePath = join( - dirname(base.path), - 'clones', - `folder-${cloneOpts.name}${sufix}${hash}.svg` - ); - - return { - type: base.type, - path: clonePath, - }; -} - -/** - * removes the clones folder if it exists - * and creates a new one if `keep` is true - */ -export function clearCloneFolder(keep: boolean = true): void { - const clonesFolderPath = resolvePath('./../icons/clones'); - - if (existsSync(clonesFolderPath)) { - rmSync(clonesFolderPath, { recursive: true }); - } - - if (keep) { - mkdirSync(clonesFolderPath); - } -} From 400d0cf7b27c06b2aa678c073c01b236446465dc Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Fri, 26 Apr 2024 18:12:14 -0300 Subject: [PATCH 12/25] =?UTF-8?q?feat:=20=E2=9C=A8=20allow=20creating=20cl?= =?UTF-8?q?ones=20at=20build=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit support for creating clones by configuring them in the fileIcons.ts and folderIcons.ts files so that the icons are created at build time allowing contributors to create clones that are shipped with the extension --- .gitignore | 1 + src/icons/generator/clones/clonesGenerator.ts | 68 +++++++++++++++- .../generator/clones/utils/clone-data.ts | 81 +++++++++++-------- src/icons/generator/clones/utils/cloning.ts | 2 +- src/icons/generator/constants.ts | 10 +++ src/icons/generator/fileGenerator.ts | 21 +++-- src/icons/generator/folderGenerator.ts | 23 ++++-- src/icons/generator/jsonGenerator.ts | 22 ++++- src/models/icons/cloneOptions.ts | 5 ++ src/models/icons/files/fileIcon.ts | 6 ++ src/models/icons/folders/folderIcon.ts | 6 ++ 11 files changed, 193 insertions(+), 52 deletions(-) create mode 100644 src/models/icons/cloneOptions.ts diff --git a/.gitignore b/.gitignore index aac90f073c..1a47c83859 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ icons/folder.svg icons/folder-open.svg icons/folder-root.svg icons/folder-root-open.svg +icons/*.clone.svg icons/clones src/scripts/preview/*.html diff --git a/src/icons/generator/clones/clonesGenerator.ts b/src/icons/generator/clones/clonesGenerator.ts index 6e81b8e27f..7f309642c1 100644 --- a/src/icons/generator/clones/clonesGenerator.ts +++ b/src/icons/generator/clones/clonesGenerator.ts @@ -1,6 +1,9 @@ import { + CustomClone, FileIconClone, + FileIcons, FolderIconClone, + FolderTheme, IconConfiguration, IconJsonOptions, } from '../../../models'; @@ -14,10 +17,11 @@ import merge from 'lodash.merge'; import { getFileConfigHash } from '../../../helpers/fileConfig'; import { cloneIcon, createCloneConfig } from './utils/cloning'; import { writeFileSync } from 'fs'; +import { cloneIconExtension, clonesFolder } from '../constants'; /** * Creates custom icons by cloning already existing icons and changing - * their colors, allowing users create their own variations. + * their colors, based on the user's provided configurations. */ export function customClonesIcons( config: IconConfiguration, @@ -43,6 +47,62 @@ export function customClonesIcons( return clonedIconsConfig; } +/** + * Creates custom icons by cloning already existing icons and changing + * their colors, based on the configurations provided by the extension. + * (this is meant to be called at build time) + */ +export function generateConfiguredClones( + iconsList: FolderTheme[] | FileIcons, + config: IconConfiguration +) { + let iconsToClone: CustomClone[] = []; + + if (Array.isArray(iconsList)) { + iconsToClone = iconsList.reduce((acc, theme) => { + const icons = theme.icons?.filter((icon) => icon.clone) ?? []; + return acc.concat( + icons.map((icon) => ({ + folderNames: icon.folderNames, + name: icon.name, + ...icon.clone!, + })) + ); + }, [] as FolderIconClone[]); + } else { + const icons = iconsList.icons?.filter((icon) => icon.clone) ?? []; + iconsToClone = icons.map( + (icon) => + ({ + fileExtensions: icon.fileExtensions, + fileNames: icon.fileNames, + name: icon.name, + ...icon.clone!, + } as FileIconClone) + ); + } + + iconsToClone?.forEach((clone) => { + const clones = getCloneData(clone, config, '', '', cloneIconExtension); + if (!clones) { + return; + } + + clones.forEach((clone) => { + try { + // generates the new icon content (svg) + const content = cloneIcon(clone.base.path, clone.color); + + // write the new .svg file to the disk + writeFileSync(clone.path, content); + } catch (error) { + console.error(error); + return; + } + }); + }); +} + /** Checks if there are any custom clones to be created */ export function hasCustomClones(options: IconJsonOptions): boolean { return ( @@ -58,13 +118,13 @@ export function hasCustomClones(options: IconJsonOptions): boolean { * @param hash current hash being applied to the icons * @returns a partial icon configuration for the new icon */ -export function createIconClone( +function createIconClone( cloneOpts: FolderIconClone | FileIconClone, config: IconConfiguration, hash: string ): IconConfiguration { // get clones to be created - const clones = getCloneData(cloneOpts, config, hash); + const clones = getCloneData(cloneOpts, config, clonesFolder, hash); if (!clones) { return {}; } @@ -74,7 +134,7 @@ export function createIconClone( clones.forEach((clone) => { try { // generates the new icon content (svg) - const content = cloneIcon(clone.base.path, hash, clone.color); + const content = cloneIcon(clone.base.path, clone.color, hash); try { // write the new .svg file to the disk diff --git a/src/icons/generator/clones/utils/clone-data.ts b/src/icons/generator/clones/utils/clone-data.ts index c6fdce390c..9f8dad2606 100644 --- a/src/icons/generator/clones/utils/clone-data.ts +++ b/src/icons/generator/clones/utils/clone-data.ts @@ -43,7 +43,7 @@ function resolvePath(path: string): string { return join(__dirname, String(path)); } else { // executed via script - return join(__dirname, '..', '..', '..', String(path)); + return join(__dirname, '..', '..', '..', '..', String(path)); } } @@ -65,7 +65,9 @@ const isDark = (daa: IconData) => export function getCloneData( cloneOpts: CustomClone, config: IconConfiguration, - hash: string + subFolder: string, + hash: string, + ext?: string ): CloneData[] | undefined { const baseIcon = isFolder(cloneOpts) ? getFolderIconBaseData(cloneOpts, config) @@ -74,15 +76,17 @@ export function getCloneData( if (baseIcon) { return baseIcon.map((base) => { const cloneIcon = isFolder(cloneOpts) - ? getFolderIconCloneData(base, cloneOpts, hash) - : getFileIconCloneData(base, cloneOpts, hash); + ? getFolderIconCloneData(base, cloneOpts, hash, subFolder, ext) + : getFileIconCloneData(base, cloneOpts, hash, subFolder, ext); return { name: getIconName(cloneOpts.name, base), color: isDark(base) ? cloneOpts.color : cloneOpts.lightColor ?? cloneOpts.color, - inConfigPath: `${iconFolderPath}clones/${basename(cloneIcon.path)}`, + inConfigPath: `${iconFolderPath}${subFolder}${basename( + cloneIcon.path + )}`, base, ...cloneIcon, }; @@ -114,7 +118,7 @@ function getFileIconBaseData( }); light && icons.push({ - type: Type.Folder, + type: Type.File, variant: Variant.Light, path: resolvePath(light), }); @@ -126,10 +130,12 @@ function getFileIconBaseData( function getFileIconCloneData( base: IconData, cloneOpts: FileIconClone, - hash: string + hash: string, + subFolder: string, + ext = '.svg' ): IconData { const name = getIconName(cloneOpts.name, base); - const clonePath = join(dirname(base.path), 'clones', `${name}${hash}.svg`); + const clonePath = join(dirname(base.path), subFolder, `${name}${hash}${ext}`); return { variant: base.variant, @@ -140,12 +146,16 @@ function getFileIconCloneData( /** returns path, type and variant for the base folder icons to be cloned */ function getFolderIconBaseData( - cloneOpts: FolderIconClone, + clone: FolderIconClone, config: IconConfiguration ): IconData[] | undefined { const icons = []; const folderBase = - cloneOpts.base === 'folder' ? 'folder' : `folder-${cloneOpts.base}`; + clone.base === 'folder' + ? 'folder' + : clone.base.startsWith('folder-') + ? clone.base + : `folder-${clone.base}`; const base = config.iconDefinitions?.[`${folderBase}`]?.iconPath; const open = @@ -158,20 +168,19 @@ function getFolderIconBaseData( ]?.iconPath; if (base && open) { - base && - icons.push({ - type: Type.Folder, - variant: Variant.Base, - path: resolvePath(base), - }); - open && - icons.push({ - type: Type.Folder, - variant: Variant.Open, - path: resolvePath(open), - }); + icons.push({ + type: Type.Folder, + variant: Variant.Base, + path: resolvePath(base), + }); - if (cloneOpts.lightColor && (!light || !lightOpen)) { + icons.push({ + type: Type.Folder, + variant: Variant.Open, + path: resolvePath(open), + }); + + if (clone.lightColor && (!light || !lightOpen)) { // the original icon does not have a light version, so we re-use the base icons light = base; lightOpen = open; @@ -201,10 +210,12 @@ function getFolderIconBaseData( function getFolderIconCloneData( base: IconData, cloneOpts: FolderIconClone, - hash: string + hash: string, + subFolder: string, + ext = '.svg' ): IconData { const name = getIconName(cloneOpts.name, base); - const path = join(dirname(base.path), 'clones', `${name}${hash}.svg`); + const path = join(dirname(base.path), subFolder, `${name}${hash}${ext}`); return { type: base.type, variant: base.variant, path }; } @@ -226,26 +237,32 @@ export function clearCloneFolder(keep: boolean = true): void { function getIconName(baseName: string, data: IconData): string { let prefix = ''; - let sufix = ''; + let suffix = ''; if (data.type === Type.Folder) { - prefix = baseName === 'folder' ? '' : `folder-`; + prefix = + baseName === 'folder' + ? '' + : baseName.startsWith('folder-') + ? '' + : 'folder-'; + switch (data.variant) { case Variant.Base: break; case Variant.Open: - sufix = openedFolder; + suffix = openedFolder; break; case Variant.Light: - sufix = lightColorFileEnding; + suffix = lightColorFileEnding; break; case Variant.LightOpen: - sufix = `${openedFolder}${lightColorFileEnding}`; + suffix = `${openedFolder}${lightColorFileEnding}`; break; } } else { - sufix = data.variant === Variant.Light ? lightColorFileEnding : ''; + suffix = data.variant === Variant.Light ? lightColorFileEnding : ''; } - return `${prefix}${baseName}${sufix}`; + return `${prefix}${baseName}${suffix}`; } diff --git a/src/icons/generator/clones/utils/cloning.ts b/src/icons/generator/clones/utils/cloning.ts index 6ca355fc32..94589905fe 100644 --- a/src/icons/generator/clones/utils/cloning.ts +++ b/src/icons/generator/clones/utils/cloning.ts @@ -28,7 +28,7 @@ export function readIcon(path: string, hash: string): string { } /** Clones an icon and changes its colors according to the clone options. */ -export function cloneIcon(path: string, hash: string, color: string): string { +export function cloneIcon(path: string, color: string, hash = ''): string { const baseContent = readIcon(path, hash); const svg = parseSync(baseContent); const replacements = replacementMap(color, getColorList(svg)); diff --git a/src/icons/generator/constants.ts b/src/icons/generator/constants.ts index 01b9568697..fbf1cf3f3e 100644 --- a/src/icons/generator/constants.ts +++ b/src/icons/generator/constants.ts @@ -23,6 +23,16 @@ export const lightColorFileEnding: string = '_light'; */ export const highContrastColorFileEnding: string = '_highContrast'; +/** + * Pattern to match the file icon definition. + */ +export const cloneIconExtension: string = '.clone.svg'; + +/** + * User Defined Clones subfolder + */ +export const clonesFolder: string = 'clones/'; + /** * Pattern to match wildcards for custom file icon mappings. */ diff --git a/src/icons/generator/fileGenerator.ts b/src/icons/generator/fileGenerator.ts index 9311170d6e..2d52688424 100644 --- a/src/icons/generator/fileGenerator.ts +++ b/src/icons/generator/fileGenerator.ts @@ -8,6 +8,7 @@ import { IconJsonOptions, } from '../../models/index'; import { + cloneIconExtension, highContrastColorFileEnding, iconFolderPath, lightColorFileEnding, @@ -38,20 +39,26 @@ export const loadFileIconDefinitions = ( allFileIcons.forEach((icon) => { if (icon.disabled) return; - config = merge({}, config, setIconDefinition(config, icon.name)); + const isClone = icon.clone !== undefined; + config = merge({}, config, setIconDefinition(config, icon.name, isClone)); if (icon.light) { config = merge( {}, config, - setIconDefinition(config, icon.name, lightColorFileEnding) + setIconDefinition(config, icon.name, isClone, lightColorFileEnding) ); } if (icon.highContrast) { config = merge( {}, config, - setIconDefinition(config, icon.name, highContrastColorFileEnding) + setIconDefinition( + config, + icon.name, + isClone, + highContrastColorFileEnding + ) ); } @@ -79,7 +86,7 @@ export const loadFileIconDefinitions = ( config = merge( {}, config, - setIconDefinition(config, fileIcons.defaultIcon.name) + setIconDefinition(config, fileIcons.defaultIcon.name, false) ); config.file = fileIcons.defaultIcon.name; @@ -90,6 +97,7 @@ export const loadFileIconDefinitions = ( setIconDefinition( config, fileIcons.defaultIcon.name, + false, lightColorFileEnding ) ); @@ -105,6 +113,7 @@ export const loadFileIconDefinitions = ( setIconDefinition( config, fileIcons.defaultIcon.name, + false, highContrastColorFileEnding ) ); @@ -187,13 +196,15 @@ const disableIconsByPack = ( const setIconDefinition = ( config: IconConfiguration, iconName: string, + isClone: boolean, appendix: string = '' ) => { const obj: Partial = { iconDefinitions: {} }; + const ext = isClone ? cloneIconExtension : '.svg'; if (config.options) { const fileConfigHash = getFileConfigHash(config.options); obj.iconDefinitions![`${iconName}${appendix}`] = { - iconPath: `${iconFolderPath}${iconName}${appendix}${fileConfigHash}.svg`, + iconPath: `${iconFolderPath}${iconName}${appendix}${fileConfigHash}${ext}`, }; } return obj; diff --git a/src/icons/generator/folderGenerator.ts b/src/icons/generator/folderGenerator.ts index c661a31b32..f5efe65efb 100644 --- a/src/icons/generator/folderGenerator.ts +++ b/src/icons/generator/folderGenerator.ts @@ -11,6 +11,7 @@ import { IconJsonOptions, } from '../../models/index'; import { + cloneIconExtension, highContrastColorFileEnding, iconFolderPath, lightColorFileEnding, @@ -173,20 +174,27 @@ const setIconDefinitions = ( config: IconConfiguration, icon: FolderIcon | DefaultIcon ) => { + const isClone = (icon as FolderIcon).clone !== undefined; config = merge({}, config); - config = createIconDefinitions(config, icon.name); + + config = createIconDefinitions(config, icon.name, '', isClone); if (icon.light) { config = merge( {}, config, - createIconDefinitions(config, icon.name, lightColorFileEnding) + createIconDefinitions(config, icon.name, lightColorFileEnding, isClone) ); } if (icon.highContrast) { config = merge( {}, config, - createIconDefinitions(config, icon.name, highContrastColorFileEnding) + createIconDefinitions( + config, + icon.name, + highContrastColorFileEnding, + isClone + ) ); } return config; @@ -195,17 +203,20 @@ const setIconDefinitions = ( const createIconDefinitions = ( config: IconConfiguration, iconName: string, - appendix: string = '' + appendix: string = '', + isClone = false ) => { config = merge({}, config); const fileConfigHash = getFileConfigHash(config.options ?? {}); const configIconDefinitions = config.iconDefinitions; + const ext = isClone ? cloneIconExtension : '.svg'; + if (configIconDefinitions) { configIconDefinitions[iconName + appendix] = { - iconPath: `${iconFolderPath}${iconName}${appendix}${fileConfigHash}.svg`, + iconPath: `${iconFolderPath}${iconName}${appendix}${fileConfigHash}${ext}`, }; configIconDefinitions[`${iconName}${openedFolder}${appendix}`] = { - iconPath: `${iconFolderPath}${iconName}${openedFolder}${appendix}${fileConfigHash}.svg`, + iconPath: `${iconFolderPath}${iconName}${openedFolder}${appendix}${fileConfigHash}${ext}`, }; } return config; diff --git a/src/icons/generator/jsonGenerator.ts b/src/icons/generator/jsonGenerator.ts index 7e7c5aceae..1e8645c476 100644 --- a/src/icons/generator/jsonGenerator.ts +++ b/src/icons/generator/jsonGenerator.ts @@ -26,7 +26,10 @@ import { validateOpacityValue, validateSaturationValue, } from './index'; -import { customClonesIcons } from './clones/clonesGenerator'; +import { + customClonesIcons, + generateConfiguredClones, +} from './clones/clonesGenerator'; /** * Generate the complete icon configuration object that can be written as JSON file. @@ -133,8 +136,16 @@ export const createIconFile = ( } renameIconFiles(iconJsonPath, options); - // generate custom cloned icons after opacity and saturation have - // been set so that those changes are also applied to the clones + // create configured icon clones at build time + if (!updatedConfigs) { + console.log('Generating icon clones...'); + generateConfiguredClones(folderIcons, json); + generateConfiguredClones(fileIcons, json); + } + + // generate custom cloned icons set by the user via vscode options + // after opacity and saturation have been set so that those changes + // are also applied to the user defined clones json = merge({}, json, customClonesIcons(json, options)); } catch (error) { throw new Error('Failed to update icons: ' + error); @@ -198,7 +209,10 @@ const renameIconFiles = (iconJsonPath: string, options: IconJsonOptions) => { // append file config to file name const newFilePath = join( iconPath, - f.replace(/(^[^\.~]+)(.*)\.svg/, `$1${fileConfigHash}.svg`) + f.replace( + /(^[^\.~]+).*?(\.clone\.svg|\.svg)/, + `$1${fileConfigHash}$2` + ) ); // if generated files are already in place, do not overwrite them diff --git a/src/models/icons/cloneOptions.ts b/src/models/icons/cloneOptions.ts new file mode 100644 index 0000000000..7d1d1e737c --- /dev/null +++ b/src/models/icons/cloneOptions.ts @@ -0,0 +1,5 @@ +export interface CloneOptions { + base: string; + color: string; + lightColor?: string; +} diff --git a/src/models/icons/files/fileIcon.ts b/src/models/icons/files/fileIcon.ts index 617fe48ac0..55b6278157 100644 --- a/src/models/icons/files/fileIcon.ts +++ b/src/models/icons/files/fileIcon.ts @@ -1,4 +1,5 @@ import { RequireAtLeastOne } from '../../../helpers/types'; +import { CloneOptions } from '../cloneOptions'; import { IconPack } from '../index'; interface BasicFileIcon { @@ -38,6 +39,11 @@ interface BasicFileIcon { * Defines a pack to which this icon belongs. A pack can be toggled and all icons inside this pack can be enabled or disabled together. */ enabledFor?: IconPack[]; + + /** + * Options for generating an icon based on another icon. + */ + clone?: CloneOptions; } /** diff --git a/src/models/icons/folders/folderIcon.ts b/src/models/icons/folders/folderIcon.ts index 386d3a7f64..bc75a7852d 100644 --- a/src/models/icons/folders/folderIcon.ts +++ b/src/models/icons/folders/folderIcon.ts @@ -1,3 +1,4 @@ +import { CloneOptions } from '../cloneOptions'; import { IconPack } from '../index'; export interface FolderIcon { @@ -31,4 +32,9 @@ export interface FolderIcon { * Defines a pack to which this icon belongs. A pack can be toggled and all icons inside this pack can be enabled or disabled together. */ enabledFor?: IconPack[]; + + /** + * Options for generating an icon based on another icon. + */ + clone?: CloneOptions; } From 32f2e486385fd10b3b268ae654bf5dc4cca75f4e Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Sat, 27 Apr 2024 09:31:06 -0300 Subject: [PATCH 13/25] =?UTF-8?q?test:=20=F0=9F=A7=AA=20test=20clone=20con?= =?UTF-8?q?figuration=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/spec/icons/fileIcons.spec.ts | 64 ++++++++++++++ src/test/spec/icons/folderIcons.spec.ts | 113 ++++++++++++++++++++++++ 2 files changed, 177 insertions(+) diff --git a/src/test/spec/icons/fileIcons.spec.ts b/src/test/spec/icons/fileIcons.spec.ts index ee3503e4d2..acecb07bbc 100644 --- a/src/test/spec/icons/fileIcons.spec.ts +++ b/src/test/spec/icons/fileIcons.spec.ts @@ -247,4 +247,68 @@ describe('file icons', () => { /* eslint-enable camelcase */ deepStrictEqual(iconDefinitions, expectedConfig); }); + + it('should generate cloned file icons config', () => { + const fileIcons: FileIcons = { + defaultIcon: { name: 'file' }, + icons: [ + { + name: 'foo', + fileNames: ['foo.bar'], + }, + { + name: 'foo-clone', + fileNames: ['bar.foo'], + fileExtensions: ['baz'], + light: true, + clone: { + base: 'foo', + color: 'green-500', + lightColor: 'green-100', + }, + }, + ], + }; + + const options = getDefaultIconOptions(); + const iconConfig = merge({}, new IconConfiguration(), { options }); + const iconDefinitions = loadFileIconDefinitions( + fileIcons, + iconConfig, + options + ); + + expectedConfig.iconDefinitions = { + foo: { + iconPath: './../icons/foo.svg', + }, + 'foo-clone': { + iconPath: './../icons/foo-clone.clone.svg', + }, + 'foo-clone_light': { + iconPath: './../icons/foo-clone_light.clone.svg', + }, + file: { + iconPath: './../icons/file.svg', + }, + }; + expectedConfig.light = { + fileExtensions: { + baz: 'foo-clone_light', + }, + fileNames: { + 'bar.foo': 'foo-clone_light', + }, + }; + expectedConfig.fileNames = { + 'foo.bar': 'foo', + 'bar.foo': 'foo-clone', + }; + expectedConfig.fileExtensions = { + baz: 'foo-clone', + }; + expectedConfig.file = 'file'; + + deepStrictEqual(iconDefinitions, expectedConfig); + }); }); diff --git a/src/test/spec/icons/folderIcons.spec.ts b/src/test/spec/icons/folderIcons.spec.ts index 5fa307cf6a..32f7aaacf2 100644 --- a/src/test/spec/icons/folderIcons.spec.ts +++ b/src/test/spec/icons/folderIcons.spec.ts @@ -529,4 +529,117 @@ describe('folder icons', () => { deepStrictEqual(iconDefinitions.hidesExplorerArrows, true); }); + + it('should generate cloned folder icons config', () => { + const folderTheme: FolderTheme[] = [ + { + name: 'specific', + defaultIcon: { name: 'folder' }, + rootFolder: { name: 'folder-root' }, + icons: [ + { name: 'foo', folderNames: ['foo', 'bar'] }, + { + name: 'foo-clone', + folderNames: ['baz', 'qux'], + light: true, + clone: { + base: 'foo', + color: 'green-500', + lightColor: 'green-100', + }, + }, + ], + }, + ]; + + const options = getDefaultIconOptions(); + const iconConfig = merge({}, new IconConfiguration(), { options }); + const iconDefinitions = loadFolderIconDefinitions( + folderTheme, + iconConfig, + options + ); + + expectedConfig.iconDefinitions = { + foo: { iconPath: './../icons/foo.svg' }, + 'foo-open': { iconPath: './../icons/foo-open.svg' }, + 'foo-clone': { iconPath: './../icons/foo-clone.clone.svg' }, + 'foo-clone-open': { iconPath: './../icons/foo-clone-open.clone.svg' }, + 'foo-clone_light': { iconPath: './../icons/foo-clone_light.clone.svg' }, + 'foo-clone-open_light': { + iconPath: './../icons/foo-clone-open_light.clone.svg', + }, + 'folder-open': { iconPath: './../icons/folder-open.svg' }, + 'folder-root': { iconPath: './../icons/folder-root.svg' }, + 'folder-root-open': { iconPath: './../icons/folder-root-open.svg' }, + folder: { iconPath: './../icons/folder.svg' }, + }; + expectedConfig.folder = 'folder'; + expectedConfig.folderExpanded = 'folder-open'; + expectedConfig.rootFolder = 'folder-root'; + expectedConfig.rootFolderExpanded = 'folder-root-open'; + expectedConfig.folderNames = { + foo: 'foo', + '.foo': 'foo', + _foo: 'foo', + __foo__: 'foo', + bar: 'foo', + '.bar': 'foo', + _bar: 'foo', + __bar__: 'foo', + baz: 'foo-clone', + '.baz': 'foo-clone', + _baz: 'foo-clone', + __baz__: 'foo-clone', + qux: 'foo-clone', + '.qux': 'foo-clone', + _qux: 'foo-clone', + __qux__: 'foo-clone', + }; + expectedConfig.folderNamesExpanded = { + foo: 'foo-open', + '.foo': 'foo-open', + _foo: 'foo-open', + __foo__: 'foo-open', + bar: 'foo-open', + '.bar': 'foo-open', + _bar: 'foo-open', + __bar__: 'foo-open', + baz: 'foo-clone-open', + '.baz': 'foo-clone-open', + _baz: 'foo-clone-open', + __baz__: 'foo-clone-open', + qux: 'foo-clone-open', + '.qux': 'foo-clone-open', + _qux: 'foo-clone-open', + __qux__: 'foo-clone-open', + }; + expectedConfig.light = { + fileExtensions: {}, + fileNames: {}, + folderNames: { + baz: 'foo-clone_light', + '.baz': 'foo-clone_light', + _baz: 'foo-clone_light', + __baz__: 'foo-clone_light', + qux: 'foo-clone_light', + '.qux': 'foo-clone_light', + _qux: 'foo-clone_light', + __qux__: 'foo-clone_light', + }, + folderNamesExpanded: { + baz: 'foo-clone-open_light', + '.baz': 'foo-clone-open_light', + _baz: 'foo-clone-open_light', + __baz__: 'foo-clone-open_light', + qux: 'foo-clone-open_light', + '.qux': 'foo-clone-open_light', + _qux: 'foo-clone-open_light', + __qux__: 'foo-clone-open_light', + }, + }; + expectedConfig.hidesExplorerArrows = false; + + deepStrictEqual(iconDefinitions, expectedConfig); + }); }); From 8eebc00862c1a8ed419461d14cd51229cffca2f4 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Sun, 28 Apr 2024 08:53:00 -0300 Subject: [PATCH 14/25] =?UTF-8?q?chore:=20=F0=9F=A7=B9=20fix=20file=20name?= =?UTF-8?q?s=20to=20camelCase=20to=20match=20project=20style?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/icons/generator/clones/clonesGenerator.ts | 2 +- .../generator/clones/utils/{clone-data.ts => cloneData.ts} | 0 src/icons/generator/clones/utils/color/colors.ts | 2 +- .../utils/color/{material-palette.ts => materialPalette.ts} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename src/icons/generator/clones/utils/{clone-data.ts => cloneData.ts} (100%) rename src/icons/generator/clones/utils/color/{material-palette.ts => materialPalette.ts} (100%) diff --git a/src/icons/generator/clones/clonesGenerator.ts b/src/icons/generator/clones/clonesGenerator.ts index 7f309642c1..87ce9bfbfd 100644 --- a/src/icons/generator/clones/clonesGenerator.ts +++ b/src/icons/generator/clones/clonesGenerator.ts @@ -12,7 +12,7 @@ import { clearCloneFolder, getCloneData, isFolder, -} from './utils/clone-data'; +} from './utils/cloneData'; import merge from 'lodash.merge'; import { getFileConfigHash } from '../../../helpers/fileConfig'; import { cloneIcon, createCloneConfig } from './utils/cloning'; diff --git a/src/icons/generator/clones/utils/clone-data.ts b/src/icons/generator/clones/utils/cloneData.ts similarity index 100% rename from src/icons/generator/clones/utils/clone-data.ts rename to src/icons/generator/clones/utils/cloneData.ts diff --git a/src/icons/generator/clones/utils/color/colors.ts b/src/icons/generator/clones/utils/color/colors.ts index 1e450a54a8..c3687189cd 100644 --- a/src/icons/generator/clones/utils/color/colors.ts +++ b/src/icons/generator/clones/utils/color/colors.ts @@ -4,7 +4,7 @@ import chroma, { Color, valid } from 'chroma-js'; import { closerMaterialColorTo, getMaterialColorByKey, -} from './material-palette'; +} from './materialPalette'; /** Get all the colors used in the SVG node as a `Set` list. **/ export function getColorList(node: INode) { diff --git a/src/icons/generator/clones/utils/color/material-palette.ts b/src/icons/generator/clones/utils/color/materialPalette.ts similarity index 100% rename from src/icons/generator/clones/utils/color/material-palette.ts rename to src/icons/generator/clones/utils/color/materialPalette.ts From efa6ea19a171ceb6386dfa329ccc60035fdb59a2 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Sun, 28 Apr 2024 11:32:02 -0300 Subject: [PATCH 15/25] =?UTF-8?q?test:=20=F0=9F=A7=AA=20test=20clone=20dat?= =?UTF-8?q?a=20config=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/icons/generator/clones/utils/cloneData.ts | 2 +- .../generator/clones/utils/color/colors.ts | 2 +- src/test/spec/icons/cloning.spec.ts | 405 ++++++++++++++++++ 3 files changed, 407 insertions(+), 2 deletions(-) create mode 100644 src/test/spec/icons/cloning.spec.ts diff --git a/src/icons/generator/clones/utils/cloneData.ts b/src/icons/generator/clones/utils/cloneData.ts index 9f8dad2606..8143bfc703 100644 --- a/src/icons/generator/clones/utils/cloneData.ts +++ b/src/icons/generator/clones/utils/cloneData.ts @@ -38,7 +38,7 @@ export interface CloneData extends IconData { } /** resolves the path of the icon depending on the caller */ -function resolvePath(path: string): string { +export function resolvePath(path: string): string { if (basename(__dirname) === 'dist') { return join(__dirname, String(path)); } else { diff --git a/src/icons/generator/clones/utils/color/colors.ts b/src/icons/generator/clones/utils/color/colors.ts index c3687189cd..15d4abd2a1 100644 --- a/src/icons/generator/clones/utils/color/colors.ts +++ b/src/icons/generator/clones/utils/color/colors.ts @@ -46,7 +46,7 @@ export function getColorList(node: INode) { } /** given a set of colors, orders them from dark to light. **/ -function orderDarkToLight(colors: Set) { +export function orderDarkToLight(colors: Set) { const colorArray = Array.from(colors); return colorArray.sort((a, b) => { // sort by lightness diff --git a/src/test/spec/icons/cloning.spec.ts b/src/test/spec/icons/cloning.spec.ts new file mode 100644 index 0000000000..cb0fe9b56b --- /dev/null +++ b/src/test/spec/icons/cloning.spec.ts @@ -0,0 +1,405 @@ +import { + lightColorFileEnding, + openedFolder, + iconFolderPath, +} from '../../../icons/generator/constants'; +import { + getCloneData, + resolvePath, + Type, + Variant, +} from '../../../icons/generator/clones/utils/cloneData'; +import { IconConfiguration } from '../../../models'; +import { + FileIconClone, + FolderIconClone, +} from '../../../models/icons/iconJsonOptions'; +import { deepStrictEqual } from 'assert'; + +describe('cloning: icon cloning', () => { + describe('#getCloneData(..)', () => { + const subFolder = 'sub/'; + const hash = '~-fakehash123456789'; + const ext = '.ext'; + let config: Partial; + + before(() => { + config = { + iconDefinitions: { + base: { + iconPath: 'icons/icon.svg', + }, + base2: { + iconPath: 'icons/icon2.svg', + }, + // eslint-disable-next-line camelcase + base2_light: { + iconPath: 'icons/icon2_light.svg', + }, + 'folder-base': { + iconPath: 'icons/folder-base.svg', + }, + 'folder-base-open': { + iconPath: 'icons/folder-base_open.svg', + }, + 'folder-base2': { + iconPath: 'icons/folder-base2.svg', + }, + 'folder-base2-open': { + iconPath: 'icons/folder-base2_open.svg', + }, + 'folder-base2_light': { + iconPath: 'icons/folder-base2_light.svg', + }, + 'folder-base2-open_light': { + iconPath: 'icons/folder-base2_open_light.svg', + }, + }, + }; + }); + + describe('clone data generation for file icons', () => { + it('should create a single clone object if not light version exists or asked', () => { + const cloneOpts: FileIconClone = { + name: 'foo', + base: 'base', + color: 'green-500', + fileExtensions: ['bar'], + fileNames: ['file.xyz'], + }; + + const result = getCloneData(cloneOpts, config, subFolder, hash, ext); + + const expected = [ + { + base: { + path: resolvePath(config.iconDefinitions!.base.iconPath), + type: Type.File, + variant: Variant.Base, + }, + color: 'green-500', + inConfigPath: `${iconFolderPath}${subFolder}foo${hash}${ext}`, + name: 'foo', + path: resolvePath(`./icons/${subFolder}foo${hash}${ext}`), + type: Type.File, + variant: Variant.Base, + }, + ]; + + deepStrictEqual(result, expected); + }); + + it('should create two clone objects if light version exists', () => { + const cloneOpts: FileIconClone = { + name: 'foo', + base: 'base2', + color: 'green-500', + fileExtensions: ['bar'], + fileNames: ['file.xyz'], + }; + + const result = getCloneData(cloneOpts, config, subFolder, hash, ext); + + const expected = [ + { + base: { + path: resolvePath(config.iconDefinitions!.base2.iconPath), + type: Type.File, + variant: Variant.Base, + }, + color: 'green-500', + inConfigPath: `${iconFolderPath}${subFolder}foo${hash}${ext}`, + name: 'foo', + path: resolvePath(`./icons/${subFolder}foo${hash}${ext}`), + type: Type.File, + variant: Variant.Base, + }, + { + base: { + path: resolvePath(config.iconDefinitions!.base2_light!.iconPath), + type: Type.File, + variant: Variant.Light, + }, + color: 'green-500', + inConfigPath: `${iconFolderPath}${subFolder}foo${lightColorFileEnding}${hash}${ext}`, + name: `foo${lightColorFileEnding}`, + path: resolvePath( + `./icons/${subFolder}foo${lightColorFileEnding}${hash}${ext}` + ), + type: Type.File, + variant: Variant.Light, + }, + ]; + + deepStrictEqual(result, expected); + }); + + it("should create two clone objects if light version is asked and base light doesn't exist", () => { + const cloneOpts: FileIconClone = { + name: 'foo', + base: 'base', + color: 'green-500', + lightColor: 'green-800', + fileExtensions: ['bar'], + fileNames: ['file.xyz'], + }; + + const result = getCloneData(cloneOpts, config, subFolder, hash, ext); + + const expected = [ + { + base: { + path: resolvePath(config.iconDefinitions!.base.iconPath), + type: Type.File, + variant: Variant.Base, + }, + color: 'green-500', + inConfigPath: `${iconFolderPath}${subFolder}foo${hash}${ext}`, + name: 'foo', + path: resolvePath(`./icons/${subFolder}foo${hash}${ext}`), + type: Type.File, + variant: Variant.Base, + }, + { + base: { + // since light version of icon base doesn't exist, the base icon is used as a base + // to clone the light version + path: resolvePath(config.iconDefinitions!.base.iconPath), + type: Type.File, + variant: Variant.Light, + }, + color: 'green-800', + inConfigPath: `${iconFolderPath}${subFolder}foo${lightColorFileEnding}${hash}${ext}`, + name: `foo${lightColorFileEnding}`, + path: resolvePath( + `./icons/${subFolder}foo${lightColorFileEnding}${hash}${ext}` + ), + type: Type.File, + variant: Variant.Light, + }, + ]; + + deepStrictEqual(result, expected); + }); + }); + + describe('clone data generation for folder icons', () => { + it('should create a single clone object if not light version exists or asked', () => { + const cloneOpts: FolderIconClone = { + name: 'foo', + base: 'base', + color: 'green-500', + folderNames: ['bar'], + }; + + const result = getCloneData(cloneOpts, config, subFolder, hash, ext); + + const expected = [ + { + base: { + path: resolvePath( + config.iconDefinitions!['folder-base'].iconPath + ), + type: Type.Folder, + variant: Variant.Base, + }, + color: 'green-500', + inConfigPath: `${iconFolderPath}${subFolder}folder-foo${hash}${ext}`, + name: 'folder-foo', + path: resolvePath(`./icons/${subFolder}folder-foo${hash}${ext}`), + type: Type.Folder, + variant: Variant.Base, + }, + { + base: { + path: resolvePath( + config.iconDefinitions!['folder-base-open'].iconPath + ), + type: Type.Folder, + variant: Variant.Open, + }, + color: 'green-500', + inConfigPath: `${iconFolderPath}${subFolder}folder-foo${openedFolder}${hash}${ext}`, + name: `folder-foo${openedFolder}`, + path: resolvePath( + `./icons/${subFolder}folder-foo${openedFolder}${hash}${ext}` + ), + type: Type.Folder, + variant: Variant.Open, + }, + ]; + + deepStrictEqual(result, expected); + }); + + it('should create two clone objects if light version exists', () => { + const cloneOpts: FolderIconClone = { + name: 'foo', + base: 'folder-base2', + color: 'green-500', + folderNames: ['bar'], + }; + + const result = getCloneData(cloneOpts, config, subFolder, hash, ext); + + const expected = [ + { + base: { + path: resolvePath( + config.iconDefinitions!['folder-base2'].iconPath + ), + type: Type.Folder, + variant: Variant.Base, + }, + color: 'green-500', + inConfigPath: `${iconFolderPath}${subFolder}folder-foo${hash}${ext}`, + name: 'folder-foo', + path: resolvePath(`./icons/${subFolder}folder-foo${hash}${ext}`), + type: Type.Folder, + variant: Variant.Base, + }, + { + base: { + path: resolvePath( + config.iconDefinitions!['folder-base2-open'].iconPath + ), + type: Type.Folder, + variant: Variant.Open, + }, + color: 'green-500', + inConfigPath: `${iconFolderPath}${subFolder}folder-foo${openedFolder}${hash}${ext}`, + name: `folder-foo${openedFolder}`, + path: resolvePath( + `./icons/${subFolder}folder-foo${openedFolder}${hash}${ext}` + ), + type: Type.Folder, + variant: Variant.Open, + }, + { + base: { + path: resolvePath( + config.iconDefinitions!['folder-base2_light']!.iconPath + ), + type: Type.Folder, + variant: Variant.Light, + }, + color: 'green-500', + inConfigPath: `${iconFolderPath}${subFolder}folder-foo${lightColorFileEnding}${hash}${ext}`, + name: `folder-foo${lightColorFileEnding}`, + path: resolvePath( + `./icons/${subFolder}folder-foo${lightColorFileEnding}${hash}${ext}` + ), + type: Type.Folder, + variant: Variant.Light, + }, + { + base: { + path: resolvePath( + config.iconDefinitions!['folder-base2-open_light']!.iconPath + ), + type: Type.Folder, + variant: Variant.LightOpen, + }, + color: 'green-500', + inConfigPath: `${iconFolderPath}${subFolder}folder-foo${openedFolder}${lightColorFileEnding}${hash}${ext}`, + name: `folder-foo${openedFolder}${lightColorFileEnding}`, + path: resolvePath( + `./icons/${subFolder}folder-foo${openedFolder}${lightColorFileEnding}${hash}${ext}` + ), + type: Type.Folder, + variant: Variant.LightOpen, + }, + ]; + + deepStrictEqual(result, expected); + }); + + it("should create two clone objects if light version is asked and base light doesn't exist", () => { + const cloneOpts: FolderIconClone = { + name: 'foo', + base: 'folder-base', + color: 'green-500', + lightColor: 'green-800', + folderNames: ['bar'], + }; + + const result = getCloneData(cloneOpts, config, subFolder, hash, ext); + + const expected = [ + { + base: { + path: resolvePath( + config.iconDefinitions!['folder-base'].iconPath + ), + type: Type.Folder, + variant: Variant.Base, + }, + color: 'green-500', + inConfigPath: `${iconFolderPath}${subFolder}folder-foo${hash}${ext}`, + name: 'folder-foo', + path: resolvePath(`./icons/${subFolder}folder-foo${hash}${ext}`), + type: Type.Folder, + variant: Variant.Base, + }, + { + base: { + path: resolvePath( + config.iconDefinitions!['folder-base-open'].iconPath + ), + type: Type.Folder, + variant: Variant.Open, + }, + color: 'green-500', + inConfigPath: `${iconFolderPath}${subFolder}folder-foo${openedFolder}${hash}${ext}`, + name: `folder-foo${openedFolder}`, + path: resolvePath( + `./icons/${subFolder}folder-foo${openedFolder}${hash}${ext}` + ), + type: Type.Folder, + variant: Variant.Open, + }, + { + base: { + // since light version of icon base doesn't exist, the base icon is used as a base + // to clone the light version + path: resolvePath( + config.iconDefinitions!['folder-base'].iconPath + ), + type: Type.Folder, + variant: Variant.Light, + }, + color: 'green-800', + inConfigPath: `${iconFolderPath}${subFolder}folder-foo${lightColorFileEnding}${hash}${ext}`, + name: `folder-foo${lightColorFileEnding}`, + path: resolvePath( + `./icons/${subFolder}folder-foo${lightColorFileEnding}${hash}${ext}` + ), + type: Type.Folder, + variant: Variant.Light, + }, + { + base: { + // since light version of icon base doesn't exist, the base icon is used as a base + // to clone the light version + path: resolvePath( + config.iconDefinitions!['folder-base-open'].iconPath + ), + type: Type.Folder, + variant: Variant.LightOpen, + }, + color: 'green-800', + inConfigPath: `${iconFolderPath}${subFolder}folder-foo${openedFolder}${lightColorFileEnding}${hash}${ext}`, + name: `folder-foo${openedFolder}${lightColorFileEnding}`, + path: resolvePath( + `./icons/${subFolder}folder-foo${openedFolder}${lightColorFileEnding}${hash}${ext}` + ), + type: Type.Folder, + variant: Variant.LightOpen, + }, + ]; + + deepStrictEqual(result, expected); + }); + }); + }); +}); From f8329a0f725be6eda6f6296473f87f5fcf86872e Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Sun, 28 Apr 2024 11:42:15 -0300 Subject: [PATCH 16/25] =?UTF-8?q?test:=20=F0=9F=A7=AA=20color=20manipulati?= =?UTF-8?q?on=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/spec/icons/cloning.spec.ts | 50 ++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/test/spec/icons/cloning.spec.ts b/src/test/spec/icons/cloning.spec.ts index cb0fe9b56b..975d71bb4a 100644 --- a/src/test/spec/icons/cloning.spec.ts +++ b/src/test/spec/icons/cloning.spec.ts @@ -14,7 +14,55 @@ import { FileIconClone, FolderIconClone, } from '../../../models/icons/iconJsonOptions'; -import { deepStrictEqual } from 'assert'; +import { deepStrictEqual, throws } from 'assert'; +import { orderDarkToLight } from '../../../icons/generator/clones/utils/color/colors'; +import { + closerMaterialColorTo, + materialPalette, +} from '../../../icons/generator/clones/utils/color/materialPalette'; + +describe('cloning: color manipulation', () => { + describe('#orderDarkToLight(..)', () => { + it('should order colors from dark to light', () => { + const colors = new Set(['#000', '#fff', '#f00', '#0f0', '#00f']); + const result = orderDarkToLight(colors); + deepStrictEqual(result, ['#000', '#f00', '#0f0', '#00f', '#fff']); + }); + + it('if empty set, should return empty array', () => { + const colors = new Set(); + const result = orderDarkToLight(colors); + deepStrictEqual(result, []); + }); + + it('if one color, should return array with that color', () => { + const colors = new Set(['#000']); + const result = orderDarkToLight(colors); + deepStrictEqual(result, ['#000']); + }); + }); + + describe('#closerMaterialColorTo(..)', () => { + it('should return the closest material color to the given color', () => { + const color = '#e24542'; + const result = closerMaterialColorTo(color); + deepStrictEqual(result, materialPalette['red-600']); + }); + + it('should return the same color if it is already a material color', () => { + const color = materialPalette['indigo-500']; + const result = closerMaterialColorTo(color); + deepStrictEqual(result, color); + }); + + it('should throw an error if the given color is not valid', () => { + const color = 'bad-color'; + throws(() => closerMaterialColorTo(color), { + message: 'The given color "bad-color" is not valid!', + }); + }); + }); +}); describe('cloning: icon cloning', () => { describe('#getCloneData(..)', () => { From 796d096eac8d6c1d77bf27315c221ea671b437d0 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Sun, 28 Apr 2024 13:04:37 -0300 Subject: [PATCH 17/25] =?UTF-8?q?test:=20=F0=9F=A7=AA=20test=20svg=20cloni?= =?UTF-8?q?ng=20and=20recolor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 248 ++++++++++++++++++++ package.json | 2 + src/icons/generator/clones/utils/cloning.ts | 10 +- src/test/spec/icons/cloning.spec.ts | 186 ++++++++++++++- src/test/spec/icons/data/icons.ts | 56 +++++ 5 files changed, 494 insertions(+), 8 deletions(-) create mode 100644 src/test/spec/icons/data/icons.ts diff --git a/package-lock.json b/package-lock.json index 7da6726389..2969b42722 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@types/mocha": "^9.1.1", "@types/node": "^17.0.35", "@types/puppeteer": "^5.4.6", + "@types/sinon": "^17.0.3", "@types/vscode": "~1.51.0", "@typescript-eslint/eslint-plugin": "^5.26.0", "@typescript-eslint/parser": "^5.26.0", @@ -33,6 +34,7 @@ "prettier": "^2.6.2", "puppeteer": "^14.1.1", "rimraf": "^3.0.2", + "sinon": "^17.0.1", "svg-color-linter": "^1.3.0", "svgo": "^2.8.0", "ts-loader": "^9.3.0", @@ -211,6 +213,50 @@ "node": ">= 8" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -343,6 +389,21 @@ "@types/node": "*" } }, + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, "node_modules/@types/vscode": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.51.0.tgz", @@ -2638,6 +2699,12 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -2693,6 +2760,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2965,6 +3038,19 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -3155,6 +3241,12 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -3666,6 +3758,33 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, + "node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -4069,6 +4188,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -4604,6 +4732,52 @@ "fastq": "^1.6.0" } }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "requires": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + }, + "dependencies": { + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -4730,6 +4904,21 @@ "@types/node": "*" } }, + "@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, "@types/vscode": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.51.0.tgz", @@ -6421,6 +6610,12 @@ } } }, + "just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -6461,6 +6656,12 @@ "p-locate": "^5.0.0" } }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6677,6 +6878,19 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -6811,6 +7025,12 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -7163,6 +7383,28 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, + "sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "dependencies": { + "diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true + } + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -7458,6 +7700,12 @@ "prelude-ls": "^1.2.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", diff --git a/package.json b/package.json index 192d11d399..973fb2086f 100644 --- a/package.json +++ b/package.json @@ -325,6 +325,7 @@ "@types/mocha": "^9.1.1", "@types/node": "^17.0.35", "@types/puppeteer": "^5.4.6", + "@types/sinon": "^17.0.3", "@types/vscode": "~1.51.0", "@typescript-eslint/eslint-plugin": "^5.26.0", "@typescript-eslint/parser": "^5.26.0", @@ -339,6 +340,7 @@ "prettier": "^2.6.2", "puppeteer": "^14.1.1", "rimraf": "^3.0.2", + "sinon": "^17.0.1", "svg-color-linter": "^1.3.0", "svgo": "^2.8.0", "ts-loader": "^9.3.0", diff --git a/src/icons/generator/clones/utils/cloning.ts b/src/icons/generator/clones/utils/cloning.ts index 94589905fe..7af149453e 100644 --- a/src/icons/generator/clones/utils/cloning.ts +++ b/src/icons/generator/clones/utils/cloning.ts @@ -7,12 +7,16 @@ import { getColorList, replacementMap } from './color/colors'; * Recursively walks through an SVG node tree and its children, * calling a callback on each node. */ -export function traverse(node: INode, callback: (node: INode) => void) { - if (node.attributes['mit-no-recolor'] !== 'true') { +export function traverse( + node: INode, + callback: (node: INode) => void, + filter = true +) { + if (node.attributes['mit-no-recolor'] !== 'true' || !filter) { callback(node); if (node.children) { - node.children.forEach((child) => traverse(child, callback)); + node.children.forEach((child) => traverse(child, callback, filter)); } } } diff --git a/src/test/spec/icons/cloning.spec.ts b/src/test/spec/icons/cloning.spec.ts index 975d71bb4a..090e428e5d 100644 --- a/src/test/spec/icons/cloning.spec.ts +++ b/src/test/spec/icons/cloning.spec.ts @@ -14,12 +14,24 @@ import { FileIconClone, FolderIconClone, } from '../../../models/icons/iconJsonOptions'; -import { deepStrictEqual, throws } from 'assert'; -import { orderDarkToLight } from '../../../icons/generator/clones/utils/color/colors'; +import assert, { deepStrictEqual, throws, equal } from 'assert'; +import { stub } from 'sinon'; +import fs from 'fs'; +import { + cloneIcon, + getStyle, + traverse, +} from '../../../icons/generator/clones/utils/cloning'; +import { + isValidColor, + orderDarkToLight, +} from '../../../icons/generator/clones/utils/color/colors'; import { closerMaterialColorTo, - materialPalette, + materialPalette as palette, } from '../../../icons/generator/clones/utils/color/materialPalette'; +import * as icon from './data/icons'; +import { INode, parseSync } from 'svgson'; describe('cloning: color manipulation', () => { describe('#orderDarkToLight(..)', () => { @@ -46,11 +58,11 @@ describe('cloning: color manipulation', () => { it('should return the closest material color to the given color', () => { const color = '#e24542'; const result = closerMaterialColorTo(color); - deepStrictEqual(result, materialPalette['red-600']); + deepStrictEqual(result, palette['red-600']); }); it('should return the same color if it is already a material color', () => { - const color = materialPalette['indigo-500']; + const color = palette['indigo-500']; const result = closerMaterialColorTo(color); deepStrictEqual(result, color); }); @@ -450,4 +462,168 @@ describe('cloning: icon cloning', () => { }); }); }); + + describe('#cloneIcon(..)', () => { + const bluePalette = [ + palette['blue-50'], + palette['blue-100'], + palette['blue-200'], + palette['blue-300'], + palette['blue-400'], + palette['blue-500'], + palette['blue-600'], + palette['blue-700'], + palette['blue-800'], + palette['blue-900'], + palette['blue-A100'], + palette['blue-A200'], + palette['blue-A400'], + palette['blue-A700'], + ]; + + afterEach( + () => + // restore the fs.readFileSync method to its original state + (fs.readFileSync as any).restore && (fs.readFileSync as any).restore() + ); + + it('should replace the color with the given color', () => { + // stub the fs.readFileSync method to return the desired icon file + stub(fs, 'readFileSync').returns(icon.file); + const result = cloneIcon('fake/path/to/icon.svg', 'blue-600', ''); + + assert((fs.readFileSync as any).called); + + const colorCount = forEachColor(parseSync(result), (color, loc) => { + equal(color, palette['blue-600']); + equal(loc, 'style:fill'); + }); + + equal(colorCount, 1); + }); + + it('should replace the color with the given color if color is in fill attribute', () => { + // stub the fs.readFileSync method to return the desired icon file + stub(fs, 'readFileSync').returns(icon.fileFill); + const result = cloneIcon('fake/path/to/icon.svg', 'blue-600', ''); + + assert((fs.readFileSync as any).called); + + const colorCount = forEachColor(parseSync(result), (color, loc) => { + equal(color, palette['blue-600']); + equal(loc, 'attr:fill'); + }); + + equal(colorCount, 1); + }); + + it('should replace the color with the given color if color is in stop-color attribute', () => { + stub(fs, 'readFileSync').returns(icon.gradient); + const result = cloneIcon('fake/path/to/icon.svg', 'blue-600', ''); + + assert((fs.readFileSync as any).called); + + const colorCount = forEachColor(parseSync(result), (color, loc) => { + assert(bluePalette.includes(color)); + equal(loc, 'attr:stop-color'); + }); + + equal(colorCount, 3); + }); + + it('should replace colors on icons with multiple nodes', () => { + stub(fs, 'readFileSync').returns(icon.folder); + const result = cloneIcon('fake/path/to/icon.svg', 'blue-600', ''); + + assert((fs.readFileSync as any).called); + + const colors: string[] = []; + const colorCount = forEachColor(parseSync(result), (color, loc) => { + colors.push(color); + assert(bluePalette.includes(color)); + equal(loc, 'style:fill'); + }); + + // check that one of the colors is actually blue-600 + assert(colors.includes(palette['blue-600'])); + + equal(colorCount, 2); + }); + + describe('`mit-no-recolor` attribute', () => { + afterEach( + () => + // restore the fs.readFileSync method to its original state + (fs.readFileSync as any).restore && (fs.readFileSync as any).restore() + ); + + it('should not replace the color if the node has the `mit-no-recolor` attribute', () => { + stub(fs, 'readFileSync').returns(icon.folderIgnores); + const result = cloneIcon('fake/path/to/icon.svg', 'blue-600', ''); + + assert((fs.readFileSync as any).called); + + const parsed = parseSync(result); + const changedNodeStyle = getStyle(parsed.children[0]); + const unchangedNodeStyle = getStyle(parsed.children[1]); + + equal(changedNodeStyle.fill, palette['blue-600']); + equal(unchangedNodeStyle.fill, 'red'); + }); + + it('should not replace the color of any child of a node with the `mit-no-recolor` attribute', () => { + stub(fs, 'readFileSync').returns(icon.gradientIgnore); + const result = cloneIcon('fake/path/to/icon.svg', 'blue-600', ''); + + assert((fs.readFileSync as any).called); + + const colorCount = forEachColor(parseSync(result), (color, loc) => { + assert(['#00695c', '#26a69a', '#b2dfdb'].includes(color)); + assert(!bluePalette.includes(color)); + equal(loc, 'attr:stop-color'); + }); + + equal(colorCount, 3); + }); + }); + }); }); + +/** helper function to traverse the svg tree and notify the colors found */ +function forEachColor( + node: INode, + callback: (color: string, loc?: string) => void +) { + let colorCount = 0; + + const notify = (color: string, loc: string) => { + colorCount++; + callback(color, loc); + }; + + traverse( + node, + (node) => { + // check colors in style attribute + const style = getStyle(node); + style?.fill && + isValidColor(style.fill) && + notify(style.fill, 'style:fill'); + style?.stroke && + isValidColor(style.stroke) && + notify(style.stroke, 'style:stroke'); + node.attributes?.fill && + isValidColor(node.attributes.fill) && + notify(node.attributes.fill, 'attr:fill'); + node.attributes?.stroke && + isValidColor(node.attributes.stroke) && + notify(node.attributes.stroke, 'attr:stroke'); + node.attributes?.['stop-color'] && + isValidColor(node.attributes['stop-color']) && + notify(node.attributes['stop-color'], 'attr:stop-color'); + }, + false // no filtering + ); + + return colorCount; +} diff --git a/src/test/spec/icons/data/icons.ts b/src/test/spec/icons/data/icons.ts new file mode 100644 index 0000000000..e4beb85ca5 --- /dev/null +++ b/src/test/spec/icons/data/icons.ts @@ -0,0 +1,56 @@ +/** a file icon with just one node */ +export const file = ` + + + +`; + +export const fileFill = ` + + + +`; + +/** an icon with a gradient */ +export const gradient = ` + + + + + + + + + + +`; + +/** a folder icon with many nodes */ +export const folder = ` + + + + +`; + +/** a folder icon asking for one node to not be recolorized */ +export const folderIgnores = ` + + + + +`; + +/** an icon with a gradient that asks for the gradient node to not be recolorized */ +export const gradientIgnore = ` + + + + + + + + + + +`; From dc5309488dfea1d4de931c5b69b589b617ddc69f Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Sun, 28 Apr 2024 14:44:31 -0300 Subject: [PATCH 18/25] =?UTF-8?q?test:=20=F0=9F=A7=AA=20json=20config=20ge?= =?UTF-8?q?neration=20from=20user=20options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/spec/icons/cloning.spec.ts | 101 ++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/src/test/spec/icons/cloning.spec.ts b/src/test/spec/icons/cloning.spec.ts index 090e428e5d..5dbfb20841 100644 --- a/src/test/spec/icons/cloning.spec.ts +++ b/src/test/spec/icons/cloning.spec.ts @@ -2,6 +2,7 @@ import { lightColorFileEnding, openedFolder, iconFolderPath, + clonesFolder, } from '../../../icons/generator/constants'; import { getCloneData, @@ -13,6 +14,7 @@ import { IconConfiguration } from '../../../models'; import { FileIconClone, FolderIconClone, + IconJsonOptions, } from '../../../models/icons/iconJsonOptions'; import assert, { deepStrictEqual, throws, equal } from 'assert'; import { stub } from 'sinon'; @@ -32,6 +34,9 @@ import { } from '../../../icons/generator/clones/utils/color/materialPalette'; import * as icon from './data/icons'; import { INode, parseSync } from 'svgson'; +import { customClonesIcons } from '../../../icons/generator/clones/clonesGenerator'; +import { getFileConfigHash } from '../../../helpers/fileConfig'; +import merge from 'lodash.merge'; describe('cloning: color manipulation', () => { describe('#orderDarkToLight(..)', () => { @@ -627,3 +632,99 @@ function forEachColor( return colorCount; } + +describe('cloning: json config generation from user options', () => { + before(() => { + stub(fs, 'readFileSync').returns(icon.file); + stub(fs, 'writeFileSync').returns(); + }); + + after(() => { + (fs.readFileSync as any).restore(); + (fs.writeFileSync as any).restore(); + }); + + const getDefinition = (hash: string, options: IconJsonOptions) => { + return { + iconDefinitions: { + foo: { iconPath: `./../icons/foo${hash}.svg` }, + file: { iconPath: `./../icons/file${hash}.svg` }, + 'folder-foo': { iconPath: `./../icons/folder${hash}.svg` }, + 'folder-foo-open': { iconPath: `./../icons/folder-open${hash}.svg` }, + }, + fileNames: { 'foo.bar': 'foo' }, + options, + file: 'file', + }; + }; + + it('should generate the json config from the user options', () => { + const options: IconJsonOptions = { + files: { + customClones: [ + { + base: 'foo', + name: 'foo-clone', + fileNames: ['bar.foo'], + fileExtensions: ['baz'], + color: 'green-400', + lightColor: 'green-800', + }, + ], + }, + folders: { + customClones: [ + { + base: 'folder-foo', + name: 'folder-foo-clone', + folderNames: ['bar'], + color: 'green-400', + lightColor: 'green-800', + }, + ], + }, + }; + const hash = getFileConfigHash(options); + const result = customClonesIcons(getDefinition(hash, options), options); + + const expected = merge(new IconConfiguration(), { + iconDefinitions: { + 'folder-foo-clone': { + iconPath: `./../icons/${clonesFolder}folder-foo-clone${hash}.svg`, + }, + 'folder-foo-clone-open': { + iconPath: `./../icons/${clonesFolder}folder-foo-clone${openedFolder}${hash}.svg`, + }, + 'folder-foo-clone_light': { + iconPath: `./../icons/${clonesFolder}folder-foo-clone${lightColorFileEnding}${hash}.svg`, + }, + 'folder-foo-clone-open_light': { + iconPath: `./../icons/${clonesFolder}folder-foo-clone${openedFolder}${lightColorFileEnding}${hash}.svg`, + }, + 'foo-clone': { + iconPath: `./../icons/${clonesFolder}foo-clone${hash}.svg`, + }, + 'foo-clone_light': { + iconPath: `./../icons/${clonesFolder}foo-clone${lightColorFileEnding}${hash}.svg`, + }, + }, + folderNames: { bar: 'folder-foo-clone' }, + folderNamesExpanded: { bar: `folder-foo-clone${openedFolder}` }, + fileExtensions: { baz: 'foo-clone' }, + fileNames: { 'bar.foo': 'foo-clone' }, + languageIds: {}, + light: { + fileExtensions: { baz: `foo-clone${lightColorFileEnding}` }, + fileNames: { 'bar.foo': `foo-clone${lightColorFileEnding}` }, + folderNames: { bar: `folder-foo-clone${lightColorFileEnding}` }, + folderNamesExpanded: { + bar: `folder-foo-clone${openedFolder}${lightColorFileEnding}`, + }, + }, + highContrast: { fileExtensions: {}, fileNames: {} }, + options: {}, + }); + + deepStrictEqual(result, expected); + }); +}); From 73f68e0925002a296969dd19b0afe95c42a3c634 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Mon, 29 Apr 2024 13:26:49 -0300 Subject: [PATCH 19/25] =?UTF-8?q?docs:=20=F0=9F=93=9D=20update=20contribut?= =?UTF-8?q?ing=20guide=20and=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 57 +++++++++++++++++ README.md | 58 ++++++++++++++++++ images/how-tos/cloned-file-icons-example.png | Bin 0 -> 1143 bytes .../how-tos/cloned-folder-icons-example.png | Bin 0 -> 1609 bytes images/how-tos/cloned-rust-icon-example.png | Bin 0 -> 4093 bytes 5 files changed, 115 insertions(+) create mode 100644 images/how-tos/cloned-file-icons-example.png create mode 100644 images/how-tos/cloned-folder-icons-example.png create mode 100644 images/how-tos/cloned-rust-icon-example.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ce39c1d7e..a3946139b0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,6 +33,17 @@ A new icon for a file name, file extension or folder name is needed? Please crea It is always welcome to add new icons to the extension. However, there are a few things you should take into account so that the icon can be included in the extension. +```mermaid +flowchart LR + B{Shape already exists\nwith different colors?} + B ---->|No| E + B ---->|Yes| C + C[Cloning Workflow] + E[Creating New Icons Workflow] +``` + +### Creating New Icons Workflow + **Checklist** 1. [ ] Create icon as SVG ([how to](#create-icon-as-svg)) @@ -41,6 +52,16 @@ It is always welcome to add new icons to the extension. However, there are a few 4. [ ] Unique assignment to file and folder names ([how to](#icon-assignments)) 5. [ ] Provide separate icons for color themes if necessary ([how to](#icons-for-color-themes)) +### Cloning Workflow + +There are times when we just need to create a variant of an existing icon. + +For example, we might want to create an icon using the shape of the `typescript` icon, but we want it to be green and associated with the `library.ts` file name. In that case, we don't need to create a new svg. This can be done by configuration. + +**Checklist** + +1. [ ] Clone the existing icon adjusting its color ([how to](#icon-cloning)) + ## How tos

Create icon as SVG

@@ -299,6 +320,42 @@ The following are some tips to help you design nice and sharp-looking icons. The - **Curves vs straight lines**: Let's face it, pixels are square, there's nothing we can do about it. And since pixels are square, drawing a curve actually involves drawing a series of... squares. Consequently, when rendering a curve, we're essentially asking the display to render a fraction of a pixel, which is impossible. As a result, curves tend to appear blurry. This is normal. However, it's perfectly fine to use curves, circles, and rounded edges in your icons. Just keep in mind these limitations if you're wondering why your icon doesn't look as sharp as you'd like. +

Cloning existing icons

+ +The extension allows you to clone existing icons and adjust their colors through configuration. This enables you to create new color variants of an existing icon without having to create new SVG files. + +As we mentioned previously, icons are assigned to filenames, file extensions, and folder names in the following files: + +- [fileIcons.ts](src/icons/fileIcons.ts) +- [folderIcons.ts](src/icons/folderIcons.ts) + +The following example demonstrates how the shapes of the `rust` file icon can be reused to create a clone of it, utilizing different colors and associated with different file names than the original icon. + +```ts +{ + name: 'rust-library', + fileNames: ['lib.rs'], + light: true, // needed if a `lightColor` is provided + clone: { + base: 'rust', + color: 'green-400', + lightColor: 'green-700', // optional + }, +}, +``` + +This will generate a new icon assignment for the file name `lib.rs` with the same shape as the already existing `rust` icon but with a green color instead. Additionally, it will create a light theme variant of the icon with a darker green color for better contrast when using a light theme. + +That's it. We don't need to create a new SVG file. The extension will automatically adjust the colors of the existing icon. + + + +The same technique can be applied to folder icons by using the `clone` attribute in the folder icon configuration. + +You might have noticed that we are using aliases for the colors. These aliases correspond to the Material Design color palette. + +You can find a list of all available color aliases in the [materialPalette.ts](./src/icons/generator/clones/utils/color/materialPalette.ts) file. + ## Add translations This project offers translations into different languages. If you notice an error here, please help to fix it. You can do this as follows: diff --git a/README.md b/README.md index 8283ea0497..bb7b668e69 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,35 @@ In the settings.json (User Settings only!) the icon can be associated to a file _Note: The custom file name must be configured in the settings without the file ending `.svg` as shown in the example above._ +#### Custom clones + +It's also possible to clone existing file icons and change their colors to create new icons that can be associated with file names or file extensions. The following example shows how to clone the `rust` icon: + +```json +"material-icon-theme.files.customClones": [ + { + "name": "rust-mod", + "base": "rust", + "color": "blue-400", + "fileNames": ["mod.rs"] + }, + { + "name": "rust-lib", + "base": "rust", + "color": "light-green-300", + "lightColor": "light-green-600", + "fileNames": ["lib.rs"] + } +] +``` + +This will create two new icons called `rust-mod` and `rust-lib` that are associated with the file names `mod.rs` and `lib.rs` respectively. The `base` property defines the icon that should be cloned (in this case the `rust` icon). The `color` property defines the color of the new icon. The `lightColor` property is optional and defines the color of the icon when Visual Studio Code is running with a light color theme. The `fileNames` property defines the file names that should be associated with the new icon. There's also a `fileExtensions` property, which can be used to associate the new icon with file extensions (`"fileExtensions": ["ext", "ext2"]`). + +cloned file icons + +- Although you can use any `#RRGGBB` color for the `color` and `lightColor` properties, if you want to stick with colors from the material palette, you can check the full list of allowed aliases [here](https://github.com/PKief/vscode-material-icon-theme/tree/main/icons/generator/clones/utils/color/materialPalette.ts#L7). +- You can check the full list of available icons to be used as the `base` [here](https://github.com/PKief/vscode-material-icon-theme/blob/main/src/icons/fileIcons.ts). + ### Folder associations The following configuration can customize the folder icons. It is also possible to overwrite existing associations and create nice combinations. For example you could change the folder theme to "classic" and define icons only for the folder names you like. @@ -141,6 +170,35 @@ In the settings.json (User Settings only!) the folder icons can be associated to } ``` +#### Custom clones + +It's also possible to clone existing folder icons and change their colors to create new icons that can be associated with folder names. The following example shows how to clone the `admin` folder icon: + +```json +"material-icon-theme.folders.customClones": [ + { + "name": "users-admin", + "base": "admin", + "color": "light-green-500", + "lightColor": "light-green-700", + "folderNames": ["users"] + }, + { + "name": "roles-admin", + "base": "admin", + "color": "purple-400", + "folderNames": ["roles"] + } +] +``` + +This will create two new icons called `users-admin` and `roles-admin` that are associated with the folder names `users` and `roles` respectively. The `base` property defines the icon that should be cloned (in this case the `admin` folder icon). The `color` property defines the color of the new icon. The `lightColor` property is optional and defines the color of the icon when Visual Studio Code is running with a light color theme. The `folderNames` property defines the folder names that should be associated with the new icon. + +cloned folder icons + +- Although you can use any `#RRGGBB` color for the `color` and `lightColor` properties, if you want to stick with colors from the material palette, you can check the full list of allowed aliases [here](https://github.com/PKief/vscode-material-icon-theme/tree/main/icons/generator/clones/utils/color/materialPalette.ts#L7). +- You can check the full list of available icon to be used as the `base` [here](https://github.com/PKief/vscode-material-icon-theme/blob/main/src/icons/folderIcons.ts). + ### Language associations With the following configuration you can customize the language icons. It is also possible to overwrite existing associations. diff --git a/images/how-tos/cloned-file-icons-example.png b/images/how-tos/cloned-file-icons-example.png new file mode 100644 index 0000000000000000000000000000000000000000..90278c0a043703003fa493aaf669b172d7b0a3cf GIT binary patch literal 1143 zcmV--1c>{IP)0{{R3bI@^m0002tP)t-sB_$?{ z!%9M>^{mXyB_)BunPJS#%z>@U%)gvqB__el%q3!+f3Zfaf+jze+ni!1%*?@wz)3=* z@hCMwWr;j6HYp{7tU86kH+83Rn?ZpkCd|yNY?D8nzsx&|$;_>RL80$Hm)|*kv@>Ii zB_&}iLsnLDH%wwQV41-yK2JGCEkB&*h`vdJwMINiE-p-Eg}O+0qe4YjF)&tiIf1)# zpF%*M={=L!%)h}fSa^M{MJO>jG(0O{Vw`!XLp_bpfvv&8%&cIUtUpaJtgXz!%)vHp zot$EUU?qWIC1GEHI))9X+yDRrQAtEWR9M68lU-}uFcgO0m#yyBsbm6akW`^J#Tv#o zw!tt$7WP3e+W-H9s)N#06bCA(d*$as;z*!}_ejFR!ou6IeT%(o@iA0@Bc0|5Sj>-Q zy`%0rrMXkAFgpX;ln{JFddy7ado8+1SswSa-GuGP(HKKAjEejZlyr7Jp`(s{B>+!Kdo}ylExi`u!?q~C zuTK8oSH*SMifeCXubU{0NJ4aMr-Z-mS7D5&OZj&L{0L6`%MrqLiBMUH&|yj_XX9>r z3*lJi_qpYBLZ!lBq-gYhN_ewD;Afnqlu80;(No*g9j>Vm&~y_*36m(KlfmVSsu;>a;RyLQc~gEqmlX+sqnaz zK}E7k>OqrsQYg1|x4qefwIGYaKdX>1_ZEkPetgNNdtXf_l%Fp5l?pG5`0w-Zr1BP` zYsdk!Q&AnLRo7{%nilh8fsxvgF4*xgv~G{Kw^&$sYR8OK9EYJOdb!|~DxQkCQY0C{ z7#A*Vk_{=QS$6mTzi9Q4nb8zsQK4ar9Ec)#po1(sdDkyq^uxLY8xy{V8HnUAbif1s zvGj(rT!ZiyIHNl&xlB;Dg}kFP^WAyX6(;|_TC|`&K?@Jkul064IrkJIa6(O_M_1uZ z%m?QUz+!p|ljC_DN0NCoWCA8eTQB#fjhXgm+6il%Icf7∈VV3mXDlq#$ER02?_0 z5V}fu8nhezsbkjU^)4z^xlVQcp(VgsS&Hx=0a%zSVX^3_9j{tTw_T=c7@7jyeyxPY zB6HnQ2`|Svm3~-1DC_FQ zWY{cJw~#BkGc(*M;kpQor$v7rfTQeW%yxVaDOx0F@#4k*z+X4WPaP$ljpqOW002ov JPDHLkV1lY(AEW>P literal 0 HcmV?d00001 diff --git a/images/how-tos/cloned-folder-icons-example.png b/images/how-tos/cloned-folder-icons-example.png new file mode 100644 index 0000000000000000000000000000000000000000..18c31b156bb9366e2c43fb553f8fdd745cdc1fc7 GIT binary patch literal 1609 zcmV-P2DbT$P)aN0004@P)t-sB_$@S zN4$%}N>pxo%*@OsVw}Ou%&dYYB_)BZ%*1^Np>|4~ zZc3MDG<`@W>Cx`tzUJ4e;N0%jz{=05z{Sz%!o}jPi9VMvSciLogGgItahpNA!^V@e z#jw7_XqdL7tFNA_o{5Qyz^sR8HgP>lP$eZjSadj8FDAji!HlrKwvW7_u(zzgoQjs3 zroNKDrh->8Uo|N?OJFoHI4VvrCfUu~(7Mym+tGcg#l+OTn?tFkou|dQmxYg%idd2_ zPJ$&!Zbwg4jDAXeZc2Z!MnFSGU=hVi6(X7ABy>80Pzrmcp!N0Y^x4^l<%&eu0 zs)ClEZIqXDh?1GAjxS=2uGWUZt%0nqft+H2U?qW~&vAT|WTA~?R9IVemQgPyCM6rm z4gdfIV@X6oRA}Dqnu%8uK^(^ib_+GGwT!X|C?blWvP+b(C^Ie1Ow+RazVG|KufMwA zZ+~pRU55uc5A*q&&y(5N;XUWW&dkDhKxj0YMZ?HJWF#&YinKu_(p##Cy2O^ZOKnS* zMuj?^#!M3b+#v4eL8bDbp@9hr|8(pYvg`?Qqv$g#-JJ zo$wfp7{mnhUmAcS@4mvOX-27Ys99hDZGnhsE^qHxZJNv41fB~KS@yLRPa7(*02?Go zAPap@7zSPk;n4AXl@Ke;08}F@p&Bu5188Vz?RtWx1U zkY^o*lU;jn?K^#5%rHo>frIc&1RP%bI`C_j4ag9ybrrVuI|6KwHXH`R`7DUs2oH)X zFtGOMD;vlPB;YaNd(y;!fy4(V-HXpV3K3NUMlwhk4%t!`%FawUzG0TY3F7{LFIB2b;(JWwZ7>F=+I zg(V|28jVJy(fp^}z9(dc_anp8VwSYDs%Pfoo7eA_8qfHMt7kSkOwXHv*j__njZfn0 zMg;>!1=%2(%O!oo^I^aWd+Hb@dxF7Wch611U}&e$?RJhM11Hr}h1zG!x+AP08QA?7 zp}6KY7^B2$QHDkXN>P+)u+lLE1_}z2K@$4z@%eIaa2rr;z!EnO(0~GiU8;=9@yplQ zSTe}@e2={b9NH<*VV`)UDM4y;V%>(VyXstn?x)Ej8@NM~lWEZaCIt}{D{vO53&;jU zV*|^TGC@Q|fDM>pFqgyy8GPY2s67fy1pH<#uw$oSq_t3_Hu$3}q9DNU3rwyX1&XhQWB?u0Er<*#;M?1KbGjiN zxGN;4hu1JS#~+T52QP{RBy;q_g`+|Ny_KNRXfztlBBf?}A3cPOo)Zc*CB2{Z6Zg+< za!`^{Pw#8}68AR=W}X#fgH)lA@{bAyRMNX_km?JCLcM+8g#rrESvIiyFGDfJ&tqPr z&`h7ut?DOtXoh1nhF{Cm7J`z3*B}Lb_xb$=IMgzryz;pkr(@k*$tciEu#TdG{!zR0ZqjKw(zzZZy~5;S9vXz>3C&mW-=!O+RMUGU<2MlP!Ny|QoSh} zjf}jHga-7J6C8IoU{NIjdS74)y(mzAFPQ4Soe68<6RiN}*D!&q0dFCwrT25AYnfkD zkEW(VR|G@f|F8UFug2;PiE7Sf25%uWDZO91q|>`b(>(qF;Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D50XhlK~#8N?VS%) zRM(xyzset#L1a`I{4*Ls4M+|~#rz@NX6&A*jg$4T*+@@M#28|Oai!YeCV_2tvZl7)iZ_l?#T&;2 zB;Kqkp{Q7sfCpnDcndH;G8~8gZXQ<7oWfgF#)^RAyn_$vVbn(n$Ny1(5uJTEV7fOB zlR_rok6qVctT->6QyCk460h=`3HbbSA3nGD3B7Jx(oB9vo;S{R;nU9RxM%ugBu7ui zKc8qt_rOhlRwYop7H`4Sa8$2b#A_lq25+I+F@VONe$?B0S<3{>W}>FXXfRI`hOmhf z#0xo(f7pcTP6w~41d7+$c(>^aa%f?}vpf2EHf z0jHRg;;(ST=Hpfzw+;1Ha(n_IY0InF^%xV*VTI+K1-9-me&)_WvFEL0J>?D7V+NR+ z`&LymZ}lEe=Y+<=->*%;)UXiz>&XrjGjG&C3^2W&i_|F%IP&B!l-!{{kfM06ERSOj z#|&Qc9V_!hcYJoiO~g$LwfIr$Z0v8igdf(~c&o2uq0pR;__lIv{Z1jQy!B2)Ry@j% z{M)iPte7EYP6|$S3ZD{s=6qxZ=V-??#?Rk`+Joe3nLYk}mLN_*j>~5ISVm0{nH}2xK;bR+} zm!NYLoV)TC^I}(XFZLgQe;y|;UKfr>hxa&MbH#Ghh@6AGqFIU!+q3c53O%$^igmyN z+qp`V{WHEn16GHVI2=-xy0ogBX#HvL)NYC>? zw+%JF$ikiv#JVrY_PpjECrp3p5RU7w^b4O#kXWuoLK^ct`LOMp@Jabl?}a05XknhE z<(v(|5oaw#J)qjk;#5IZJ!klpOvHhvj$juG|(+EBKM6#WF_)zCH%QZ z0nKI>!aPjE@diS(8m8BBVO*kP|BMFI9;-#eIV*ZJI>gQ4*Z4>%{~Si_Wa-q1{){3d z+~M=C+~rs)yUcDI_Bw-y<-ECGy|~yZXa@AuNFlXU=P;z z0_<87kMtecD9TEKqXgpEI{<&6y1WXa<5zbkMkeBOam*URxy! z-9#YR-|h8c)R6g`yAm18#8$uVQACSyTM2Sqg(G-h2#;btQaluK)S>PSKlTw>@&0}J zEPe!}cp)B{MjF?sc$&!#E?%hwC9Vk?Q^~6h8Yyw?r(;&>&t+&3TRf;mZ4=3(!@PUN z7QgOM#DRv&GLMIVQCc$ptH3vN6SGbsem-xK3jJO!aT zwG&b=l%SB}wfpC|-9Mj?)->mLpy8r$91IL`Y&>B&*tj(kPRUa9p5V_t%Jks!P-HYj zb1`yWDn@m)9h&*6NdN8u96nr(eb1)(EuyS=6nAa;#3}^RArmJE=Fhdb#&`tPsa=A? z1y`i=QVC_>NppQPSzB|7rjmj~O{X|xp_x3|AToe{(P#4O9%USGjFgNzQ-(d;Ho~~c zgyISd9FuiOdMpn``38Q0Z^()#AqCHr9+S+XQjwsZM}fKAHS}EsP0|J%$63(})g`ZE zZipPINLxtHc?TL#h%Nry6EI!1DEnCsGF~WW{xcgTlMV1kK1K0Tw3CHTnp8C{)44e% za>W%-Cx*()YlIukXME6!q5PEhDCRb4*+b$q{v)%ie#-)OnjP>5&nck_7VBkx)WU1NqL3a- zWanOG=V(~9TVZKx#FtKHo)#xTPo7CvH42~GgL2nr&F6az$a^anX)_$CJNPv6kBTRT zf^;5~yRxMaW}_bVb}L#MPs1Ly1PKZ8&_@lxax@P)2j$%*B@}xe&9|k!e-7Ezt+4d; z3qSt5E}0c?d1uvpteq2uFRu0rxlA&jw4P;5IKv!+X9{3G&CFq>9`VZzSbaYeA>4+B ziX!BD3P<>JkFT7pL&GH}B4@>8>BB}Cmc=7((2D936JGK|I6|@Kxw2*P^xGjsKO48+ zWdXR-G6k~*2|>M% z1>woe+|tZ}E7Xh6U-QShtH|NCc(k77KOdYc%&woh+=mBZBSxL;qfmo9R&sn&n1d47 zvIiAUycSOm^CJs{siXeETS6k6@}W{%je>lN6;;L##noe@ajv@`(UU^))WlQ4HlByRzx=_Xe!9}z- zTtn<#5v)wX2Zc@OzplQ{94x#RFFI}#vR+T-HIc!=Te#ZMk1Or{=xpj07RFCyqRwF^ zlg#HtkIC30rgN~?d$oQ}#S6R^FYzHQ*6mmz91jlM!s%mODF6A$3y7j(!g0?xW@7oH zF_;t~zHw3cW-Cs<@BXfzivK9i=h2TodvPQd-WQGKMjaxeLeTQ(9vsT4N8=xr*P@Vl zq+WLB6FXv<$($BD2~n(9_VoDcnvu9VHbXPz`w;EMI z3s(+(*ClH&-uj+@tELo}*tx;qT}LenzHLGkZ~a?AF;t*&Oro>ty5GVPNB1_eUOP12 zk>e9g3@?_#Y&N6#`BdKWx6dfzi9awoCRJ}Z&Y_ZPXbDifK((%*xsw++eeh0Nq=FXaIE4xvf@>hvWLDamLX@Q^35Qv?W7kun8Q-Ih1Z9nh2gnEl$M!=>!=(q-iPc5y~)9n z`DORKZDL1B$lNPBN9NKzr12{>DI1WtzZ7LhhrYk8bRV+3dKKC~WW~E`>&LOT#1}za zJ(;QuKb_XTd;-7HJvTBfMDY zNdgS_0(B+%$S@jVQEQR3z?%JE8eQ7hMvRN_@SNKM}kZvC7r2w zn#p9tbbd4hC9VmYIx=L6yXTHAHdthL7^+3X1?v5gm@c=i(lD&3IW#;gRCT;g*tSEr zW)DjD<{*6qH>+cetaxQV{t{#lyX5JC&%X%DC4dEpifDK-XmOebU4Hv9;6L;#sFpU5{7=@*acIBX&rx| zdtC+d#)}Oa-Bwtmjo5vt7zbX-MeZwwIPf+Lpi|f{-c^m_pSol{SLLCU0`0@Qk?tYL zNF=P^#lG(_iudn>>AN{F?LC0f+%#sY?Wq0WSNy8rqbMHDw-s-%$NR6HhrQh?%*Q=p za{rGpyy1x-EMoUY;Zkd#Fq=;1lh(6*b=dIVI|^WK3rAdvLD-2wKgAB~@sgnI^w9Wu z4XoYa(9KUm>LaU>dUqVcyBlEM$AqjHyD{DCWf05P#j+4Wbn7W~cKV|(R1_)iqq6$h zd06uB41}WIuJC9)|WDy7vZ$fsCQW$XxOH;AqaeV7s%hX2{yh~~O(SWk%|0eO|b+1~_bueuw= vSQ1drQ~ArH7%Hgzb)$+mj#0%Mw*dbSGZOnmDMNbC00000NkvXXu0mjf(6QmX literal 0 HcmV?d00001 From caf78ffcfbef3f7f4c6f714f573f2e50a1a6356c Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Sun, 5 May 2024 09:07:22 -0300 Subject: [PATCH 20/25] =?UTF-8?q?feat:=20=E2=9C=A8=20documentation=20for?= =?UTF-8?q?=20`mit-no-recolor`=20attribute?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 43 ++++++++++++++++++ .../how-tos/cloned-icon-no-recolor-result.png | Bin 0 -> 1001 bytes images/how-tos/cloned-icon-no-recolor.png | Bin 0 -> 1055 bytes 3 files changed, 43 insertions(+) create mode 100644 images/how-tos/cloned-icon-no-recolor-result.png create mode 100644 images/how-tos/cloned-icon-no-recolor.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3946139b0..2b3a65f91a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -356,6 +356,49 @@ You might have noticed that we are using aliases for the colors. These aliases c You can find a list of all available color aliases in the [materialPalette.ts](./src/icons/generator/clones/utils/color/materialPalette.ts) file. +#### Preventing recoloring in cloned icons + +When cloning icons, recoloring works by replacing each color attribute in each path/shape of the SVG with a new color, which is determined by the selected color in the configuration. + +However, there are cases where you might want to prevent certain parts of the icon from being recolored. + +Let's see an example: + +![gitlab icon](./images/how-tos/cloned-icon-no-recolor.png) + +In this example, we have the `folder-gitlab` folder icon. If we were to clone it, we might want to prevent recoloring from happening over the gitlab logo and only allow recoloring of the folder shape itself. + +To do this, we need to set the attribute `mit-no-recolor="true"` to the paths, shapes, or groups we do not want to be recolored. + +```svg + + + + + + + + + +``` + +Now if we create a clone of this icon, the paths, shapes, or groups marked with `mit-no-recolor="true"` will retain their original colors. Recoloring will only affect paths not marked with this attribute. + +```typescript +{ name: 'folder-gitlab', folderNames: ['gitlab'] }, +{ + name: 'folder-green-gitlab', + clone: { + base: 'folder-gitlab', + color: 'blue-300' + }, +} +``` + +Will result in: + +![result of cloning gitlab icon with selective recoloring](./images/how-tos/cloned-icon-no-recolor-result.png) + ## Add translations This project offers translations into different languages. If you notice an error here, please help to fix it. You can do this as follows: diff --git a/images/how-tos/cloned-icon-no-recolor-result.png b/images/how-tos/cloned-icon-no-recolor-result.png new file mode 100644 index 0000000000000000000000000000000000000000..1c0cd3faa06731771d38f1910b7a36c46955c3e4 GIT binary patch literal 1001 zcmVPx#R8UM*Mel3?C@eJPIW=Up_W7tKO@gglq~7di3_o9e zgqOueaFjAcR_8)2&Tu<)rQYgN8>DxYnu($8Uk&b-FYn+dGf6~2RA}Dqo7uLaFc3v^6Es4Y2Sxq=|7=hcGC^Wh8hiCU&y}oA zZ9s*R{Cq*kFN6s~oYo^maEQZ^ZRajRLI~!Vg9rw3V$L~=KoElYM1+AjctNmJ2nfGM z3`qn&MB)p9dzE|&F4G_sM#4ki(h`5a1i}SL|13<)-($bk>DDHE72s$QrEWXhR85g3Y)V&q?qxg&t zaenXXkcz(gh6?dKHdi0dw$U3h#I!A~+|Qo}GAhJt+q3J1G$CU`yllAnC}(6$h#RFI zKIn{$2r)Op#Ya3>mxvH0r5-*O)jc9azksMbYz$W^8brR__Uu*Np+PKdZ2OR_(IBK< zH+{&}9TJ3mXj?w=Fc4>X+YKKIGR1*V4sFYaTHT>QsE0OwIC>yc6bNnCRUaBMy&^#9 zhqmfNukH{a^h2Y!*qTbOrf>*DBYe|=Axby|1w-@*5LX}sf{mvS2n2?WM=Kx@q%Kk* zK~azn1QC-V5RQRhNDqR@$TRh47{#=lTp=d(-bc+G}t9*?j-B3aJ1?eHl3dKW7vCV++;4I^7&8SxxFdwE? zb_(9w88YNo4OS&l)Y$MU^fy07CA^Ks7@NUEyofU>;n9@Vq$12K$xK)AN@GxAfJhhu z1=m#=o53ybMNbG2OC>x~(yA)PD5DEMM+}NtNu;GwRE$x_-V%mG!F3fD)&9dq)%;T}*j>Azkn+yjLep=aZ zW;U08B00004XF*Lt006O% z3;baP00001b5ch_0Itp)=>Px#JWxzjMddj)?`!}lEHrg>b@`|zYiw;iK14J$JL_W% zhiz$2P*kZ~Rq0J3zezsNU?=sMCxt*RUtnVIgd)XrHJejfxBvhHfk{L`RCwC$oavIQ zAP|M&x_~0C@Bgl2GMNe@d%8Q;E&I3a`8Y>3QIY+-da;}ECcFu6!mkmweL}UZ_!6P$ ztJG@RZxFUsPBns@Fb|N#M)nclj@Z)@E>(W@l!R?nQVSvBQbdS6Umyr^55@ywQ}T*#3Z3xtYMNeLo6vi(T#3*<=dMNQI&)o&&~g618ewyJ!m7PI;c^qgABZ>MP52)Z zb{8k?7}H&03kl(_wpUlyLPDsXa;mI_gz%}xbXf}p;csGDF&ixug!(0?azlQ$5D@

K*$Wa>_KS)!nYskF~3__hfkcFg9WG&-TF zmKw9!!a8h1eU!@$c^?%r;ZZFuW_9clxP+FP&JLNe8JEyfONrUC{S-4Sp{XMm=Lq02a!;n)l@;xj7NADi^t3;2_1n&V|MlkEJEc{Qw2RU7NJWl z7PEHtAQ3w4C@UmF4M#Ym5Oxf(1O}mkB%Cn_Apy;~0!9cLvKccN;mGAMz|aj+@)@3x zKT87O{q6{yu;Z_i0|4J%0-A8-{By=PAAiS;Kv4XEg0iNKLvTZhc!x6)QDy)_-^Y$C=lIm?1Z_;c1tksurMMmgOTD|~1Hp<^S z-ehm4NoT?VMEUA$P}WwYB)!&$T3rd#5+0M57NBk8`lJ(@iNekE5lY3Tuc*(eWBt(M4IGXec3?4Go&{WTpEb}b9AR}_LHR$Cpc z?FiV$S7nL?9B~SGxWycFt-Wob89OiH6gAMZI#`Gnwmeg1J8bmb71qjO<1}At4S0J( zTramu-I~+e1=rD=b2>F);+6jMl$|j7w+Ob}gz3MQ@JKo*;ctA;VV-k4?r&n;gg0SI Z{QxZqvtQbo+8Y1>002ovPDHLkV1iM!&3*s? literal 0 HcmV?d00001 From c5565bb90331ee91ee9c4ee43de921742ddec338 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Sun, 5 May 2024 12:10:18 -0300 Subject: [PATCH 21/25] =?UTF-8?q?fix:=20=F0=9F=9A=91=20icon=20availability?= =?UTF-8?q?=20check=20failing=20on=20clone=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../icons/checks/checkIconAvailability.ts | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/src/scripts/icons/checks/checkIconAvailability.ts b/src/scripts/icons/checks/checkIconAvailability.ts index 01edffbc44..08de2f6efc 100644 --- a/src/scripts/icons/checks/checkIconAvailability.ts +++ b/src/scripts/icons/checks/checkIconAvailability.ts @@ -16,6 +16,7 @@ import { lightColorFileEnding, openedFolder, } from './../../../icons'; +import { CloneOptions } from '../../../models/icons/cloneOptions'; /** * Defines the folder where all icon files are located. @@ -27,6 +28,12 @@ const folderPath = join('icons'); */ const availableIcons: Record = {}; +/** + * Utility type that represents a File or Folder icon that has a clone property + * defined. + */ +type CloneIcon = (FileIcon & FolderIcon) & { clone: CloneOptions }; + /** * Save the misconfigured icons. */ @@ -82,11 +89,16 @@ const isIconAvailable = ( iconColor: IconColor, hasOpenedFolder?: boolean ) => { - let iconName = `${icon.name}${hasOpenedFolder ? openedFolder : ''}`; - if (icon.light && iconColor === IconColor.light) { + const isClone = isCloneIcon(icon); + + let iconName = isClone + ? getCloneBaseName(icon, iconType, hasOpenedFolder) + : `${icon.name}${hasOpenedFolder ? openedFolder : ''}`; + + if (!isClone && icon.light && iconColor === IconColor.light) { iconName += lightColorFileEnding; } - if (icon.highContrast && iconColor === IconColor.highContrast) { + if (!isClone && icon.highContrast && iconColor === IconColor.highContrast) { iconName += highContrastColorFileEnding; } @@ -98,6 +110,39 @@ const isIconAvailable = ( } }; +/** + * Type guard to check if the icon is a clone icon + */ +const isCloneIcon = ( + icon: FileIcon | FolderIcon | DefaultIcon +): icon is CloneIcon => { + return ( + (icon as CloneIcon).clone && + (icon as FileIcon | FolderIcon).clone?.base !== undefined + ); +}; + +/** + * Get the base file name of a clone icon. + */ +const getCloneBaseName = ( + icon: CloneIcon, + iconType: IconType, + hasOpenedFolder?: boolean +) => { + const clone = icon.clone; + const folderBase = + iconType === IconType.folderIcons + ? clone.base === 'folder' + ? 'folder' + : clone.base.startsWith('folder-') + ? clone.base + : `folder-${clone?.base}` + : clone.base; + + return `${folderBase}${hasOpenedFolder ? openedFolder : ''}`; +}; + /** * Check if the folder icons from the configuration are available on the file system. */ From 49f6e7bf6fd8a6ad5fc0bc2609cffcb7ad1e8a6c Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Sun, 5 May 2024 12:34:32 -0300 Subject: [PATCH 22/25] =?UTF-8?q?fix:=20=F0=9F=9A=91=20broken=20links=20wh?= =?UTF-8?q?en=20generating=20icons=20preview=20png?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripts/preview/index.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/scripts/preview/index.ts b/src/scripts/preview/index.ts index c30c5afdc4..10903c1505 100644 --- a/src/scripts/preview/index.ts +++ b/src/scripts/preview/index.ts @@ -9,22 +9,27 @@ const filterDuplicates = (icons: string[]) => { const basicFileIcons = filterDuplicates( fileIcons.icons - .map((i) => i.name) + .map((i) => `${i.name}${i.clone ? '.clone' : ''}`) // merge language icons .concat(languageIcons.map((i) => i.icon.name)) -).map((i) => ({ iconName: i, label: i })); +).map((i) => ({ iconName: i, label: i.replace('.clone', '') })); const folderThemes = filterDuplicates( folderIcons .map((theme) => { const folders = []; if (theme.icons && theme.icons.length > 0) { - folders.push(...theme.icons.map((i) => i.name)); + folders.push( + ...theme.icons.map((i) => `${i.name}${i.clone ? '.clone' : ''}`) + ); } return [...folders]; }) .reduce((a, b) => a.concat(b)) -).map((i) => ({ iconName: i, label: i.replace('folder-', '') })); +).map((i) => ({ + iconName: i, + label: i.replace('folder-', '').replace('.clone', ''), +})); generatePreview('fileIcons', basicFileIcons, 5, [ 'virtual', From c1b77962a33462887acc54b64ce9a3e08363f01e Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Sun, 5 May 2024 13:08:40 -0300 Subject: [PATCH 23/25] =?UTF-8?q?fix:=20=F0=9F=9A=91=20icon=20usage=20chec?= =?UTF-8?q?k=20failing=20for=20clone=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripts/icons/checks/checkIconUsage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/icons/checks/checkIconUsage.ts b/src/scripts/icons/checks/checkIconUsage.ts index fb8f362d12..4648cc41a5 100644 --- a/src/scripts/icons/checks/checkIconUsage.ts +++ b/src/scripts/icons/checks/checkIconUsage.ts @@ -34,7 +34,7 @@ const fsReadAllIconFiles = ( files.forEach((file) => { const fileName = file; - const iconName = parse(file).name; + const iconName = parse(file).name.replace('.clone', ''); availableIcons[iconName] = fileName; }); From 636503c2fcfe67a1376b146102b980aa72042179 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Sun, 5 May 2024 13:18:50 -0300 Subject: [PATCH 24/25] =?UTF-8?q?docs:=20=F0=9F=93=9D=20CONTRIBUTING.md=20?= =?UTF-8?q?-=20add=20missing=20section=20to=20the=20TOC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b3a65f91a..0346bdd552 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,7 @@ Glad you're here and interested in expanding this project 🎉 In order to make - [Unique assignment to file and folder names](#icon-assignments) - [Create icon packs](#icon-packs) - [Designing Pixel Perfect Icons](#pixel-perfect-icons) + - [Cloning existing icons](#icon-cloning) - [Add translations](#add-translations) - [Update API](#update-api) From f5edca0cdc76fa14d13832024bf69741cfa68dc1 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Sun, 5 May 2024 14:39:49 -0300 Subject: [PATCH 25/25] =?UTF-8?q?fix:=20=F0=9F=9A=91=20filter=20out=20clon?= =?UTF-8?q?ed=20icons=20from=20README=20preview=20pngs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripts/preview/index.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/scripts/preview/index.ts b/src/scripts/preview/index.ts index 10903c1505..74021eed69 100644 --- a/src/scripts/preview/index.ts +++ b/src/scripts/preview/index.ts @@ -9,10 +9,12 @@ const filterDuplicates = (icons: string[]) => { const basicFileIcons = filterDuplicates( fileIcons.icons - .map((i) => `${i.name}${i.clone ? '.clone' : ''}`) + // remove icons that are clones + .filter((i) => i.clone === undefined) + .map((i) => i.name) // merge language icons .concat(languageIcons.map((i) => i.icon.name)) -).map((i) => ({ iconName: i, label: i.replace('.clone', '') })); +).map((i) => ({ iconName: i, label: i })); const folderThemes = filterDuplicates( folderIcons @@ -20,16 +22,16 @@ const folderThemes = filterDuplicates( const folders = []; if (theme.icons && theme.icons.length > 0) { folders.push( - ...theme.icons.map((i) => `${i.name}${i.clone ? '.clone' : ''}`) + ...theme.icons + // remove icons that are clones + .filter((i) => i.clone === undefined) + .map((i) => i.name) ); } return [...folders]; }) .reduce((a, b) => a.concat(b)) -).map((i) => ({ - iconName: i, - label: i.replace('folder-', '').replace('.clone', ''), -})); +).map((i) => ({ iconName: i, label: i.replace('folder-', '') })); generatePreview('fileIcons', basicFileIcons, 5, [ 'virtual',