├── thumbnail.png
├── icons
├── mfixx.woff2
├── octicons.woff2
├── devopicons.woff2
├── file-icons.woff2
└── fontawesome.woff2
├── .github
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .editorconfig
├── scripts
├── content-types.xml
├── extension.xml
├── missing-filenames.txt
└── update.mjs
├── LICENSE.md
├── Makefile
├── CONTRIBUTING.md
├── README.md
├── package.json
└── CHANGELOG.md
/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/file-icons/vscode/HEAD/thumbnail.png
--------------------------------------------------------------------------------
/icons/mfixx.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/file-icons/vscode/HEAD/icons/mfixx.woff2
--------------------------------------------------------------------------------
/icons/octicons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/file-icons/vscode/HEAD/icons/octicons.woff2
--------------------------------------------------------------------------------
/icons/devopicons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/file-icons/vscode/HEAD/icons/devopicons.woff2
--------------------------------------------------------------------------------
/icons/file-icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/file-icons/vscode/HEAD/icons/file-icons.woff2
--------------------------------------------------------------------------------
/icons/fontawesome.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/file-icons/vscode/HEAD/icons/fontawesome.woff2
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | Thumbs.db
3 | Desktop.ini
4 | node_modules
5 | .nyc_output
6 | ._*
7 | .\#*
8 | \#*\#
9 | *~
10 | *.log
11 | *.lock
12 | [._]*.s[a-v][a-z]
13 | [._]*.sw[a-p]
14 | [._]s[a-v][a-z]
15 | [._]sw[a-p]
16 |
17 | # Project-specific
18 | *.vsix
19 | *.zip
20 | /defs
21 | /tmp
22 | /version
23 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | trim_trailing_whitespace = false
8 | indent_style = tab
9 |
10 | [*.{yaml,yml}]
11 | indent_style = space
12 | indent_size = 4
13 |
14 | [{COMMIT_EDITMSG,MERGE_MSG}]
15 | trim_trailing_whitespace = true
16 | max_line_length = 72
17 | indent_style = space
18 | indent_size = 4
19 |
--------------------------------------------------------------------------------
/scripts/content-types.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017-2019 Daniel Brooker
2 | Copyright (c) 2019-2023 John Gardner
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining
5 | a copy of this software and associated documentation files (the
6 | "Software"), to deal in the Software without restriction, including
7 | without limitation the rights to use, copy, modify, merge, publish,
8 | distribute, sublicense, and/or sell copies of the Software, and to
9 | permit persons to whom the Software is furnished to do so, subject to
10 | the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PATH := ./node_modules/.bin:$(PATH)
2 |
3 | all: install update lint
4 |
5 |
6 | # Install or download project dependencies
7 | install: node_modules defs
8 |
9 | defs:
10 | test -d $@ || git clone \
11 | --branch master \
12 | --single-branch \
13 | --filter=tree:0 \
14 | 'https://github.com/file-icons/atom.git' $@
15 |
16 | node_modules:
17 | npm install --legacy-peer-deps .
18 |
19 |
20 | # Pull the latest updates from upstream
21 | update: defs
22 | cd $^ && git pull -f origin master
23 | node scripts/update.mjs $^ ./icons
24 |
25 |
26 | # Check source for errors and style violations
27 | lint: node_modules
28 | eslint .
29 |
30 | .PHONY: lint
31 |
32 |
33 |
34 | # Package a VSIX bundle for uploading to VSCode's marketplace thingie
35 | release: tmp
36 | cp scripts/content-types.xml 'tmp/[Content_Types].xml'
37 | grep -e version package.json | tr -d '", \t' | cut -d: -f2 > version
38 | sed -e "s/%%VERSION%%/`cat version`/g" scripts/extension.xml > tmp/extension.vsixmanifest
39 | mkdir tmp/extension
40 | cp -r CHANGELOG.md LICENSE.md README.md icons package.json thumbnail.png tmp/extension
41 | vsix="file-icons.file-icons-`cat version`.vsix"; \
42 | cd tmp && zip -r "$$vsix" *
43 | mv tmp/*.vsix .
44 | rm -rf version tmp
45 | open 'https://marketplace.visualstudio.com/manage/publishers/file-icons'
46 |
47 | tmp:; mkdir $@
48 |
49 |
50 | # Wipe generated files and build artefacts
51 | clean:
52 | rm -rf tmp version
53 |
54 | .PHONY: clean
55 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Adding a new icon
2 | =================
3 |
4 | Please [submit a request][1] to the `file-icons/icons` repository. Make sure the
5 | icon isn't already included in one of the other icon-fonts first:
6 |
7 | * [**Devicons**](https://github.com/file-icons/DevOpicons/blob/master/charmap.md)
8 | * [**Mfizz**](https://github.com/file-icons/MFixx/blob/master/charmap.md)
9 | * [**Octicons**](https://octicons.github.com/)
10 |
11 |
12 | Adding support for a new filetype
13 | =================================
14 |
15 | This package pulls its icon-to-filetype mappings from the [`file-icons/atom`][2]
16 | repository using an [update script][3]. The `*-icon-theme.json` files themselves
17 | are auto-generated and shouldn't be edited by hand. Please add new extensions to
18 | the upstream [`config.cson`][4] file instead.
19 |
20 | If `config.cson` already lists the desired extension, then it's likely a problem
21 | with the [update script][3]. See below.
22 |
23 |
24 | Fixing a missing filetype
25 | =========================
26 |
27 | The [update script][3] is unable to generate filetype mappings for patterns with
28 | an indefinite number of variants, such as `/^foo(.*)\.bar/`. In cases like this,
29 | a workaround is to add a new entry to [`missing-filenames.txt`][5]:
30 |
31 | ~~~text
32 | filename.extension
33 | ~~~
34 |
35 | A caveat of this solution is that only fixed-length strings can be added; a more
36 | intuitive system for icon-mapping will eventually be developed in the future.
37 |
38 |
39 |
40 | [1]: https://github.com/file-icons/icons/issues/new
41 | [2]: https://github.com/file-icons/atom
42 | [3]: ./scripts/update.mjs
43 | [4]: https://github.com/file-icons/atom/blob/master/config.cson
44 | [5]: ./scripts/missing-filenames.txt
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | File Icons for VSCode
2 | =====================
3 |
4 | [![Latest package version][VSC-version]][VSC-link]
5 |
6 | File-specific icons in VSCode for improved visual grepping.
7 |
8 | ![Icon previews][]
9 |
10 | * Vast array of icons for most languages and frameworks
11 | * Comes in coloured and colourless flavours
12 | * Uses same icon set as [file-icons for Atom](https://github.com/file-icons/atom)
13 |
14 |
15 | Installation
16 | ------------
17 | Open the [command palette] and run `ext install file-icons`.
18 |
19 | Alternatively, install through the command-line:
20 |
21 | code --install-extension file-icons.file-icons
22 |
23 | Once installed, activate the theme by selecting `File Icons` from the **File Icon Theme** menu.
24 |
25 |
26 | Icon fonts
27 | ----------
28 |
29 | * [**File-Icons**](https://github.com/file-icons/icons/blob/master/charmap.md)
30 | * [**FontAwesome**](https://fontawesome.com/v4/cheatsheet/)
31 | * [**Mfizz**](https://github.com/file-icons/MFixx/blob/master/charmap.md)
32 | * [**Devicons**](https://github.com/file-icons/DevOpicons/blob/master/charmap.md)
33 | * [**Octicons**](https://octicons.github.com/)
34 |
35 |
36 | New icons
37 | ---------
38 | Please request new icons over at [`file-icons/icons`](https://github.com/file-icons/icons).
39 |
40 |
41 |
42 | [VSC-version]: https://img.shields.io/visual-studio-marketplace/v/file-icons.file-icons?color=4c1&label=Visual%20Studio%20Marketplace
43 | [VSC-link]: https://marketplace.visualstudio.com/items?itemName=file-icons.file-icons
44 | [command palette]: https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette
45 | [Icon previews]: https://raw.githubusercontent.com/file-icons/atom/6714706f268e257100e03c9eb52819cb97ad570b/preview.png
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "file-icons",
3 | "displayName": "file-icons",
4 | "description": "File-specific icons in VSCode for improved visual grepping.",
5 | "version": "1.1.0",
6 | "publisher": "file-icons",
7 | "license": "MIT",
8 | "author": "Daniel Brooker ",
9 | "maintainers": [{
10 | "name": "John Gardner",
11 | "email": "gardnerjohng@gmail.com",
12 | "url": "https://github.com/Alhadis"
13 | }],
14 | "engines": {
15 | "node": ">=15",
16 | "vscode": "^1.5.0"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/file-icons/vscode"
21 | },
22 | "bugs": "https://github.com/file-icons/vscode/issues",
23 | "homepage": "https://github.com/file-icons/vscode",
24 | "icon": "./thumbnail.png",
25 | "galleryBanner": {
26 | "color": "#15191B",
27 | "theme": "dark"
28 | },
29 | "categories": [
30 | "Themes",
31 | "Other"
32 | ],
33 | "keywords": [
34 | "icons",
35 | "filetypes",
36 | "file-icons",
37 | "icon-theme",
38 | "theme"
39 | ],
40 | "contributes": {
41 | "iconThemes": [{
42 | "id": "file-icons",
43 | "label": "File Icons",
44 | "path": "./icons/file-icons-icon-theme.json"
45 | },{
46 | "id": "file-icons-colourless",
47 | "label": "File Icons (Colourless)",
48 | "path": "./icons/file-icons-colourless-icon-theme.json"
49 | }]
50 | },
51 | "scripts": {
52 | "import": "make update",
53 | "lint": "make lint"
54 | },
55 | "eslintConfig": {
56 | "extends": "@alhadis",
57 | "overrides": [{
58 | "files": ["scripts/*.mjs"],
59 | "rules": {"no-irregular-whitespace": 0}
60 | }],
61 | "rules": {"camelcase": 0}
62 | },
63 | "eslintIgnore": ["defs"],
64 | "devDependencies": {
65 | "@alhadis/eslint-config": "^2.3.4",
66 | "@babel/eslint-parser": "^7.15.0",
67 | "cson": "^7.20.0",
68 | "eslint": "^7.32.0",
69 | "eslint-plugin-import": "^2.24.2",
70 | "genex": "v1.1.0",
71 | "less": "^2.7.3"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/scripts/extension.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | file-icons
6 | File-specific icons in VSCode for improved visual grepping.
7 | icons,filetypes,file-icons,icon-theme,theme
8 | Themes,Other
9 | Public
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | extension/LICENSE.md
24 | extension/thumbnail.png
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Change Log
2 | ==========
3 |
4 | **NOTE:** Since 1.0.10 (March 2018), all notable changes are being tracked in the change-logs of
5 | [`file-icons/atom`](https://github.com/file-icons/atom/blob/master/CHANGELOG.md).
6 |
7 | Only bug-fixes and updates specific to VSCode will continue to be documented here.
8 |
9 |
10 | ## 1.0.10
11 | New icons and extensions from upstream
12 |
13 | ## 1.0.6
14 | New icons and extensions from upstream
15 | - [Fix] Dockerfile, fileNames needed to be lower cased, thanks @costela
16 |
17 | ## 1.0.5
18 | New icons and extensions from upstream
19 |
20 | ## 1.0.4
21 | New icons and extensions from upstream
22 | Lots # TODO: add list here
23 |
24 | ## 1.0.3
25 | New icons and extensions from upstream
26 | - [Support] AngelScript (`.acs`, `.angelscript`), Bazel (`.bzl`, `BUILD`, `WORKSPACE`), BEM (`.bemjson.js`), Caddy (`Caddyfile`), DeviceTree (`.dts`, `.dtsi`), Franca (`.fdl`, `.fidl`, `.fdepl`), Jison (`.jison`, `.jisonlex`), Meson (`meson.build`, `meson_options.txt`), MiniZinc (`.mzn`, `.dzn`), Miranda (`.m`), Nanoc (`.nanoc.yaml`), P4 (`.p4`), Watchman (`.watchmanconfig`, `watchman.json`), ESDoc (`esdoc.json`), JSONT (`.jsont`), Twine (`.tw`), and Phoenix (`phoenix.{ex,js}`)
27 | - [Support] Generic code (`.bc`, `.dtd`, `.fo`, `.fidl`, `.stellaris`, `.spthy`, `.wlp4`), Markdown (`.gfm`, `.pfm`), PostCSS (`.postcssrc.{js,json,yaml}`)
28 | - [Change]
29 |
30 | ## 1.0.2
31 | * [Support] .cfignore as a default
32 |
33 | ## 1.0.1
34 | New icons and extensions from upstream
35 | * [Support] ABIF (`.abif`, `.ab1`, `.fsa`), EJS (`.ejs`), Hoplon (`.hl`), KiCad (`.kicad_pcb`), Mercurial (`.hg`), PlatformIO (`platformio.ini`), Polymer (`polymer.json`), Rhino3D (`.3dm`, `.rvb`), VirtualBox (`.vbox`), VMware (`.vmdk,` `.nvram`, `.vmsd`, `.vmsn`, `.vmss`, `.vmtm`, `.vmx`, `.vmxf`)
36 | * [Support] LookML (`.lkml`), SQL (`.hql`)
37 | * See https://github.com/file-icons/atom/releases/tag/v2.0.15 for full list
38 |
39 | ## 1.0.0
40 | Publish
41 |
42 | ## 0.0.5
43 | New icons and extensions from upstream
44 | * [Support] 33 new icons: Ansible, Aurelia, bitHound, Brunch, Buck, Bundler, CakePHP (updated logo), Chef, COBOL, CodeKit, Delphi, Doclets, DoneJS, Drone, GitLab, HaxeDevelop, Jasmine, Jest, KitchenCI, Lerna, Lime, Microsoft InfoPath, Nuclide, Octave, PHPUnit, Redux, RSpec, Sequelize, Shipit, Shippable, Swagger, Template Toolkit, Twig
45 | * [Support] Blade (.blade), Erlang (Emakefile), GraphViz (.plantuml, .iuml, .puml, .pu), Jekyll (_config.yml, .nojekyll), MkDocs (mkdocs.yml), Paket (Various paket.* configs, .paket folders), Process IDs (.pid), Puppet (.epp), Tcl (.exp), Terminal (.profile), Visual Studio (.vscodeignore, .vsix, .vssettings.json, .vscode folders), Yarn (.yarnrc, .yarn-metadata.json, .yarn-integrity, .yarnclean), WeChat (.wxml, .wxss)
46 | * See https://github.com/file-icons/atom/releases/tag/v2.0.14 for full list
47 |
48 | ## 0.0.4
49 | * [Fix] Remove BUILD file python icon, conflicts with build.*
50 | * [Support] Add colour to .git* icons
51 |
52 | ## 0.0.3
53 | * [Support] .gitignore and .gitattributes
54 | * [Support] Varying folder icons
55 | * [Fix] Colour variations of icons
56 |
57 | ## 0.0.2
58 | * [Fix] Devicons
59 | * [Fix] Updated Octicons Font
60 | * [Support] Added Atom's default icons definitions
61 | * [Feature] Colourless icon theme
62 |
63 | ## 0.0.1
64 | - Initial release
65 |
--------------------------------------------------------------------------------
/scripts/missing-filenames.txt:
--------------------------------------------------------------------------------
1 | babel.config.esm.js
2 | babel.config.js
3 | babel.config.json
4 | .cfignore
5 | .eslintrc.cjs
6 | .eslintrc.js
7 | .eslintrc.json
8 | .eslintrc.mjs
9 | .eslintrc.yaml
10 | .eslintrc.yml
11 | .gitattributes
12 | .git-blame-ignore-revs
13 | .gitkeep
14 | .gitmodules
15 | .global.gitattributes
16 | .global.gitignore
17 | gruntfile.babel.cjs
18 | gruntfile.babel.coffee
19 | gruntfile.babel.js
20 | gruntfile.babel.jsx
21 | gruntfile.babel.litcoffee
22 | gruntfile.babel.mjs
23 | gruntfile.ts
24 | gruntfile.tsx
25 | gulpfile.babel.cjs
26 | gulpfile.babel.coffee
27 | gulpfile.babel.js
28 | gulpfile.babel.jsx
29 | gulpfile.babel.litcoffee
30 | gulpfile.babel.mjs
31 | gulpfile.esm.cjs
32 | gulpfile.esm.coffee
33 | gulpfile.esm.js
34 | gulpfile.esm.jsx
35 | gulpfile.esm.litcoffee
36 | gulpfile.esm.mjs
37 | .jestrc
38 | .jestrc.cjs
39 | .jestrc.js
40 | .jestrc.json
41 | .jestrc.mjs
42 | postcss.config.cjs
43 | postcss.config.js
44 | postcss.config.mjs
45 | .stylelintrc.js
46 | .stylelintrc.json
47 | .stylelintrc.yaml
48 | .stylelintrc.yml
49 | tsconfig.base.json
50 | tsconfig.build.json
51 | tsconfig.es5.json
52 | tsconfig.eslint.json
53 | tsconfig.esm.json
54 | tsconfig.node.json
55 | tsconfig.spec.json
56 | webpack.analyze.js
57 | webpack.base.conf.cjs
58 | webpack.base.conf.coffee
59 | webpack.base.conf.js
60 | webpack.base.conf.mjs
61 | webpack.base.conf.ts
62 | webpack.common.cjs
63 | webpack.common.coffee
64 | webpack.common.js
65 | webpack.common.mjs
66 | webpack.common.ts
67 | webpack.config.babel.cjs
68 | webpack.config.babel.coffee
69 | webpack.config.babel.js
70 | webpack.config.babel.mjs
71 | webpack.config.babel.ts
72 | webpack.config.base.babel.cjs
73 | webpack.config.base.babel.coffee
74 | webpack.config.base.babel.js
75 | webpack.config.base.babel.mjs
76 | webpack.config.base.babel.ts
77 | webpack.config.base.cjs
78 | webpack.config.base.coffee
79 | webpack.config.base.js
80 | webpack.config.base.mjs
81 | webpack.config.base.ts
82 | webpack.config.cjs
83 | webpack.config.coffee
84 | webpack.config.common.babel.cjs
85 | webpack.config.common.babel.coffee
86 | webpack.config.common.babel.js
87 | webpack.config.common.babel.mjs
88 | webpack.config.common.babel.ts
89 | webpack.config.common.cjs
90 | webpack.config.common.coffee
91 | webpack.config.common.js
92 | webpack.config.common.mjs
93 | webpack.config.common.ts
94 | webpack.config.dev.babel.cjs
95 | webpack.config.dev.babel.coffee
96 | webpack.config.dev.babel.js
97 | webpack.config.dev.babel.mjs
98 | webpack.config.dev.babel.ts
99 | webpack.config.dev.cjs
100 | webpack.config.dev.coffee
101 | webpack.config.development.babel.cjs
102 | webpack.config.development.babel.coffee
103 | webpack.config.development.babel.js
104 | webpack.config.development.babel.mjs
105 | webpack.config.development.babel.ts
106 | webpack.config.development.cjs
107 | webpack.config.development.coffee
108 | webpack.config.development.js
109 | webpack.config.development.mjs
110 | webpack.config.development.ts
111 | webpack.config.dev.js
112 | webpack.config.dev.mjs
113 | webpack.config.dev.ts
114 | webpack.config.js
115 | webpack.config.mjs
116 | webpack.config.prod.babel.cjs
117 | webpack.config.prod.babel.coffee
118 | webpack.config.prod.babel.js
119 | webpack.config.prod.babel.mjs
120 | webpack.config.prod.babel.ts
121 | webpack.config.prod.cjs
122 | webpack.config.prod.coffee
123 | webpack.config.prod.js
124 | webpack.config.prod.mjs
125 | webpack.config.prod.ts
126 | webpack.config.production.babel.cjs
127 | webpack.config.production.babel.coffee
128 | webpack.config.production.babel.js
129 | webpack.config.production.babel.mjs
130 | webpack.config.production.babel.ts
131 | webpack.config.production.cjs
132 | webpack.config.production.coffee
133 | webpack.config.production.js
134 | webpack.config.production.mjs
135 | webpack.config.production.ts
136 | webpack.config.staging.babel.cjs
137 | webpack.config.staging.babel.coffee
138 | webpack.config.staging.babel.js
139 | webpack.config.staging.babel.mjs
140 | webpack.config.staging.babel.ts
141 | webpack.config.staging.cjs
142 | webpack.config.staging.coffee
143 | webpack.config.staging.js
144 | webpack.config.staging.mjs
145 | webpack.config.staging.ts
146 | webpack.config.test.babel.cjs
147 | webpack.config.test.babel.coffee
148 | webpack.config.test.babel.js
149 | webpack.config.test.babel.mjs
150 | webpack.config.test.babel.ts
151 | webpack.config.test.cjs
152 | webpack.config.test.coffee
153 | webpack.config.test.js
154 | webpack.config.test.mjs
155 | webpack.config.test.ts
156 | webpack.config.ts
157 | webpack.dev.cjs
158 | webpack.dev.coffee
159 | webpack.dev.conf.cjs
160 | webpack.dev.conf.coffee
161 | webpack.dev.conf.js
162 | webpack.dev.conf.mjs
163 | webpack.dev.conf.ts
164 | webpack.dev.js
165 | webpack.dev.mjs
166 | webpack.dev.ts
167 | webpack.main.config.cjs
168 | webpack.main.config.coffee
169 | webpack.main.config.js
170 | webpack.main.config.mjs
171 | webpack.main.config.ts
172 | webpack.mix.cjs
173 | webpack.mix.coffee
174 | webpack.mix.js
175 | webpack.mix.mjs
176 | webpack.mix.ts
177 | webpack.plugins.cjs
178 | webpack.plugins.coffee
179 | webpack.plugins.js
180 | webpack.plugins.mjs
181 | webpack.plugins.ts
182 | webpack.prod.cjs
183 | webpack.prod.coffee
184 | webpack.prod.conf.cjs
185 | webpack.prod.conf.coffee
186 | webpack.prod.conf.js
187 | webpack.prod.conf.mjs
188 | webpack.prod.conf.ts
189 | webpack.prod.js
190 | webpack.prod.mjs
191 | webpack.prod.ts
192 | webpack.renderer.config.cjs
193 | webpack.renderer.config.coffee
194 | webpack.renderer.config.js
195 | webpack.renderer.config.mjs
196 | webpack.renderer.config.ts
197 | webpack.rules.cjs
198 | webpack.rules.coffee
199 | webpack.rules.js
200 | webpack.rules.mjs
201 | webpack.rules.ts
202 | webpack.test.conf.cjs
203 | webpack.test.conf.coffee
204 | webpack.test.conf.js
205 | webpack.test.conf.mjs
206 | webpack.test.conf.ts
207 |
--------------------------------------------------------------------------------
/scripts/update.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import Less from "less";
4 | import Genex from "genex";
5 | import {copyFileSync, linkSync, lstatSync, statSync, readFileSync, unlinkSync, writeFileSync} from "fs";
6 | import {basename, dirname, join, resolve} from "path";
7 | import {fileURLToPath} from "url";
8 | import {inspect, isDeepStrictEqual} from "util";
9 | import assert from "assert";
10 |
11 | const $0 = fileURLToPath(import.meta.url);
12 | const root = dirname($0).replace(/\/scripts$/i, "");
13 | const isObj = obj => "object" === typeof obj && null !== obj;
14 | const isKey = key => "string" === typeof key || "symbol" === typeof key;
15 | const isEnt = obj => Array.isArray(obj) && 2 === obj.length && isKey(obj[0]);
16 | const isOwn = Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty);
17 |
18 | const source = resolve(process.argv[2] || join(root, "..", "atom"));
19 | const output = resolve(process.argv[3] || join(root, "icons"));
20 | const missing = resolve(process.argv[4] || join(root, "scripts", "missing-filenames.txt"));
21 | const iconDB = join(source, "lib", "icons", ".icondb.js");
22 | const icons = join(source, "styles", "icons.less");
23 | const fonts = join(source, "styles", "fonts.less");
24 | const colours = join(source, "styles", "colours.less");
25 | assertDir(source, output);
26 | assertFile(fonts, colours);
27 |
28 | export default Promise.all([
29 | loadDB(iconDB, missing),
30 | loadIcons(icons),
31 | loadFonts(fonts),
32 | loadColours(colours),
33 | ]).then(async ([iconDB, icons, fonts, colours]) => {
34 | const count = updateFonts(fonts, output);
35 | console.info(count ? `${count} font(s) updated` : "Fonts already up-to-date");
36 |
37 | fonts.push({
38 | id: "octicons regular",
39 | src: [{path: join(output, "octicons.woff2"), format: "woff2"}],
40 | weight: "normal",
41 | style: "normal",
42 | size: "100%",
43 | });
44 | for(const font of fonts)
45 | font.id = icons.fonts[font.id].id || font.id;
46 |
47 | // Icons provided by Octicons and/or Atom's core stylesheets
48 | const defaultIcons = {
49 | _file: {fontId: "octicons", fontCharacter: "\\f011", fontSize: "114%"},
50 | _folder: {fontId: "octicons", fontCharacter: "\\f016", fontSize: "114%"},
51 | _repo: {fontId: "octicons", fontCharacter: "\\f001", fontSize: "114%"},
52 | };
53 | const unlistedIcons = {
54 | "circuit-board": {fontId: "octicons", fontCharacter: "\\f0d6", fontSize: "114%"},
55 | mail: {fontId: "octicons", fontCharacter: "\\f03b", fontSize: "114%"},
56 | paintcan: {fontId: "octicons", fontCharacter: "\\f0d1", fontSize: "114%"},
57 | pdf: {fontId: "octicons", fontCharacter: "\\f014", fontSize: "114%"},
58 | star: {fontId: "octicons", fontCharacter: "\\f02a", fontSize: "114%"},
59 | text: {fontId: "octicons", fontCharacter: "\\f011", fontSize: "114%"},
60 | };
61 | ({icons} = icons);
62 | icons = {...unlistedIcons, ...icons};
63 |
64 | // Truncate font paths to output directory
65 | for(const font of fonts)
66 | font.src.forEach(src => src.path = "./" + basename(src.path));
67 |
68 | // Alphabetise font-families, but keep File-Icons at the front of array
69 | fonts = fonts.sort((a, b) => a.id.toLowerCase().localeCompare(b.id.toLowerCase()));
70 | const index = fonts.findIndex(font => "fi" === font.id);
71 | if(-1 !== index)
72 | fonts.unshift(...fonts.splice(index, 1));
73 | else throw new ReferenceError("Failed to locate font with ID 'fi'");
74 |
75 | const colouredTheme = buildTheme({icons, fonts, colours, iconDB});
76 | colouredTheme.iconDefinitions = {...defaultIcons, ...colouredTheme.iconDefinitions};
77 | saveJSON(colouredTheme, join(output, "file-icons-icon-theme.json"));
78 |
79 | const uncolouredTheme = desaturate(colouredTheme, colours);
80 | saveJSON(uncolouredTheme, join(output, "file-icons-colourless-icon-theme.json"));
81 |
82 | }).catch(error => {
83 | console.error(error);
84 | process.exit(1);
85 | });
86 |
87 |
88 | // Section: Icon Database {{{1
89 |
90 | /**
91 | * Generate an icon-theme in the format required by VS Code.
92 | * @param {Object} source
93 | * @param {Array} source.iconDB
94 | * @param {Object} source.icons
95 | * @param {Object} source.fonts
96 | * @param {Object} source.colours
97 | * @param {String} [source.prefix="_"]
98 | * @return {IconTheme}
99 | * @internal
100 | */
101 | function buildTheme({iconDB, icons, fonts, colours, prefix = "_"} = {}){
102 | const theme = {
103 | __proto__: null,
104 | fonts,
105 | file: prefix + "file",
106 | folder: prefix + "folder",
107 | rootFolder: prefix + "repo",
108 | iconDefinitions: {},
109 | fileExtensions: {},
110 | fileNames: {},
111 | folderNames: {},
112 | languageIds: {},
113 | light: {},
114 | };
115 |
116 | const getColourValue = colour => {
117 | if(!colour) return "#000000";
118 | const index = colour.indexOf("-");
119 | const brightness = colour.slice(0, index);
120 | const name = colour.slice(index + 1);
121 | const value = colours[name]?.[brightness];
122 | if(null == value)
123 | throw new ReferenceError(`No such colour ${colour}`);
124 | return value;
125 | };
126 |
127 | const [directoryIcons, fileIcons] = iconDB;
128 | for(const iconList of [directoryIcons, fileIcons])
129 | for(let [
130 | icon,
131 | colours,
132 | match,,
133 | matchPath,,
134 | scope,
135 | language,
136 | signature,
137 | ] of iconList[0]){
138 | if(matchPath || !(match instanceof RegExp)) continue;
139 |
140 | // HACK
141 | if(/^\.atom-socket-.+\.\d$/.source === match.source)
142 | continue;
143 |
144 | // HACK: Conflicting file-extension: `.vh` (V, SystemVerilog)
145 | // Searching GitHub yields mostly Verilog results, so exclude V.
146 | if("v-icon" === icon && /\.vh$/i.source === match.source)
147 | continue;
148 |
149 | // Normalise icon ID: "pdf-icon" => "pdf", "icon-file-text" => "text"
150 | if(icon.startsWith("icon-file-")) icon = icon.slice(10);
151 | else if(icon.startsWith("icon-")) icon = icon.slice(5);
152 | else if(icon.endsWith("-icon")) icon = icon.slice(0, -5);
153 | if(icon.startsWith("_")) icon = icon.slice(1);
154 | validateIcon(icon, icons, fonts);
155 |
156 | // HACK: Manually add scopes to commonly-used generic formats like XML and YAML
157 | signature = String(signature);
158 | if("yaml" === icon && String(match) === String(/\.ya?ml$/i)){
159 | language ||= /^YA?ML$/i;
160 | scope ||= /\.ya?ml$/i;
161 | }
162 | else if(signature === String(/^<\?xml /)){
163 | language ||= /^XML$/i;
164 | scope ||= /^text\.xml$/i;
165 | }
166 | else if(signature === String(/^\xEF\xBB\xBF|^\xFF\xFE/))
167 | scope ||= /^(text\.plain|plain[-_ ]?text|fundamental)$/i;
168 |
169 | // Normalise dark- and light-motif variants
170 | colours = Array.isArray(colours) ? [...colours].slice(0, 2) : [colours];
171 | colours[0] === colours[1] && colours.pop();
172 |
173 | const add = (listName, key) => {
174 | key = key.toLowerCase();
175 | let list = theme[listName];
176 | for(const colour of colours){
177 | const uid = prefix + icon + (colour ? "_" + colour : "");
178 | list[key] = uid;
179 | if(null == theme.iconDefinitions[uid]){
180 | const def = {...icons[icon], fontColor: getColourValue(colour)};
181 | if("#000000" === def.fontColor)
182 | delete def.fontColor;
183 | theme.iconDefinitions[uid] = def;
184 | }
185 | list = theme.light[listName] ??= {};
186 | }
187 | };
188 | try{
189 | match = new RegExp(
190 | match.source
191 | .replace(/^\^stdlib\(\?:-\.\+\)\?/, "^stdlib")
192 | .replace(/(? [...set]).flat()
211 | .map(scope => scope.toLowerCase()
212 | .replace(/^\.+|\.+$/g, "")
213 | .replace(/^(?:source|text)\.+/g, ""));
214 | if(language){
215 | language = parseRegExp(match = new RegExp(
216 | language.source,
217 | language.flags,
218 | ));
219 | langIDs.push(...Object.values(language).map(set => {
220 | set = [...set];
221 | const downcase = set.map(lang => lang.toLowerCase());
222 | const upcase = set.map(lang => lang.toUpperCase());
223 | return set.concat(downcase, upcase);
224 | }).flat());
225 | }
226 | for(const langID of new Set(langIDs))
227 | theme.languageIds[langID] = iconID;
228 | }
229 | }
230 | catch(error){
231 | if(error instanceof RangeError
232 | || error.message.includes("Unsupported lookbehind")){
233 | console.warn("Skipping:", match);
234 | continue;
235 | }
236 | console.warn("Stopped at", match);
237 | throw error;
238 | }
239 | }
240 |
241 | // Make diffs less chaotic by enforcing alphanumeric order
242 | for(const obj of [theme, theme.light].filter(Boolean))
243 | for(const key of "iconDefinitions fileExtensions fileNames folderNames folderNamesExpanded languageIds".split(" ")){
244 | const value = obj[key];
245 | if(isOwn(obj, key) && isObj(value))
246 | obj[key] = sortProps(value);
247 | }
248 | return theme;
249 | }
250 |
251 |
252 | /**
253 | * Generate a colourless version of a coloured icon-theme.
254 | * @param {IconTheme} input
255 | * @param {Object} colours
256 | * @return {IconTheme}
257 | * @internal
258 | */
259 | function desaturate(input, colours){
260 |
261 | // Construct a regex for stripping the trailing colour name/shade
262 | const names = new Set();
263 | for(const [colour, value] of Object.entries(colours))
264 | for(const [luminosity] of Object.entries(value))
265 | names.add([luminosity, colour].join("-"));
266 | const regex = new RegExp(`[-_]?(?:${[...names].join("|")})$`, "i");
267 |
268 | // Start culling
269 | const result = JSON.parse(JSON.stringify(input));
270 | const oldDefs = input.iconDefinitions;
271 | const newDefs = result.iconDefinitions = {__proto__: null};
272 |
273 | for(const [oldID, props] of Object.entries(oldDefs)){
274 | const newID = oldID.replace(regex, "");
275 | delete props.fontColor;
276 |
277 | // Sanity check
278 | if(newID in newDefs)
279 | assert.deepStrictEqual(props, newDefs[newID]);
280 | else newDefs[newID] = props;
281 | }
282 | const remap = from => {
283 | const to = {__proto__: null};
284 | for(let [key, iconID] of Object.entries(from)){
285 | iconID = iconID.replace(regex, "");
286 | assert(iconID in newDefs);
287 | to[key] = iconID;
288 | }
289 | return to;
290 | };
291 | const isEmpty = obj => isObj(obj) && !Object.keys(obj).length;
292 | const cull = (...listNames) => {
293 | for(const listName of listNames){
294 | if(!isObj(result?.light?.[listName])
295 | || !isObj(result[listName])) continue;
296 | const darkIcons = result[listName];
297 | const lightIcons = result.light[listName];
298 | for(const [key, value] of Object.entries(lightIcons)){
299 | if(key in darkIcons && isDeepStrictEqual(darkIcons[key], value))
300 | delete lightIcons[key];
301 | }
302 | if(isEmpty(result.light[listName]))
303 | delete result.light[listName];
304 | }
305 | if(isEmpty(result.light))
306 | delete result.light;
307 | };
308 | for(const context of [result, result.light]){
309 | if(!context) continue;
310 | context.fileExtensions = remap(context.fileExtensions);
311 | context.fileNames = remap(context.fileNames);
312 | context.folderNames = remap(context.folderNames);
313 | }
314 | cull(..."fileExtensions fileNames folderNames".split(" "));
315 | return result;
316 | }
317 |
318 |
319 | /**
320 | * Load a compiled icon database.
321 | *
322 | * @example await loadDB("/path/to/.icondb.js");
323 | * @param {String} from - Path to an `.icondb.js` file.
324 | * @param {String} [filenameList=null]
325 | * Path to a line-delimited list of filenames to include in a matching pattern.
326 | * This is necessary because some compiled regexes are too complex to be easily
327 | * enumerated by {@link parseRegExp}.
328 | * @return {Array}
329 | * @internal
330 | */
331 | async function loadDB(from, filenameList = null){
332 | const {default: db} = await import(resolve(from));
333 | if(!filenameList) return db;
334 |
335 | // Add missing filenames to patterns too complex for `parseRegExp` to handle
336 | const filenames = readFileSync(resolve(filenameList), "utf8")
337 | .split(/\r?\n|\r|\x85|\u2028|\u2029/)
338 | .map((line, index) => (line = line.trim()) ? [index + 1, line] : null)
339 | .filter(Boolean);
340 |
341 | const unmatched = new Set(filenames);
342 | const matched = new Map();
343 |
344 | for(const filename of filenames){
345 | for(const icon of db[1][0]){
346 | if(!icon[4] && icon[2].test(filename[1])){
347 | unmatched.delete(filename);
348 | matched.has(icon) || matched.set(icon, new Set());
349 | matched.get(icon).add(filename);
350 | break;
351 | }
352 | }
353 | }
354 |
355 | // Report any filenames that failed to match any icon whatsoever
356 | const {size} = unmatched;
357 | if(size){
358 | const [prefix, path, dimOn, dimOff] = process.stderr.isTTY
359 | ? ["\x1B[33mWarning:\x1B[39m", `\x1B[4m${filenameList}\x1B[24m`, "\x1B[2m", "\x1B[22m"]
360 | : ["Warning:", filenameList];
361 | console.warn(`${prefix} Unmatched ${1 === size ? "entry" : "entries"} in ${path}:`);
362 | for(const [lineNumber, filename] of unmatched)
363 | console.warn(`\t${dimOn}line ${lineNumber}:${dimOff} ${filename}`);
364 | }
365 |
366 | // Patch the existing database in-place
367 | for(const [icon, filenames] of matched){
368 | const regex = icon[2];
369 | const additions = [""];
370 | for(const [, filename] of filenames)
371 | additions.push(`^${filename.replace(/[/\\^$*+?{}[\]().|]/g, "\\$&")}$`);
372 | regex.compile(regex.source + additions.join("|"), regex.flags);
373 | }
374 | return db;
375 | }
376 |
377 |
378 | /**
379 | * Extract a list of unique filename/extension matches from a regex.
380 | * @param {RegExp} input
381 | * @return {MatchesByType}
382 | * @internal
383 | */
384 | function parseRegExp(input){
385 | /**
386 | * @typedef {Object} MatchesByType
387 | * @property {Set} substrings - Substrings appearing anywhere in a filename
388 | * @property {Set} prefixes - Filename prefixes; i.e., /^foo…/
389 | * @property {Set} suffixes - File extensions; i.e., /…foo$/
390 | * @property {Set} full - Full-string matches; i.e., /^foo$/
391 | */
392 | const output = {
393 | __proto__: null,
394 | substrings: new Set(),
395 | prefixes: new Set(),
396 | suffixes: new Set(),
397 | full: new Set(),
398 | };
399 |
400 | if("genex" !== input?.constructor?.name.toLowerCase())
401 | input = Genex(input);
402 |
403 | const anchors = new Set();
404 | const chars = new Set();
405 | const killList = new Set();
406 | const lists = new Set();
407 | const walk = (obj, refs = new WeakSet()) => {
408 | if("object" !== typeof obj || null === obj || refs.has(obj))
409 | return;
410 | refs.add(obj);
411 | if(Array.isArray(obj)){
412 | lists.add(obj);
413 | for(const item of obj){
414 | try{ walk(item, refs); }
415 | catch(e){ killList.add(item); }
416 | }
417 | }
418 | else{
419 | if(Infinity === obj.max){
420 | if(!obj.min)
421 | throw new RangeError("Bad range");
422 | obj.max = ~~obj.min;
423 | }
424 | else switch(obj.type){
425 | case 2: "^$".includes(obj.value) && anchors.add(obj); break;
426 | case 7: chars.add(obj.value); break;
427 | default: {
428 | const {options: opts, stack} = obj;
429 | if(Array.isArray(opts)){
430 | lists.add(opts);
431 | for(const opt of opts)
432 | try{ walk(opt, refs); }
433 | catch(e){ killList.add(opt); }
434 | }
435 | else if(Array.isArray(stack)){
436 | lists.add(stack);
437 | walk(stack, refs);
438 | }
439 | else walk(obj.value, refs);
440 | }
441 | }
442 | }
443 | };
444 |
445 | walk(input.tokens);
446 | for(const token of killList){
447 | for(const list of lists){
448 | while(list.includes(token))
449 | list.splice(list.indexOf(token), 1);
450 | }
451 | }
452 |
453 | const used = String.fromCodePoint(...chars);
454 | const unused = Array.from(getUnusedChar(used + "\\[]{}()?+*", 2))
455 | .map(char => char.codePointAt(0));
456 |
457 | for(const anchor of anchors){
458 | anchor.type = 7;
459 | anchor.value = "^" === anchor.value
460 | ? unused[0]
461 | : unused[1];
462 | }
463 |
464 | const cases = new Set();
465 | input.generate(result => {
466 | if(cases.size > 1000) throw new RangeError("Too many cases to generate");
467 | cases.add(result);
468 | });
469 |
470 | for(let str of cases){
471 | let anchoredToStart = false;
472 | let anchoredToEnd = false;
473 | str = [...str];
474 | str = str.map((char, index) => {
475 | const code = char.codePointAt(0);
476 | if(code === unused[0]){
477 | assert.strictEqual(index, 0);
478 | anchoredToStart = true;
479 | }
480 | else if(code === unused[1]){
481 | assert.strictEqual(index, str.length - 1);
482 | anchoredToEnd = true;
483 | }
484 | else return char;
485 | return "";
486 | }).join("");
487 | const type =
488 | anchoredToStart && anchoredToEnd ? "full" :
489 | anchoredToStart ? "prefixes" :
490 | anchoredToEnd ? "suffixes" :
491 | "substrings";
492 | output[type].add(str);
493 | }
494 | return output;
495 | }
496 |
497 |
498 | /**
499 | * Write an object to disk as a JSON file, preserving timestamps if identical.
500 | * @param {Object} input
501 | * @param {String} path
502 | * @return {void}
503 | * @internal
504 | */
505 | function saveJSON(input, path){
506 | path = resolve(path);
507 | let existingFile = null;
508 | try{ existingFile = readFileSync(path, "utf8"); }
509 | catch(e){}
510 | input = JSON.stringify(input, null, "\t").trim() + "\n";
511 | input === existingFile
512 | ? console.info(`Theme already up-to-date: ${basename(path)}`)
513 | : writeFileSync(path, input, "utf8");
514 | }
515 |
516 |
517 | /**
518 | * Ensure that a complete icon-definition with the given ID exists.
519 | * @param {String} name
520 | * @param {Object} icons
521 | * @param {Object} fonts
522 | * @return {void}
523 | * @internal
524 | */
525 | function validateIcon(name, icons, fonts){
526 | if(name in icons){
527 | for(const key of ["fontCharacter", "fontId"]){
528 | const value = icons[name][key];
529 | if("string" !== typeof value || !value)
530 | throw new TypeError(`Missing "${key}" field in icon "${name}"`);
531 | }
532 | const {fontId} = icons[name];
533 | if(!fonts.some(font => fontId === font.id))
534 | throw new ReferenceError(`Icon "${name}" references undefined font "${fontId}"`);
535 | }
536 | else throw new ReferenceError(`Undefined icon: ${name}`);
537 | }
538 |
539 |
540 | // Section: Icons {{{1
541 |
542 | /**
543 | * Load icon definitions from the given stylesheet.
544 | *
545 | * @example loadIcons("/path/to/styles/icons.less");
546 | * @param {String} from - Path to stylesheet
547 | * @return {Object}
548 | * @private
549 | */
550 | async function loadIcons(from){
551 | from = resolve(from);
552 | const icons = {__proto__: null};
553 | const fonts = {__proto__: null};
554 | const rules = parseRules(await loadStyleSheet(from));
555 | for(const selector in rules){
556 | const rule = rules[selector];
557 | const font = (rule["font-family"] || "").toLowerCase();
558 | if(!font) continue;
559 | if(/^\.((?:(?!-|\d)[-a-z0-9]+|_\d+[-a-z0-9]*)(? "string" === typeof x || Array.isArray(x) && "local" !== x[0]) ?? src;
684 | if("string" === typeof src)
685 | path = src;
686 | else for(const item of src){
687 | if(null == path && "string" === typeof item)
688 | path = item;
689 | if(null == format && Array.isArray(item) && "format" === item[0]){
690 | format = item[1];
691 | if(Array.isArray(format))
692 | format = format[0];
693 | }
694 | }
695 | }
696 | else path = parse(src);
697 | format ||= /\.\w+$/.test(path) ? RegExp.lastMatch.slice(1) : "woff2";
698 | if(path.toLowerCase().startsWith("atom://file-icons/")){
699 | const head = resolve(dirname(from), "..");
700 | const tail = path.slice(18);
701 | path = resolve(join(head, tail));
702 | }
703 | fonts.push({
704 | id: font["font-family"].toLowerCase(),
705 | src: [{path, format}],
706 | weight: font["font-weight"] || "normal",
707 | style: font["font-style"] || "normal",
708 | size: "100%",
709 | });
710 | }
711 | return fonts;
712 | }
713 |
714 | /**
715 | * Update the VSCode package's icon-fonts with their upstream versions, if needed.
716 | *
717 | * @example updateFonts([octicons, ...fonts] "./vscode/icons/");
718 | * @param {IconThemeFont[]} fontDefs - Icon-font definitions
719 | * @param {String} targetDir - Path of destination directory
720 | * @param {Object} [options={}] - Settings governing what to update
721 | * @param {Boolean} [options.force] - Ignore timestamps when copying
722 | * @param {Boolean} [options.noLink] - Copy files instead of linking
723 | * @return {Number} The number of fonts that were updated
724 | * @private
725 | */
726 | function updateFonts(fontDefs, targetDir, {force, noLink} = {}){
727 | let updates = 0;
728 | for(const font of fontDefs){
729 | const srcPath = font.src[0].path;
730 | const srcStat = stat(srcPath);
731 | const dstPath = join(targetDir, basename(srcPath));
732 | if(!exists(dstPath)){
733 | const [str, fn] = srcStat.dev !== stat(targetDir).dev
734 | ? ["Copying", copyFileSync]
735 | : ["Linking", linkSync];
736 | console.info(`${str}: ${srcPath} -> ${dstPath}`);
737 | fn(srcPath, dstPath);
738 | linkSync(srcPath, dstPath);
739 | ++updates;
740 | }
741 | else{
742 | const dstStat = stat(dstPath);
743 | if(noLink || srcStat.dev !== dstStat.dev){
744 | if(!force && dstStat.mtimeNs >= srcStat.mtimeNs){
745 | console.info(`Already up-to-date: ${dstPath}`);
746 | continue;
747 | }
748 | console.info(`Copying: ${srcPath} -> ${dstPath}`);
749 | unlinkSync(dstPath);
750 | copyFileSync(srcPath, dstPath);
751 | ++updates;
752 | }
753 | else if(srcStat.ino !== dstStat.ino){
754 | console.info(`Linking: ${srcPath} -> ${dstPath}`);
755 | unlinkSync(dstPath);
756 | linkSync(srcPath, dstPath);
757 | ++updates;
758 | }
759 | }
760 | }
761 | return updates;
762 | }
763 |
764 | /**
765 | * @typedef {Object} IconThemeFont
766 | * @property {String} id
767 | * @property {String} [weight="normal"]
768 | * @property {String} [style="normal"]
769 | * @property {String} [size="100%"]
770 | * @property {{path: String, format: String}[]} src
771 | */
772 |
773 |
774 | // Section: Utilities {{{1
775 |
776 | /**
777 | * Throw an exception if one of the given paths isn't a directory.
778 | * @param {...String} paths
779 | * @return {void}
780 | */
781 | function assertDir(...paths){
782 | for(const path of paths){
783 | const stats = stat(path, true);
784 | if(!stats)
785 | throw new Error("No such directory: " + path);
786 | if(!stats.isDirectory())
787 | throw new Error("Not a directory: " + path);
788 | }
789 | }
790 |
791 | /**
792 | * Throw an exception if one of the given paths isn't a regular file.
793 | * @param {...String} paths
794 | * @return {void}
795 | */
796 | function assertFile(...paths){
797 | for(const path of paths){
798 | const stats = stat(path, true);
799 | if(!stats)
800 | throw new Error("No such file: " + path);
801 | if(!stats.isFile())
802 | throw new Error("Not a regular file: " + path);
803 | }
804 | }
805 |
806 | /**
807 | * Return true if a file exists on disk, even as a broken symbolic link.
808 | * @param {String}
809 | * @return {Boolean}
810 | * @see {@link fs.existsSync}
811 | */
812 | function exists(path){
813 | try{ return !!lstatSync(path); }
814 | catch(e){ return false; }
815 | }
816 |
817 | /**
818 | * Return one or more characters not contained in a string.
819 | *
820 | * @version Alhadis/Utils@c8ee57d
821 | * @example getUnusedChar("\x00\x02") == "\x01";
822 | * @example getUnusedChar("\x00\x02", 2) == "\x01\x03";
823 | * @param {String} input
824 | * @param {Number} [count=1]
825 | * @return {String}
826 | */
827 | function getUnusedChar(input, count = 1){
828 | let chars = "";
829 | let next = "\x00";
830 | let code = 0;
831 | for(let i = 0; i < count; ++i){
832 | while(-1 !== input.indexOf(next) || -1 !== chars.indexOf(next))
833 | next = String.fromCodePoint(++code);
834 | chars += next;
835 | }
836 | return chars;
837 | }
838 |
839 | /**
840 | * Synchronously lstat(2) a file without throwing an exception.
841 | *
842 | * @example Testing a symlink to `/dev/fd/0`
843 | * stat("/dev/stdin").isSymbolicLink() === true;
844 | * stat("/dev/stdin", true).isCharacterDevice() === true;
845 | *
846 | * @param {String} path - Pathname of the file being examined
847 | * @param {Boolean} [followLinks=false] - Use stat(2) instead
848 | * @return {?fs.BigIntStats}
849 | * @see {@link fs.lstatSync}
850 | */
851 | function stat(path, followSymlinks = false){
852 | try{ return (followSymlinks ? statSync : lstatSync)(path, {bigint: true}); }
853 | catch(e){ return null; }
854 | }
855 |
856 | /**
857 | * Render and reparse a Less stylesheet.
858 | * @param {String} path
859 | * @return {Less~Ruleset}
860 | * @private
861 | */
862 | async function loadStyleSheet(path){
863 | const file = readFileSync(path, "utf8");
864 | const {css} = await Less.render(file, {filename: path});
865 | path = path.replace(/\.less$/i, ".css");
866 | return Less.parse(css, {filename: path});
867 | }
868 |
869 | /**
870 | * Try to simplify Less's absurdly over-complicated parser output.
871 | * @param {String|Object|Array} node
872 | * @param {WeakSet} [refs]
873 | * @return {String|Object|Array}
874 | * @private
875 | */
876 | function parse(node, refs = new WeakSet()){
877 | if(null == node) return node;
878 | if(!isObj(node)) return String(node ?? "");
879 | if(refs.has(node)) return;
880 | refs.add(node);
881 | if(Array.isArray(node)){
882 | node = node.map(x => parse(x, refs)).filter(x => null != x);
883 | return node.length < 2
884 | ? parse(node[0], refs) ?? ""
885 | : node;
886 | }
887 | let {name, value, args, rules} = node;
888 | if("Comment" === node.type) return;
889 | if(name && value) return [parse(name, refs), parse(value, refs)];
890 | if(!name && value) return parse(value, refs);
891 | if(!value && (value = rules || args)){
892 | value = value.map(x => parse(x, refs));
893 | if(value.every(item => isEnt(item)))
894 | value = Object.fromEntries(value);
895 | return name ? [parse(name, refs), value] : value;
896 | }
897 | console.error("Bad input:", node);
898 | throw new TypeError(`Unexpected input: ${inspect(node)}`);
899 | }
900 |
901 | /**
902 | * Extract a list of rulesets from a parsed stylesheet.
903 | *
904 | * @see {@link loadStyleSheet}
905 | * @example Parsing a stylesheet with 3 class selectors
906 | * const blue = await loadStyleSheet("colours/blue.less");
907 | * parseRules(blue) == {
908 | * ".light-blue:before": {color: "#9dc0ce"},
909 | * ".medium-blue:before": {color: "#6a9fb5"},
910 | * ".dark-blue:before": {color: "#46788d"},
911 | * };
912 | * @param {Less~Ruleset} ruleset
913 | * @return {Object} A null-prototype object containing objects
914 | * keyed by selector, enumerated with parsed CSS properties.
915 | */
916 | function parseRules(ruleset){
917 | const rules = {__proto__: null};
918 | for(const rule of ruleset.rules){
919 | if(!rule || "Comment" === rule.type)
920 | continue;
921 | const selectors = rule.selectors.map(sel =>
922 | sel.elements.map(el => el.combinator.value + el.value).join("").trim());
923 | for(const name of selectors)
924 | rules[name] = {...rules[name], ...parse(rule)};
925 | }
926 | return rules;
927 | }
928 |
929 | /**
930 | * Return a new object with the properties of another sorted alphanumerically.
931 | * @param {Object} input
932 | * @return {Object}
933 | * @internal
934 | */
935 | function sortProps(input){
936 | const alnum = /[^A-Za-z0-9]/g;
937 | input = Object.entries(input).sort(([a], [b]) =>
938 | a.replace(alnum, "").localeCompare(b.replace(alnum, "")));
939 | return Object.fromEntries(input);
940 | }
941 |
942 | // vim:fdm=marker:noet
943 |
--------------------------------------------------------------------------------