From 493d23526607f4fa458171d9ffe64733a8e1a5b2 Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Sun, 5 May 2024 15:00:16 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20clone=20and=20recolorize=20?= =?UTF-8?q?icons=20(#2305)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: โœจ clone and recolorize icons * feat: โœจ integrate icon cloning with the the extension * chore: ๐Ÿงน update vscode-test dependency 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 * test: ๐Ÿงช fix failing tests * refactor: ๐Ÿ”จ improve in-code docs & comments * feat: โœจ config to create light variants of the icon * feat: โœจ improve recolorization improves recolorization logic, keeping originally darker colors dark and lighter colors light even after recolor * feat: โœจ edge cases: support ignoring recolorizing paths 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 * feat: โœจ do not recolor some paths in some edge-case icons * feat: โœจ jsconfig edge-case * refactor: ๐Ÿ”จ simplify cloning process * feat: โœจ allow creating clones at build time 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 * test: ๐Ÿงช test clone configuration generation * chore: ๐Ÿงน fix file names to camelCase to match project style * test: ๐Ÿงช test clone data config generation * test: ๐Ÿงช color manipulation tests * test: ๐Ÿงช test svg cloning and recolor * test: ๐Ÿงช json config generation from user options * docs: ๐Ÿ“ update contributing guide and readme * feat: โœจ documentation for `mit-no-recolor` attribute * fix: ๐Ÿš‘ icon availability check failing on clone icons * fix: ๐Ÿš‘ broken links when generating icons preview png * fix: ๐Ÿš‘ icon usage check failing for clone icons * docs: ๐Ÿ“ CONTRIBUTING.md - add missing section to the TOC * fix: ๐Ÿš‘ filter out cloned icons from README preview pngs --------- Co-authored-by: Philipp Kief --- .gitignore | 2 + CONTRIBUTING.md | 101 ++ README.md | 58 + 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/jsconfig.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 +- images/how-tos/cloned-file-icons-example.png | Bin 0 -> 1143 bytes .../how-tos/cloned-folder-icons-example.png | Bin 0 -> 1609 bytes .../how-tos/cloned-icon-no-recolor-result.png | Bin 0 -> 1001 bytes images/how-tos/cloned-icon-no-recolor.png | Bin 0 -> 1055 bytes images/how-tos/cloned-rust-icon-example.png | Bin 0 -> 4093 bytes package-lock.json | 1008 +++++++++-------- package.json | 84 +- package.nls.es.json | 8 + package.nls.json | 8 + src/helpers/fileConfig.ts | 4 +- src/icons/generator/clones/clonesGenerator.ts | 191 ++++ src/icons/generator/clones/utils/cloneData.ts | 268 +++++ src/icons/generator/clones/utils/cloning.ts | 119 ++ .../generator/clones/utils/color/colors.ts | 129 +++ .../clones/utils/color/materialPalette.ts | 297 +++++ src/icons/generator/constants.ts | 10 + src/icons/generator/fileGenerator.ts | 21 +- src/icons/generator/folderGenerator.ts | 23 +- src/icons/generator/iconOpacity.ts | 5 +- src/icons/generator/iconSaturation.ts | 5 +- src/icons/generator/jsonGenerator.ts | 23 +- src/models/icons/cloneOptions.ts | 5 + src/models/icons/files/fileIcon.ts | 6 + src/models/icons/folders/folderIcon.ts | 6 + src/models/icons/iconJsonOptions.ts | 18 + .../icons/checks/checkIconAvailability.ts | 51 +- src/scripts/icons/checks/checkIconUsage.ts | 2 +- src/scripts/preview/index.ts | 9 +- src/test/runTest.ts | 2 +- src/test/spec/icons/cloning.spec.ts | 730 ++++++++++++ src/test/spec/icons/data/icons.ts | 56 + src/test/spec/icons/fileIcons.spec.ts | 64 ++ src/test/spec/icons/folderIcons.spec.ts | 113 ++ 58 files changed, 2976 insertions(+), 495 deletions(-) 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-icon-no-recolor-result.png create mode 100644 images/how-tos/cloned-icon-no-recolor.png create mode 100644 images/how-tos/cloned-rust-icon-example.png create mode 100644 src/icons/generator/clones/clonesGenerator.ts create mode 100644 src/icons/generator/clones/utils/cloneData.ts create mode 100644 src/icons/generator/clones/utils/cloning.ts create mode 100644 src/icons/generator/clones/utils/color/colors.ts create mode 100644 src/icons/generator/clones/utils/color/materialPalette.ts create mode 100644 src/models/icons/cloneOptions.ts create mode 100644 src/test/spec/icons/cloning.spec.ts create mode 100644 src/test/spec/icons/data/icons.ts diff --git a/.gitignore b/.gitignore index 2f4f1fe2c0..1a47c83859 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ 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 src/scripts/contributors/*.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ce39c1d7e..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) @@ -33,6 +34,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 +53,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 +321,85 @@ 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. + +#### 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/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/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/jsconfig.svg b/icons/jsconfig.svg index 41cdc378ee..53b872eb9f 100644 --- a/icons/jsconfig.svg +++ b/icons/jsconfig.svg @@ -1,4 +1,4 @@ - + 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 @@ - + 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#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 diff --git a/images/how-tos/cloned-rust-icon-example.png b/images/how-tos/cloned-rust-icon-example.png new file mode 100644 index 0000000000000000000000000000000000000000..ec96106d695c2125fbf6bdcc93e69e907499ebe2 GIT binary patch literal 4093 zcmVPx#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 diff --git a/package-lock.json b/package-lock.json index 9367a45e93..2969b42722 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,17 +8,22 @@ "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", "@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", + "@vscode/test-electron": "^2.3.9", "axios": "^1.4.0", "changelog-machine": "^1.0.2", "eslint": "^8.16.0", @@ -29,12 +34,12 @@ "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", "ts-node": "^10.8.0", "typescript": "^4.7.2", - "vscode-test": "^1.6.1", "webpack": "^5.71.1", "webpack-cli": "^4.9.2" }, @@ -208,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", @@ -250,6 +299,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", @@ -334,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", @@ -540,6 +610,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", @@ -911,25 +996,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", @@ -950,12 +1016,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", @@ -1052,24 +1112,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", @@ -1101,15 +1143,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", @@ -1177,8 +1210,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 +1417,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", @@ -1488,45 +1543,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", @@ -1851,6 +1867,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", @@ -2129,53 +2150,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", @@ -2406,6 +2380,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", @@ -2484,6 +2464,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", @@ -2672,6 +2657,54 @@ "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/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", @@ -2694,11 +2727,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", @@ -2724,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", @@ -2845,18 +2887,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", @@ -3008,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", @@ -3147,6 +3190,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", @@ -3192,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", @@ -3468,6 +3523,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", @@ -3695,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", @@ -3876,6 +3966,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", @@ -3985,12 +4084,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", @@ -4095,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", @@ -4130,73 +4232,25 @@ "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==", + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "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" + "punycode": "^2.1.0" } }, - "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true }, - "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", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, "node_modules/v8-compile-cache-lib": { @@ -4205,21 +4259,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", @@ -4456,6 +4495,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", @@ -4676,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", @@ -4712,6 +4814,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", @@ -4796,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", @@ -4913,6 +5036,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", @@ -5219,22 +5354,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", @@ -5252,12 +5371,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", @@ -5324,18 +5437,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", @@ -5354,15 +5455,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", @@ -5407,8 +5499,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 +5662,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", @@ -5644,47 +5754,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", @@ -5922,6 +5991,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", @@ -6121,43 +6195,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", @@ -6322,6 +6359,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", @@ -6379,6 +6422,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", @@ -6518,6 +6566,56 @@ "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" + } + } + } + }, + "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", @@ -6534,11 +6632,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", @@ -6555,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", @@ -6649,15 +6756,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", @@ -6780,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", @@ -6875,6 +6986,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", @@ -6908,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", @@ -7105,6 +7228,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", @@ -7255,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", @@ -7392,6 +7542,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", @@ -7476,12 +7635,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", @@ -7547,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", @@ -7569,56 +7728,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", @@ -7646,18 +7755,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", @@ -7820,6 +7917,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..973fb2086f 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,17 +314,22 @@ } }, "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", "@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", + "@vscode/test-electron": "^2.3.9", "axios": "^1.4.0", "changelog-machine": "^1.0.2", "eslint": "^8.16.0", @@ -260,12 +340,12 @@ "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", "ts-node": "^10.8.0", "typescript": "^4.7.2", - "vscode-test": "^1.6.1", "webpack": "^5.71.1", "webpack-cli": "^4.9.2" } 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/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/clones/clonesGenerator.ts b/src/icons/generator/clones/clonesGenerator.ts new file mode 100644 index 0000000000..87ce9bfbfd --- /dev/null +++ b/src/icons/generator/clones/clonesGenerator.ts @@ -0,0 +1,191 @@ +import { + CustomClone, + FileIconClone, + FileIcons, + FolderIconClone, + FolderTheme, + IconConfiguration, + IconJsonOptions, +} from '../../../models'; +import { + Variant, + clearCloneFolder, + getCloneData, + isFolder, +} from './utils/cloneData'; +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, based on the user's provided configurations. + */ +export function customClonesIcons( + config: IconConfiguration, + options: IconJsonOptions +): IconConfiguration { + clearCloneFolder(hasCustomClones(options)); + + 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 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 cloneCfg = createIconClone(clone, config, hash); + clonedIconsConfig = merge(clonedIconsConfig, cloneCfg); + }); + + 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 ( + (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 + */ +function createIconClone( + cloneOpts: FolderIconClone | FileIconClone, + config: IconConfiguration, + hash: string +): IconConfiguration { + // get clones to be created + const clones = getCloneData(cloneOpts, config, clonesFolder, hash); + if (!clones) { + return {}; + } + + const clonesConfig = createCloneConfig(); + + clones.forEach((clone) => { + try { + // generates the new icon content (svg) + const content = cloneIcon(clone.base.path, clone.color, hash); + + 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/utils/cloneData.ts b/src/icons/generator/clones/utils/cloneData.ts new file mode 100644 index 0000000000..8143bfc703 --- /dev/null +++ b/src/icons/generator/clones/utils/cloneData.ts @@ -0,0 +1,268 @@ +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 */ +export 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, + subFolder: string, + hash: string, + ext?: 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, 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}${subFolder}${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.File, + 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, + subFolder: string, + ext = '.svg' +): IconData { + const name = getIconName(cloneOpts.name, base); + const clonePath = join(dirname(base.path), subFolder, `${name}${hash}${ext}`); + + return { + variant: base.variant, + type: base.type, + path: clonePath, + }; +} + +/** returns path, type and variant for the base folder icons to be cloned */ +function getFolderIconBaseData( + clone: FolderIconClone, + config: IconConfiguration +): IconData[] | undefined { + const icons = []; + const folderBase = + clone.base === 'folder' + ? 'folder' + : clone.base.startsWith('folder-') + ? clone.base + : `folder-${clone.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) { + icons.push({ + type: Type.Folder, + variant: Variant.Base, + path: resolvePath(base), + }); + + 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; + } + + 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, + subFolder: string, + ext = '.svg' +): IconData { + const name = getIconName(cloneOpts.name, base); + const path = join(dirname(base.path), subFolder, `${name}${hash}${ext}`); + 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 suffix = ''; + + if (data.type === Type.Folder) { + prefix = + baseName === 'folder' + ? '' + : baseName.startsWith('folder-') + ? '' + : 'folder-'; + + switch (data.variant) { + case Variant.Base: + break; + case Variant.Open: + suffix = openedFolder; + break; + case Variant.Light: + suffix = lightColorFileEnding; + break; + case Variant.LightOpen: + suffix = `${openedFolder}${lightColorFileEnding}`; + break; + } + } else { + suffix = data.variant === Variant.Light ? lightColorFileEnding : ''; + } + + return `${prefix}${baseName}${suffix}`; +} diff --git a/src/icons/generator/clones/utils/cloning.ts b/src/icons/generator/clones/utils/cloning.ts new file mode 100644 index 0000000000..7af149453e --- /dev/null +++ b/src/icons/generator/clones/utils/cloning.ts @@ -0,0 +1,119 @@ +import { readFileSync } from 'fs'; +import { INode, parseSync, stringify } from 'svgson'; +import { 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, + filter = true +) { + if (node.attributes['mit-no-recolor'] !== 'true' || !filter) { + callback(node); + + if (node.children) { + node.children.forEach((child) => traverse(child, callback, filter)); + } + } +} + +/** Reads an icon from the file system and returns its content. */ +export function readIcon(path: string, hash: string): string { + try { + return readFileSync(path, 'utf8'); + } catch (error) { + const unhashedPath = path.replace(hash, ''); + return readFileSync(unhashedPath, 'utf8'); + } +} + +/** Clones an icon and changes its colors according to the clone options. */ +export function cloneIcon(path: string, color: string, hash = ''): string { + const baseContent = readIcon(path, hash); + const svg = parseSync(baseContent); + const replacements = replacementMap(color, getColorList(svg)); + replaceColors(svg, replacements); + 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); + } + return {}; +} + +/** Parses the style attribute of an SVG node. */ +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; +} + +/** 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 + 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'] + )!; + } + } + }); +} + +/** Creates a clone configuration with empty light object. */ +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 new file mode 100644 index 0000000000..15d4abd2a1 --- /dev/null +++ b/src/icons/generator/clones/utils/color/colors.ts @@ -0,0 +1,129 @@ +import { INode } from 'svgson'; +import { getStyle, traverse } from '../cloning'; +import chroma, { Color, valid } from 'chroma-js'; +import { + closerMaterialColorTo, + getMaterialColorByKey, +} from './materialPalette'; + +/** Get all the colors used in the SVG node as a `Set` list. **/ +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 svg 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; +} + +/** given a set of colors, orders them from dark to light. **/ +export function orderDarkToLight(colors: Set) { + const colorArray = Array.from(colors); + return colorArray.sort((a, b) => { + // 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) { + return false; + } + return valid(color); +} + +/** + * 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)) { + // 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); + + // 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]); + 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); + } + + return replacement; +} diff --git a/src/icons/generator/clones/utils/color/materialPalette.ts b/src/icons/generator/clones/utils/color/materialPalette.ts new file mode 100644 index 0000000000..f5c4db24b4 --- /dev/null +++ b/src/icons/generator/clones/utils/color/materialPalette.ts @@ -0,0 +1,297 @@ +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; +} + +/** + * Given a color, returns the closest material color from the + * material palette. + */ +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) => ({ + // calculate the distance between the color and the palette color + distance: deltaE(paletteColor, color), + color: paletteColor, + })) + .sort((a, b) => a.distance - b.distance); + + return distances[0].color; +} 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/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..1e8645c476 100644 --- a/src/icons/generator/jsonGenerator.ts +++ b/src/icons/generator/jsonGenerator.ts @@ -26,6 +26,10 @@ import { validateOpacityValue, validateSaturationValue, } from './index'; +import { + customClonesIcons, + generateConfiguredClones, +} from './clones/clonesGenerator'; /** * Generate the complete icon configuration object that can be written as JSON file. @@ -73,7 +77,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 +135,18 @@ export const createIconFile = ( setIconSaturation(options); } renameIconFiles(iconJsonPath, options); + + // 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); } @@ -193,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; } 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[]; +} 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. */ 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; }); diff --git a/src/scripts/preview/index.ts b/src/scripts/preview/index.ts index c30c5afdc4..74021eed69 100644 --- a/src/scripts/preview/index.ts +++ b/src/scripts/preview/index.ts @@ -9,6 +9,8 @@ const filterDuplicates = (icons: string[]) => { const basicFileIcons = filterDuplicates( fileIcons.icons + // remove icons that are clones + .filter((i) => i.clone === undefined) .map((i) => i.name) // merge language icons .concat(languageIcons.map((i) => i.icon.name)) @@ -19,7 +21,12 @@ const folderThemes = filterDuplicates( .map((theme) => { const folders = []; if (theme.icons && theme.icons.length > 0) { - folders.push(...theme.icons.map((i) => i.name)); + folders.push( + ...theme.icons + // remove icons that are clones + .filter((i) => i.clone === undefined) + .map((i) => i.name) + ); } return [...folders]; }) 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 { diff --git a/src/test/spec/icons/cloning.spec.ts b/src/test/spec/icons/cloning.spec.ts new file mode 100644 index 0000000000..5dbfb20841 --- /dev/null +++ b/src/test/spec/icons/cloning.spec.ts @@ -0,0 +1,730 @@ +import { + lightColorFileEnding, + openedFolder, + iconFolderPath, + clonesFolder, +} from '../../../icons/generator/constants'; +import { + getCloneData, + resolvePath, + Type, + Variant, +} from '../../../icons/generator/clones/utils/cloneData'; +import { IconConfiguration } from '../../../models'; +import { + FileIconClone, + FolderIconClone, + IconJsonOptions, +} from '../../../models/icons/iconJsonOptions'; +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 as palette, +} 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(..)', () => { + 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, palette['red-600']); + }); + + it('should return the same color if it is already a material color', () => { + const color = palette['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(..)', () => { + 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); + }); + }); + }); + + 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; +} + +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); + }); +}); 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 = ` + + + + + + + + + + +`; 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); + }); });