@open-xchange/vite-plugin-icon-sprite 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +3 -0
- package/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +324 -0
- package/dist/helper.d.ts +62 -0
- package/dist/helper.js +56 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/mapping-schema.json +32 -0
- package/dist/plugin-png.d.ts +88 -0
- package/dist/plugin-png.js +233 -0
- package/dist/plugin-svg.d.ts +32 -0
- package/dist/plugin-svg.js +119 -0
- package/package.json +45 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 OX Software GmbH, Germany <info@open-xchange.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# @open-xchange/vite-plugin-icon-sprite
|
|
2
|
+
|
|
3
|
+
A Vite plugin that builds icon sprites from multiple SVG or PNG icon files.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
This plugin collects multiple SVG or PNG image files, assembles them into a single icon sprite file, and provides virtual modules that can be imported in source code.
|
|
8
|
+
|
|
9
|
+
In Vite server mode, the icon sprite files will be generated on demand. When bundling the project, all icon sprite files referenced in source code will be generated and bundled.
|
|
10
|
+
|
|
11
|
+
It is possible to instantiate this plugin repeatedly, e.g. in order to generate an SVG sprite and a PNG sprite in the same project.
|
|
12
|
+
|
|
13
|
+
### Common Options
|
|
14
|
+
|
|
15
|
+
Common configuration options for SVG sprites and PNG sprites.
|
|
16
|
+
|
|
17
|
+
#### Option `format`
|
|
18
|
+
|
|
19
|
+
- Type: `"svg" | "png"`
|
|
20
|
+
- _required_
|
|
21
|
+
|
|
22
|
+
The file format specifier of the icon sprite, and the source images. Depending on the value of this option, the plugin expects various file-format specific options (see below).
|
|
23
|
+
|
|
24
|
+
#### Option `imagesPath`
|
|
25
|
+
|
|
26
|
+
- Type: `string`
|
|
27
|
+
- _required_
|
|
28
|
+
|
|
29
|
+
Path to root directory with all image resource files to be processed.
|
|
30
|
+
|
|
31
|
+
#### Option `mappingPath`
|
|
32
|
+
|
|
33
|
+
- Type: `string`
|
|
34
|
+
- _required_
|
|
35
|
+
|
|
36
|
+
Path to the JSON or YAML configuration file containing the mapping between icon identifiers and SVG source file names (see below).
|
|
37
|
+
|
|
38
|
+
### SVG Sprites
|
|
39
|
+
|
|
40
|
+
The plugin collects multiple SVG files, and generates a virtual import for an SVG sprite file with multiple `<symbol>` elements that can be imported from source code.
|
|
41
|
+
|
|
42
|
+
Example how to consume the SVG sprite in source code:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// path/to/source.ts
|
|
46
|
+
|
|
47
|
+
import svgSprite from "virtual:svg/my-icons.svg"
|
|
48
|
+
|
|
49
|
+
// insert the SVG sprite into the DOM to be able to refer to the <symbol> elements
|
|
50
|
+
document.createElement("div").innerHTML = svgSprite
|
|
51
|
+
|
|
52
|
+
// create an SVG element referring to an icon in the sprite
|
|
53
|
+
function createIcon(id: string): SVGElement {
|
|
54
|
+
const useEl = document.createElementNS("http://www.w3.org/2000/svg", "use")
|
|
55
|
+
useEl.setAttribute("href", "#my-icon")
|
|
56
|
+
const iconEl = document.createElementNS("http://www.w3.org/2000/svg", "svg")
|
|
57
|
+
iconEl.append(useEl)
|
|
58
|
+
return iconEl
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### Option `spriteName`
|
|
63
|
+
|
|
64
|
+
- Type: `string`
|
|
65
|
+
- _required_
|
|
66
|
+
|
|
67
|
+
The module name of the SVG sprite to be generated (with ".svg" extension).
|
|
68
|
+
|
|
69
|
+
The SVG markup of the sprite can be imported from the virtual module path `"virtual:svg/[spriteName]"`.
|
|
70
|
+
|
|
71
|
+
#### Option `idPrefix`
|
|
72
|
+
|
|
73
|
+
- Type: `string`
|
|
74
|
+
- _optional_
|
|
75
|
+
|
|
76
|
+
A prefix to be added to all icon identifiers declared in the mapping file. By default, no prefix will be added.
|
|
77
|
+
|
|
78
|
+
#### SVG Sprite Example
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// vite.config.ts
|
|
82
|
+
|
|
83
|
+
import { defineConfig } from "vite" // or "vitest/config"
|
|
84
|
+
import spritePlugin from "@open-xchange/vite-plugin-icon-sprite"
|
|
85
|
+
|
|
86
|
+
export default defineConfig(() => {
|
|
87
|
+
plugins: [
|
|
88
|
+
spritePlugin({
|
|
89
|
+
format: "svg",
|
|
90
|
+
imagesPath: "src/icons/images",
|
|
91
|
+
mappingPath: "src/icons/svg-mapping.yaml",
|
|
92
|
+
spriteName: "icons.svg",
|
|
93
|
+
idPrefix: "svg-",
|
|
94
|
+
}),
|
|
95
|
+
],
|
|
96
|
+
})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- Collects all SVG files in the directory `src/icons/images`.
|
|
100
|
+
- Uses the icon mapping in `src/icons/svg-mapping.yaml`.
|
|
101
|
+
- Creates a virtual import `"virtual:svg/icons.svg"`.
|
|
102
|
+
- Prefixes all icon identifiers with `svg-`, e.g. the icon key `my-icon` in the mapping file will result in the icon identifier `"svg-my-icon"` in source code.
|
|
103
|
+
|
|
104
|
+
### PNG Sprites
|
|
105
|
+
|
|
106
|
+
The plugin collects multiple PNG files, and generates a virtual imports for one or more sprite files with different icon sizes and a CSS file that can be imported from source code.
|
|
107
|
+
|
|
108
|
+
#### Option `cssName`
|
|
109
|
+
|
|
110
|
+
- Type: `string`
|
|
111
|
+
- _required_
|
|
112
|
+
|
|
113
|
+
The module name for the CSS file (with ".css" extension). The generated CSS markup can be imported from `"virtual:png/[cssName]"`.
|
|
114
|
+
|
|
115
|
+
#### Option `cssIconSize`
|
|
116
|
+
|
|
117
|
+
- Type: `number`
|
|
118
|
+
- _required_
|
|
119
|
+
|
|
120
|
+
Base size of all icons, in CSS pixels.
|
|
121
|
+
|
|
122
|
+
#### Option `cssIconPadding`
|
|
123
|
+
|
|
124
|
+
- Type: `number`
|
|
125
|
+
- Default: `0`
|
|
126
|
+
|
|
127
|
+
Additional padding around all icons to be generated in the sprites, in CSS pixels.
|
|
128
|
+
|
|
129
|
+
#### Option `cssIconSelector`
|
|
130
|
+
|
|
131
|
+
- Type: `string`
|
|
132
|
+
- Default: `"i.png-icon"`
|
|
133
|
+
|
|
134
|
+
The CSS selector for a PNG icon element to be used in all generated CSS rules.
|
|
135
|
+
|
|
136
|
+
#### Option `rootLocaleAttr`
|
|
137
|
+
|
|
138
|
+
- Type: `string`
|
|
139
|
+
- Default: `"lang"`
|
|
140
|
+
|
|
141
|
+
Name of the root element's attribute containing the locale identifier. Needed to generate CSS selectors for localized icons.
|
|
142
|
+
|
|
143
|
+
#### Option `spriteColorType`
|
|
144
|
+
|
|
145
|
+
- Type: `"source" | "monochrome" | "alpha"`
|
|
146
|
+
- Default: `"source"`
|
|
147
|
+
|
|
148
|
+
Specifies how to generate the sprite PNG files.
|
|
149
|
+
|
|
150
|
+
| Value | Description |
|
|
151
|
+
| - | - |
|
|
152
|
+
| `"source"` | The source PNGs will be copied into the generated sprites unmodified. They will contain three color channels, and an alpha channel. |
|
|
153
|
+
| `"monochrome"` | The generated sprites will be converted to monochrome. They will contain a gray channel and an alpha channel. |
|
|
154
|
+
| `"alpha"` | Only the alpha channels of the source PNGs will be copied into the generated sprites. They will contain a single gray channel representing the original alpha channels. |
|
|
155
|
+
|
|
156
|
+
#### Option `spriteFillType`
|
|
157
|
+
|
|
158
|
+
- Type: `"background" | "mask"`
|
|
159
|
+
- Default: `"background"`
|
|
160
|
+
|
|
161
|
+
Specifies how the sprites are supposed to be used in CSS rules.
|
|
162
|
+
|
|
163
|
+
| Value | Description |
|
|
164
|
+
| `"background"` | The sprites will be attached via "background-image". |
|
|
165
|
+
| `"mask"` | The sprites will be attached via "mask-image". |
|
|
166
|
+
|
|
167
|
+
All related CSS properties (e.g. `background-position` vs. `mask-position` etc.) will be generated accordingly.
|
|
168
|
+
|
|
169
|
+
#### Option `sprites`
|
|
170
|
+
|
|
171
|
+
- Type: `Record<string, { factor: number; src: string }>`
|
|
172
|
+
- _required_
|
|
173
|
+
|
|
174
|
+
List of all icon sprites with different icon sizes to be generated.
|
|
175
|
+
|
|
176
|
+
- The keys of the dictionary are the module names of the PNG sprites (with ".png" extension). The generated PNG sprite can be imported from `"virtual:png/[key]"`.
|
|
177
|
+
|
|
178
|
+
- The values of the dictionary contain configuration options for the PNG sprite:
|
|
179
|
+
|
|
180
|
+
| Option | Type | Default | Description |
|
|
181
|
+
| - | - | - | - |
|
|
182
|
+
| `factor` | `number` | _required_ | Icon scaling factor (a multiplier for plugin option `cssIconSize`). All source PNG files must have the effective pixel size (`cssIconSize * factor`). |
|
|
183
|
+
| `src` | `string` | _required_ | The pattern used to build the path of the source PNG files. MUST contain the placeholder `[path]` that will be replaced with the base paths contained in the icon mapping file. |
|
|
184
|
+
|
|
185
|
+
#### PNG Sprite Example
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
// vite.config.ts
|
|
189
|
+
|
|
190
|
+
import { defineConfig } from "vite" // or "vitest/config"
|
|
191
|
+
import spritePlugin from "@open-xchange/vite-plugin-icon-sprite"
|
|
192
|
+
|
|
193
|
+
export default defineConfig(() => {
|
|
194
|
+
plugins: [
|
|
195
|
+
spritePlugin({
|
|
196
|
+
format: "png",
|
|
197
|
+
imagesPath: "src/icons/images",
|
|
198
|
+
mappingPath: "src/icons/png-mapping.yaml",
|
|
199
|
+
cssName: "icons.css",
|
|
200
|
+
cssIconSize: 16,
|
|
201
|
+
cssIconPadding: 1,
|
|
202
|
+
cssIconSelector: "i.my-icon",
|
|
203
|
+
rootLocaleAttr: "data-icon-locale",
|
|
204
|
+
spriteColorType: "alpha",
|
|
205
|
+
spriteFillType: "mask",
|
|
206
|
+
sprites: {
|
|
207
|
+
"icons1.png": { factor: 1, src: "[path]_16.png" },
|
|
208
|
+
"icons2.png": { factor: 2, src: "[path]_32.png" },
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
]
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
- Collects all PNG files in the directory `src/icons/images`. The images must exist in two sizes (16px and 32px), their file names must end with `_16.png` and `_32.png` respectively (according to the options `cssIconSize` and `sprites->factor`).
|
|
216
|
+
- Uses the icon mapping in `src/icons/png-mapping.yaml`.
|
|
217
|
+
- Creates the virtual imports `"virtual:svg/icons.css"`, `"virtual:svg/icons1.png"`, and `"virtual:svg/icons2.png"`.
|
|
218
|
+
- Adds one pixel padding around all icons in the PNG sprites.
|
|
219
|
+
- Generates CSS selectors for `<i>` elements with CSS class `my-icon`.
|
|
220
|
+
- Generates `:root[data-icon-locale]` CSS selectors for localized icons (i.e., the UI locale code must be stored in the root element's attribute "data-icon-locale").
|
|
221
|
+
- Generates PNG sprites consisting of an 8-bit alpha channel only.
|
|
222
|
+
- Generates CSS rules using CSS mask (instead of background).
|
|
223
|
+
|
|
224
|
+
### Icon Mapping File
|
|
225
|
+
|
|
226
|
+
The plugin expects an icon mapping file (plugin option `mappingPath`) which is a JSON or YAML configuration file containing a mapping from arbitrary icon identifiers to the paths of the source images. The icon identifiers can be used later in source code to refer to a specific icon in the generated sprite.
|
|
227
|
+
|
|
228
|
+
- The name of the configuration file can be chosen freely.
|
|
229
|
+
|
|
230
|
+
- The configuration file must consist of a single object map.
|
|
231
|
+
|
|
232
|
+
- Each entry maps a unique icon identifier (used in source code) to a source image to be used for that icon.
|
|
233
|
+
|
|
234
|
+
- The source image file can be specified directly as string, or as dictionary for localized icons (more details below).
|
|
235
|
+
|
|
236
|
+
- Only the base name of the source image file must be specified relative to the configured root directory of the image files. It must not contain the image size suffix (_PNG only_), nor a file extension (`.svg` or `.png`). See examples below.
|
|
237
|
+
|
|
238
|
+
- Localized icons will be described by a dictionary mapping the ISO language identifiers (as comma-separated strings) to the image base names (as described above). The special locale code `"*"` is mandatory, and defines a default icon for unlisted locales. See examples below.
|
|
239
|
+
|
|
240
|
+
#### Icon Mapping Examples
|
|
241
|
+
|
|
242
|
+
In all examples, the configured image root directory (plugin option `imagesPath`) shall be `path/to/images`.
|
|
243
|
+
|
|
244
|
+
##### Example 1: Simple SVG Icons
|
|
245
|
+
|
|
246
|
+
- Assign the icon with the identifier `my-icon` to the SVG image `path/to/images/commons/icon1.svg`.
|
|
247
|
+
- Assign the icon with the identifier `other-icon` to the SVG image `path/to/images/commons/icon2.svg`.
|
|
248
|
+
|
|
249
|
+
```YAML
|
|
250
|
+
# svg-mapping.yaml
|
|
251
|
+
|
|
252
|
+
my-icon: commons/icon1
|
|
253
|
+
other-icon: commons/icon2
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
In source code, the icons can be used with the identifiers `"my-icon"` and `"other-icon"`.
|
|
257
|
+
|
|
258
|
+
##### Example 2: Simple PNG Icons
|
|
259
|
+
|
|
260
|
+
Assuming that the plugin will generate PNG sprites for icons with sizes of 16px and 32px.
|
|
261
|
+
|
|
262
|
+
- Assign the icon with the identifier `my-icon` to the PNG images `path/to/images/commons/icon1_16.png` (16x16 pixels) and `path/to/images/commons/icon1_32.png` (32x32 pixels).
|
|
263
|
+
- Respectively, assign an icon with the identifier `other-icon` to the PNG images `path/to/images/commons/icon2_*.png`.
|
|
264
|
+
|
|
265
|
+
The resulting mapping file looks exactly as the former mapping file for SVG icons:
|
|
266
|
+
|
|
267
|
+
```YAML
|
|
268
|
+
# png-mapping.yaml
|
|
269
|
+
|
|
270
|
+
my-icon: commons/icon1
|
|
271
|
+
other-icon: commons/icon2
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
##### Example 3: Localized Icons
|
|
275
|
+
|
|
276
|
+
- Assign the icon with the identifier `my-icon` to the SVG image `path/to/images/commons/icon1.svg` by default.
|
|
277
|
+
- Use the SVG image `path/to/images/commons/icon2.svg` in German and French UI instead.
|
|
278
|
+
- Use the SVG image `path/to/images/commons/icon3.svg` in Swedish UI instead.
|
|
279
|
+
|
|
280
|
+
```YAML
|
|
281
|
+
# svg-mapping.yaml
|
|
282
|
+
|
|
283
|
+
my-icon:
|
|
284
|
+
"*": commons/icon1
|
|
285
|
+
de,fr: commons/icon2
|
|
286
|
+
sv: commons/icon3
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The same applies to PNG icons as well.
|
|
290
|
+
|
|
291
|
+
#### Schema Validation
|
|
292
|
+
|
|
293
|
+
This package provides a JSON schema that can be used for validation in editors.
|
|
294
|
+
|
|
295
|
+
##### JSON Mapping File
|
|
296
|
+
|
|
297
|
+
Add the path to the schema file as property `"$schema"` to the mapping file:
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
// mapping.json
|
|
301
|
+
|
|
302
|
+
{
|
|
303
|
+
"$schema": "../../node_modules/@open-xchange/vite-plugin-icon-sprite/dist/mapping-schema.json",
|
|
304
|
+
// ...
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Adjust the number of parent path fragments according to the location of the mapping file in the project.
|
|
309
|
+
|
|
310
|
+
##### YAML Mapping File
|
|
311
|
+
|
|
312
|
+
Add the path to the schema file in a `yaml-language-server` directive to the mapping file:
|
|
313
|
+
|
|
314
|
+
```yaml
|
|
315
|
+
# mapping.yaml
|
|
316
|
+
|
|
317
|
+
# yaml-language-server: $schema=../../node_modules/@open-xchange/vite-plugin-icon-sprite/dist/mapping-schema.json
|
|
318
|
+
|
|
319
|
+
# ...
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Adjust the number of parent path fragments according to the location of the mapping file in the project.
|
|
323
|
+
|
|
324
|
+
In VS Code, the plugin [redhat.vscode-yaml](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) needs to be installed to support this directive.
|
package/dist/helper.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type PluginHelperConfig, PluginHelper } from "@open-xchange/vite-helper";
|
|
2
|
+
/**
|
|
3
|
+
* Common configuration options (independent from the icon file format) for the
|
|
4
|
+
* plugin "@open-xchange/vite-plugin-icon-sprite".
|
|
5
|
+
*
|
|
6
|
+
* @template FormatT
|
|
7
|
+
* The file format specifier of the icon sprite, and the source images.
|
|
8
|
+
*/
|
|
9
|
+
export interface SpritePluginBaseOptions<FormatT extends string> {
|
|
10
|
+
/**
|
|
11
|
+
* The file format specifier of the icon sprite, and the source images.
|
|
12
|
+
*/
|
|
13
|
+
format: FormatT;
|
|
14
|
+
/**
|
|
15
|
+
* Path to root directory with all image resource files to be processed.
|
|
16
|
+
*/
|
|
17
|
+
imagesPath: string;
|
|
18
|
+
/**
|
|
19
|
+
* Path to the JSON or YAML configuration file containing the mapping
|
|
20
|
+
* between icon identifiers and SVG source file names.
|
|
21
|
+
*/
|
|
22
|
+
mappingPath: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Internal configuration of a `SpritePluginHelper` instance.
|
|
26
|
+
*/
|
|
27
|
+
export type SpritePluginConfig = Required<Pick<PluginHelperConfig, "pluginIndex" | "virtualModules">>;
|
|
28
|
+
/**
|
|
29
|
+
* Represents a single entry in an icon mapping configuration file (a mapping
|
|
30
|
+
* between icon identifier, path to source image, and language code for
|
|
31
|
+
* localized icons).
|
|
32
|
+
*/
|
|
33
|
+
export interface IconMappingEntry {
|
|
34
|
+
/** The icon identifier used in source code to select an icon. */
|
|
35
|
+
iconId: string;
|
|
36
|
+
/** The path to the source image file to be inserted into the sprite. */
|
|
37
|
+
iconPath: string;
|
|
38
|
+
/** Language code for a localized icon. */
|
|
39
|
+
iconLang?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Plugin helper for all icon file formats.
|
|
43
|
+
*
|
|
44
|
+
* @template OptionsT
|
|
45
|
+
* Exact type of the options interface.
|
|
46
|
+
*/
|
|
47
|
+
export declare class SpritePluginHelper<OptionsT extends SpritePluginBaseOptions<string>> extends PluginHelper {
|
|
48
|
+
/** Resolved configuration options. */
|
|
49
|
+
readonly options: Required<Readonly<OptionsT>>;
|
|
50
|
+
protected constructor(config: SpritePluginConfig, options: Required<OptionsT>);
|
|
51
|
+
/**
|
|
52
|
+
* Reads an icon mapping configuration file, and returns the entries as
|
|
53
|
+
* array.
|
|
54
|
+
*
|
|
55
|
+
* @param path
|
|
56
|
+
* The path to the icon mapping configuration file to be read.
|
|
57
|
+
*
|
|
58
|
+
* @returns
|
|
59
|
+
* The entries of the configuration file.
|
|
60
|
+
*/
|
|
61
|
+
protected readIconMapping(): Promise<IconMappingEntry[]>;
|
|
62
|
+
}
|
package/dist/helper.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { dictEntries } from "@open-xchange/vite-helper/utils";
|
|
2
|
+
import { resolvePath } from "@open-xchange/vite-helper/file";
|
|
3
|
+
import { PluginHelper } from "@open-xchange/vite-helper";
|
|
4
|
+
// class SpritePluginHelper ===================================================
|
|
5
|
+
/**
|
|
6
|
+
* Plugin helper for all icon file formats.
|
|
7
|
+
*
|
|
8
|
+
* @template OptionsT
|
|
9
|
+
* Exact type of the options interface.
|
|
10
|
+
*/
|
|
11
|
+
export class SpritePluginHelper extends PluginHelper {
|
|
12
|
+
/** Resolved configuration options. */
|
|
13
|
+
options;
|
|
14
|
+
// constructor ------------------------------------------------------------
|
|
15
|
+
constructor(config, options) {
|
|
16
|
+
super({
|
|
17
|
+
...config,
|
|
18
|
+
virtualPrefix: options.format,
|
|
19
|
+
loggerPrefix: options.format,
|
|
20
|
+
logLevelEnvVar: "PLUGIN_ICON_SPRITE_LOGLEVEL",
|
|
21
|
+
cacheSrcFiles: [options.imagesPath + "/**/*." + options.format, options.mappingPath],
|
|
22
|
+
});
|
|
23
|
+
this.options = options;
|
|
24
|
+
}
|
|
25
|
+
// protected methods ------------------------------------------------------
|
|
26
|
+
/**
|
|
27
|
+
* Reads an icon mapping configuration file, and returns the entries as
|
|
28
|
+
* array.
|
|
29
|
+
*
|
|
30
|
+
* @param path
|
|
31
|
+
* The path to the icon mapping configuration file to be read.
|
|
32
|
+
*
|
|
33
|
+
* @returns
|
|
34
|
+
* The entries of the configuration file.
|
|
35
|
+
*/
|
|
36
|
+
async readIconMapping() {
|
|
37
|
+
const schemaPath = resolvePath("./mapping-schema.json", import.meta.url);
|
|
38
|
+
const mappingDict = await this.readConfig(this.options.mappingPath, { schema: schemaPath });
|
|
39
|
+
// convert to array of `IconMappingEntry`
|
|
40
|
+
const iconMapping = [];
|
|
41
|
+
for (const [iconId, entry] of dictEntries(mappingDict)) {
|
|
42
|
+
const iconDict = (typeof entry === "string") ? { "*": entry } : entry;
|
|
43
|
+
for (const [languages, iconPath] of dictEntries(iconDict)) {
|
|
44
|
+
for (const iconLang of languages.split(",")) {
|
|
45
|
+
if (iconLang === "*") {
|
|
46
|
+
iconMapping.push({ iconId, iconPath });
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
iconMapping.push({ iconId, iconPath, iconLang });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return iconMapping;
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import { type SvgSpritePluginOptions } from "./plugin-svg.js";
|
|
3
|
+
import { type PngSpritePluginOptions } from "./plugin-png.js";
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for generating icon sprites from SVG or PNG source
|
|
6
|
+
* images.
|
|
7
|
+
*/
|
|
8
|
+
export type IconSpritePluginOptions = SvgSpritePluginOptions | PngSpritePluginOptions;
|
|
9
|
+
/**
|
|
10
|
+
* A plugin for Vite to generate an icon sprite file from multiple SVG or PNG
|
|
11
|
+
* source files.
|
|
12
|
+
*
|
|
13
|
+
* @param options
|
|
14
|
+
* Plugin configuration options.
|
|
15
|
+
*
|
|
16
|
+
* @returns
|
|
17
|
+
* The plugin instance.
|
|
18
|
+
*/
|
|
19
|
+
export default function iconSpritePlugin(options: IconSpritePluginOptions): Plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { svgSpritePlugin } from "./plugin-svg.js";
|
|
2
|
+
import { pngSpritePlugin } from "./plugin-png.js";
|
|
3
|
+
// plugin =====================================================================
|
|
4
|
+
/**
|
|
5
|
+
* A plugin for Vite to generate an icon sprite file from multiple SVG or PNG
|
|
6
|
+
* source files.
|
|
7
|
+
*
|
|
8
|
+
* @param options
|
|
9
|
+
* Plugin configuration options.
|
|
10
|
+
*
|
|
11
|
+
* @returns
|
|
12
|
+
* The plugin instance.
|
|
13
|
+
*/
|
|
14
|
+
export default function iconSpritePlugin(options) {
|
|
15
|
+
switch (options.format) {
|
|
16
|
+
case "svg": return svgSpritePlugin(import.meta.url, options);
|
|
17
|
+
case "png": return pngSpritePlugin(import.meta.url, options);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema",
|
|
3
|
+
"title": "Icon mapping for SVG or PNG icon sprites",
|
|
4
|
+
"description": "A dictionary that maps icon identifiers to source SVG or PNG image files.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"$schema": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"format": "uri-reference"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"patternProperties": {
|
|
13
|
+
"^[a-z0-9]+(-[a-z0-9]+)*$": {
|
|
14
|
+
"anyOf": [
|
|
15
|
+
{
|
|
16
|
+
"type": "string"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"type": "object",
|
|
20
|
+
"patternProperties": {
|
|
21
|
+
"^(\\*|[a-z]+(,[a-z]+)*)$": {
|
|
22
|
+
"type": "string"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"required": ["*"],
|
|
26
|
+
"additionalProperties": false
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"additionalProperties": false
|
|
32
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import { type Dict } from "@open-xchange/vite-helper/utils";
|
|
3
|
+
import { type SpritePluginBaseOptions } from "./helper.js";
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for generating PNG sprites from PNG source images.
|
|
6
|
+
*/
|
|
7
|
+
export interface PngSpritePluginOptions extends SpritePluginBaseOptions<"png"> {
|
|
8
|
+
/**
|
|
9
|
+
* Module name for the CSS file (with ".css" extension). The generated CSS
|
|
10
|
+
* markup can be imported from `"virtual:png/[cssName]"`.
|
|
11
|
+
*/
|
|
12
|
+
cssName: string;
|
|
13
|
+
/**
|
|
14
|
+
* Base size of all icons, in CSS pixels.
|
|
15
|
+
*/
|
|
16
|
+
cssIconSize: number;
|
|
17
|
+
/**
|
|
18
|
+
* Additional padding around all icons to be generated in the sprites, in
|
|
19
|
+
* CSS pixels. Default value is 0.
|
|
20
|
+
*/
|
|
21
|
+
cssIconPadding?: number;
|
|
22
|
+
/**
|
|
23
|
+
* The CSS selector for a PNG icon element to be used in all generated CSS
|
|
24
|
+
* rules. Default value is "i.png-icon".
|
|
25
|
+
*/
|
|
26
|
+
cssIconSelector?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Name of the root element's attribute containing the locale identifier.
|
|
29
|
+
* Needed to generate CSS selectors for localized icons. Default value is
|
|
30
|
+
* "lang".
|
|
31
|
+
*/
|
|
32
|
+
rootLocaleAttr?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Specifies how to generate the sprite PNG files.
|
|
35
|
+
* - `"source"`: The source PNGs will be copied into the generated sprites
|
|
36
|
+
* unmodified. They will contain three color channels, and an alpha
|
|
37
|
+
* channel.
|
|
38
|
+
* - `"monochrome"`: The generated sprites will be converted to monochrome.
|
|
39
|
+
* They will contain a gray channel and an alpha channel.
|
|
40
|
+
* - `"alpha"`: Only the alpha channels of the source PNGs will be copied
|
|
41
|
+
* into the generated sprites. They will contain a single gray channel
|
|
42
|
+
* representing the original alpha channels.
|
|
43
|
+
*
|
|
44
|
+
* Default value is "source".
|
|
45
|
+
*/
|
|
46
|
+
spriteColorType?: "source" | "monochrome" | "alpha";
|
|
47
|
+
/**
|
|
48
|
+
* Specifies how the sprites are supposed to be used in CSS rules.
|
|
49
|
+
* - `"background"`: The sprites will be attached via "background-image".
|
|
50
|
+
* - `"mask"`: The sprites will be attached via "mask-image".
|
|
51
|
+
*
|
|
52
|
+
* All related CSS properties (e.g. "(background|mask)-position" etc.) will
|
|
53
|
+
* be generated accordingly. Default value is "background".
|
|
54
|
+
*/
|
|
55
|
+
spriteFillType?: "background" | "mask";
|
|
56
|
+
/**
|
|
57
|
+
* List of all icon sprites to be generated.
|
|
58
|
+
*
|
|
59
|
+
* - The keys of the dictionary are the module names of the PNG sprites
|
|
60
|
+
* (with ".png" extension). The generated sprite PNG file can be imported
|
|
61
|
+
* from `"virtual:png/[key]"`.
|
|
62
|
+
* - "factor" is the scaling factor (a multiplier for "cssIconSize"). All
|
|
63
|
+
* source PNG files must have the effective pixel size (`cssIconSize *
|
|
64
|
+
* factor`).
|
|
65
|
+
* - "src" specifies the pattern used to build the path of the source PNG
|
|
66
|
+
* files. MUST contain the placeholder "[path]" that will be replaced
|
|
67
|
+
* with the base paths contained in the icon mapping file referred in the
|
|
68
|
+
* option "mappingFile".
|
|
69
|
+
*/
|
|
70
|
+
sprites: Dict<{
|
|
71
|
+
factor: number;
|
|
72
|
+
src: string;
|
|
73
|
+
}>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A plugin for Vite to generate PNG sprite files from multiple PNG source
|
|
77
|
+
* files.
|
|
78
|
+
*
|
|
79
|
+
* @param index
|
|
80
|
+
* URL of the index source file of this plugin.
|
|
81
|
+
*
|
|
82
|
+
* @param options
|
|
83
|
+
* Plugin configuration options.
|
|
84
|
+
*
|
|
85
|
+
* @returns
|
|
86
|
+
* The plugin instance.
|
|
87
|
+
*/
|
|
88
|
+
export declare function pngSpritePlugin(index: string, options: PngSpritePluginOptions): Plugin;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import Jimp from "jimp";
|
|
2
|
+
import { onceFn } from "@open-xchange/vite-helper/utils";
|
|
3
|
+
import { Cache } from "@open-xchange/vite-helper/cache";
|
|
4
|
+
import { SpritePluginHelper } from "./helper.js";
|
|
5
|
+
// constants ==================================================================
|
|
6
|
+
const FILL_VENDOR_PREFIXES = {
|
|
7
|
+
background: [],
|
|
8
|
+
mask: ["webkit"],
|
|
9
|
+
};
|
|
10
|
+
// functions ==================================================================
|
|
11
|
+
/**
|
|
12
|
+
* Generates a CSS length string in pixels (omits unit for zero).
|
|
13
|
+
*
|
|
14
|
+
* @param value
|
|
15
|
+
* The length to be emitted.
|
|
16
|
+
*
|
|
17
|
+
* @returns
|
|
18
|
+
* The passed length with "px" unit.
|
|
19
|
+
*/
|
|
20
|
+
function px(value) {
|
|
21
|
+
return value ? `${value}px` : "0";
|
|
22
|
+
}
|
|
23
|
+
// class PngSpritePluginHelper ================================================
|
|
24
|
+
class PngSpritePluginHelper extends SpritePluginHelper {
|
|
25
|
+
#parseMappingOnce;
|
|
26
|
+
// constructor ------------------------------------------------------------
|
|
27
|
+
constructor(index, options) {
|
|
28
|
+
super({
|
|
29
|
+
pluginIndex: index,
|
|
30
|
+
virtualModules: [options.cssName, ...Object.keys(options.sprites)],
|
|
31
|
+
}, {
|
|
32
|
+
cssIconPadding: 0,
|
|
33
|
+
cssIconSelector: "i.png-icon",
|
|
34
|
+
rootLocaleAttr: "lang",
|
|
35
|
+
spriteColorType: "source",
|
|
36
|
+
spriteFillType: "background",
|
|
37
|
+
...options,
|
|
38
|
+
});
|
|
39
|
+
this.#parseMappingOnce = onceFn(() => this.#parseMapping());
|
|
40
|
+
}
|
|
41
|
+
// public methods ---------------------------------------------------------
|
|
42
|
+
/**
|
|
43
|
+
* Generates an ES source module with CSS markup code for all icons.
|
|
44
|
+
*
|
|
45
|
+
* @returns
|
|
46
|
+
* The source module with CSS markup for all icons.
|
|
47
|
+
*/
|
|
48
|
+
async generateCssMarkupModule() {
|
|
49
|
+
// shortcuts to plugin options
|
|
50
|
+
const { cssName } = this.options;
|
|
51
|
+
// try to resolve cached version of generated file
|
|
52
|
+
return await this.generateModule(cssName, async () => {
|
|
53
|
+
this.info("generating CSS markup %f", cssName);
|
|
54
|
+
// parse mapping file which collects icon paths, CSS selectors, and entry positions
|
|
55
|
+
const cssMarkup = (await this.#parseMappingOnce()).cssMarkup;
|
|
56
|
+
this.info("CSS markup %f generated successfully", cssName);
|
|
57
|
+
return cssMarkup;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Generates the ES source module for a PNG sprite with a specific scaling
|
|
62
|
+
* factor.
|
|
63
|
+
*
|
|
64
|
+
* @param spriteName
|
|
65
|
+
* The name of the sprite.
|
|
66
|
+
*
|
|
67
|
+
* @param factor
|
|
68
|
+
* The scaling factor.
|
|
69
|
+
*
|
|
70
|
+
* @param srcPattern
|
|
71
|
+
* The pattern for the source PNG images.
|
|
72
|
+
*
|
|
73
|
+
* @returns
|
|
74
|
+
* The source module exporting the PNG sprite as base-64 encoded data URL.
|
|
75
|
+
*/
|
|
76
|
+
async generateSpriteModule(spriteName, factor, srcPattern) {
|
|
77
|
+
// shortcuts to plugin options
|
|
78
|
+
const { imagesPath, cssIconSize, cssIconPadding, spriteColorType } = this.options;
|
|
79
|
+
// check configuration
|
|
80
|
+
this.ensure(factor > 0, "invalid scaling factor in configuration for sprite %f", spriteName);
|
|
81
|
+
this.ensure(srcPattern.includes("[path]"), "placeholder [path] expected in configuration for sprite %f", spriteName);
|
|
82
|
+
// try to resolve cached version of generated file
|
|
83
|
+
return await this.generateModule(spriteName, async () => {
|
|
84
|
+
this.info("generating sprite %f", spriteName);
|
|
85
|
+
// parse mapping file which collects icon paths, CSS selectors, and entry positions
|
|
86
|
+
const { cssSpriteWidth, cssSpriteHeight, entries } = await this.#parseMappingOnce();
|
|
87
|
+
// new image data is not clean out-of-the-box, explicitly fill with zeros
|
|
88
|
+
const sprite = new Jimp(cssSpriteWidth * factor, cssSpriteHeight * factor, 0);
|
|
89
|
+
// process all entries in the mapping configuration
|
|
90
|
+
for (const { iconPath, x, y } of entries) {
|
|
91
|
+
// expected pixel size for the current scaling factor
|
|
92
|
+
const size = cssIconSize * factor;
|
|
93
|
+
// load and parse the source image file
|
|
94
|
+
const path = imagesPath + "/" + srcPattern.replace("[path]", iconPath);
|
|
95
|
+
const buffer = await this.readBuffer(path);
|
|
96
|
+
const image = await Jimp.read(buffer);
|
|
97
|
+
// validate the image size
|
|
98
|
+
const { width, height } = image.bitmap;
|
|
99
|
+
this.ensure((width === size) && (height === size), "wrong image width in %f (expected %s but got %s)", path, `${size}x${size}`, `${width}x${height}`);
|
|
100
|
+
// insert source image into the sprite
|
|
101
|
+
sprite.blit(image, (x + cssIconPadding) * factor, (y + cssIconPadding) * factor);
|
|
102
|
+
}
|
|
103
|
+
// copy alpha of all pixels to RGB channels (alpha channel will be exported as greyscale without alpha)
|
|
104
|
+
if (spriteColorType === "alpha") {
|
|
105
|
+
for (let d = sprite.bitmap.data, i = 0, l = d.length; i < l; i += 4) {
|
|
106
|
+
d.fill(d[i + 3], i, i + 3); // copy A to RGB
|
|
107
|
+
d[i + 3] = 255; // set A to full opacity
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// generate the binary PNG data (no preset constants for PNG color types in Jimp)
|
|
111
|
+
const pngColorType = { source: 6, monochrome: 4, alpha: 0 }[spriteColorType];
|
|
112
|
+
const spriteBuffer = await sprite.colorType(pngColorType).getBufferAsync(Jimp.MIME_PNG);
|
|
113
|
+
// convert to base64 encoded data URL
|
|
114
|
+
const spriteDataUrl = `data:image/png;base64,${spriteBuffer.toString("base64")}`;
|
|
115
|
+
const moduleSource = `export default ${JSON.stringify(spriteDataUrl)};`;
|
|
116
|
+
this.info("sprite %f generated successfully", spriteName);
|
|
117
|
+
return moduleSource;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// private methods --------------------------------------------------------
|
|
121
|
+
// parses the mapping file once, generates a map from short icon paths to `SpriteEntry` descriptor objects
|
|
122
|
+
async #parseMapping() {
|
|
123
|
+
// shortcuts to plugin options
|
|
124
|
+
const { cssIconSize, cssIconPadding, cssIconSelector, rootLocaleAttr, spriteFillType } = this.options;
|
|
125
|
+
// CSS selectors for all sprite entries, mapped by short icon path (different icons may refer to the same source PNG)
|
|
126
|
+
const selectorMap = new Cache();
|
|
127
|
+
// process all entries in the mapping configuration, collect CSS selectors for each entry in the sprite
|
|
128
|
+
for (const { iconId, iconPath, iconLang } of await this.readIconMapping()) {
|
|
129
|
+
const selectors = selectorMap.upsert(iconPath, () => []);
|
|
130
|
+
const selector = `${cssIconSelector}[data-icon-id="${iconId}"]`;
|
|
131
|
+
if (iconLang) {
|
|
132
|
+
selectors.push(`:root[${rootLocaleAttr}="${iconLang}"] ${selector}`);
|
|
133
|
+
selectors.push(`:root[${rootLocaleAttr}^="${iconLang}_"] ${selector}`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
selectors.push(selector);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// the size of one icon occupied in the sprite including padding, for scaling factor 1
|
|
140
|
+
const cssTileSize = cssIconSize + 2 * cssIconPadding;
|
|
141
|
+
// number of distinct icons in the sprites
|
|
142
|
+
const iconCount = selectorMap.size;
|
|
143
|
+
// generate a square-shaped sprite
|
|
144
|
+
const rowLength = Math.ceil(Math.sqrt(iconCount));
|
|
145
|
+
// the resulting size of a sprite, for scaling factor 1
|
|
146
|
+
const cssSpriteWidth = cssTileSize * rowLength;
|
|
147
|
+
const cssSpriteHeight = cssTileSize * Math.ceil(iconCount / rowLength);
|
|
148
|
+
// additional vendor prefixes for background/fill attributes
|
|
149
|
+
const prefixes = FILL_VENDOR_PREFIXES[spriteFillType];
|
|
150
|
+
// generates a background/mask CSS property definition with correct vendor prefixes
|
|
151
|
+
const fillProp = (name, value) => {
|
|
152
|
+
const prop = `${spriteFillType}-${name}: ${value};`;
|
|
153
|
+
return prefixes.map(prefix => `-${prefix}-${prop} `).join("") + prop;
|
|
154
|
+
};
|
|
155
|
+
// the contents of the resulting CSS file with icon definitions
|
|
156
|
+
const cssLines = [
|
|
157
|
+
`${cssIconSelector} {`,
|
|
158
|
+
" display: inline-block;",
|
|
159
|
+
` width: ${px(cssTileSize)};`,
|
|
160
|
+
` height: ${px(cssTileSize)};`,
|
|
161
|
+
" flex: 0 0 auto;",
|
|
162
|
+
` ${fillProp("size", `${px(cssSpriteWidth)} ${px(cssSpriteHeight)}`)}`,
|
|
163
|
+
` ${fillProp("origin", "content-box")}`,
|
|
164
|
+
"}",
|
|
165
|
+
`${cssIconSelector}[data-icon-id="none"] { visibility: hidden; }`,
|
|
166
|
+
];
|
|
167
|
+
// generate all sprite entries as an array
|
|
168
|
+
const entries = Array.from(selectorMap, ([iconPath, selectors], index) => {
|
|
169
|
+
const x = cssTileSize * (index % rowLength);
|
|
170
|
+
const y = cssTileSize * Math.floor(index / rowLength);
|
|
171
|
+
const prop = fillProp("position", `${px(-x)} ${px(-y)}`);
|
|
172
|
+
for (const selector of selectors) {
|
|
173
|
+
cssLines.push(`${selector} { ${prop} }`);
|
|
174
|
+
}
|
|
175
|
+
return { iconPath, x, y };
|
|
176
|
+
});
|
|
177
|
+
// create the resulting CSS markup as string
|
|
178
|
+
const cssMarkup = cssLines.join("\n") + "\n";
|
|
179
|
+
return { cssMarkup, cssSpriteWidth, cssSpriteHeight, entries };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// plugin =====================================================================
|
|
183
|
+
/**
|
|
184
|
+
* A plugin for Vite to generate PNG sprite files from multiple PNG source
|
|
185
|
+
* files.
|
|
186
|
+
*
|
|
187
|
+
* @param index
|
|
188
|
+
* URL of the index source file of this plugin.
|
|
189
|
+
*
|
|
190
|
+
* @param options
|
|
191
|
+
* Plugin configuration options.
|
|
192
|
+
*
|
|
193
|
+
* @returns
|
|
194
|
+
* The plugin instance.
|
|
195
|
+
*/
|
|
196
|
+
export function pngSpritePlugin(index, options) {
|
|
197
|
+
// resolved configuration options
|
|
198
|
+
const { cssName, sprites } = options;
|
|
199
|
+
// helper instance for file system access, logging, etc.
|
|
200
|
+
const helper = new PngSpritePluginHelper(index, options);
|
|
201
|
+
// create and return the plugin object
|
|
202
|
+
return {
|
|
203
|
+
name: "@open-xchange/vite-plugin-icon-sprite/png",
|
|
204
|
+
// initialize file system cache for generated modules
|
|
205
|
+
async configResolved(viteConfig) {
|
|
206
|
+
await helper.initializeCache(viteConfig);
|
|
207
|
+
},
|
|
208
|
+
// pick matching imports
|
|
209
|
+
resolveId(source) {
|
|
210
|
+
return helper.resolveVirtualModuleId(source);
|
|
211
|
+
},
|
|
212
|
+
// load the PNG icon files, generate final PNG sprites and CSS file
|
|
213
|
+
async load(moduleId) {
|
|
214
|
+
// only handle the target modules specified in "resolveId"
|
|
215
|
+
const target = helper.matchVirtualModuleId(moduleId);
|
|
216
|
+
if (!target) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// generate the CSS markup
|
|
220
|
+
if (target === cssName) {
|
|
221
|
+
return await helper.generateCssMarkupModule();
|
|
222
|
+
}
|
|
223
|
+
// generate a PNG sprite (export binary data as plain Base64 encoded)
|
|
224
|
+
if (target in sprites) {
|
|
225
|
+
const { factor, src } = sprites[target];
|
|
226
|
+
return await helper.generateSpriteModule(target, factor, src);
|
|
227
|
+
}
|
|
228
|
+
// invalid module identifier
|
|
229
|
+
helper.fail("unknown output file %f", target);
|
|
230
|
+
return undefined;
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import { type SpritePluginBaseOptions } from "./helper.js";
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for generating an SVG sprite from SVG source images.
|
|
5
|
+
*/
|
|
6
|
+
export interface SvgSpritePluginOptions extends SpritePluginBaseOptions<"svg"> {
|
|
7
|
+
/**
|
|
8
|
+
* Module name of the SVG sprite to be generated (with ".svg" extension).
|
|
9
|
+
* The SVG markup of the sprite can be imported from the virtual module
|
|
10
|
+
* path `"virtual:svg/[spriteName]"`.
|
|
11
|
+
*/
|
|
12
|
+
spriteName: string;
|
|
13
|
+
/**
|
|
14
|
+
* A prefix to be added to all icon identifiers declared in the mapping
|
|
15
|
+
* file. By default, no prefix will be added.
|
|
16
|
+
*/
|
|
17
|
+
idPrefix?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A plugin for Vite to generate an SVG sprite file from multiple SVG source
|
|
21
|
+
* files.
|
|
22
|
+
*
|
|
23
|
+
* @param index
|
|
24
|
+
* URL of the index source file of this plugin.
|
|
25
|
+
*
|
|
26
|
+
* @param options
|
|
27
|
+
* Plugin configuration options.
|
|
28
|
+
*
|
|
29
|
+
* @returns
|
|
30
|
+
* The plugin instance.
|
|
31
|
+
*/
|
|
32
|
+
export declare function svgSpritePlugin(index: string, options: SvgSpritePluginOptions): Plugin;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import SVGSprite from "svg-sprite";
|
|
3
|
+
import { SpritePluginHelper } from "./helper.js";
|
|
4
|
+
// class SvgSpritePluginHelper ================================================
|
|
5
|
+
class SvgSpritePluginHelper extends SpritePluginHelper {
|
|
6
|
+
constructor(index, options) {
|
|
7
|
+
super({
|
|
8
|
+
pluginIndex: index,
|
|
9
|
+
virtualModules: options.spriteName,
|
|
10
|
+
}, {
|
|
11
|
+
idPrefix: "",
|
|
12
|
+
...options,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
// public methods ---------------------------------------------------------
|
|
16
|
+
/**
|
|
17
|
+
* Generates the ES source module for an SVG sprite.
|
|
18
|
+
*
|
|
19
|
+
* @returns
|
|
20
|
+
* The source module containing the SVG markup of the sprite.
|
|
21
|
+
*/
|
|
22
|
+
async generateSpriteModule() {
|
|
23
|
+
// shortcuts to plugin options
|
|
24
|
+
const { imagesPath, idPrefix, spriteName } = this.options;
|
|
25
|
+
// try to resolve cached version of generated file
|
|
26
|
+
return await this.generateModule(spriteName, async () => {
|
|
27
|
+
this.info("compiling sprite %f", spriteName);
|
|
28
|
+
// create a reverse map from icon filenames to icon identifiers
|
|
29
|
+
const iconIdMap = new Map();
|
|
30
|
+
// create the core SVG spriter
|
|
31
|
+
const spriter = new SVGSprite({
|
|
32
|
+
shape: {
|
|
33
|
+
id: {
|
|
34
|
+
generator: name => iconIdMap.get(name) ?? "",
|
|
35
|
+
},
|
|
36
|
+
spacing: {
|
|
37
|
+
box: "icon",
|
|
38
|
+
},
|
|
39
|
+
dimension: {
|
|
40
|
+
maxWidth: 16,
|
|
41
|
+
maxHeight: 16,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
svg: {
|
|
45
|
+
xmlDeclaration: false,
|
|
46
|
+
// - remove all <style> elements declaring a class with fill color
|
|
47
|
+
// - remove all class attribute referring to the class from <style>
|
|
48
|
+
// - add "fill" attribute to all <symbol> elements
|
|
49
|
+
transform: markup => markup
|
|
50
|
+
.replace(/<style>.*?<\/style>/g, "")
|
|
51
|
+
.replace(/class=".*?" ?/g, "")
|
|
52
|
+
.replace(/<symbol /g, '<symbol fill="currentColor" '),
|
|
53
|
+
},
|
|
54
|
+
mode: {
|
|
55
|
+
inline: true,
|
|
56
|
+
symbol: {
|
|
57
|
+
sprite: spriteName,
|
|
58
|
+
render: { less: false },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
// load all source SVG images
|
|
63
|
+
for (const { iconId, iconPath, iconLang } of await this.readIconMapping()) {
|
|
64
|
+
const path = `${imagesPath}/${iconPath}.svg`;
|
|
65
|
+
// icon identifier with configured prefix and language suffix
|
|
66
|
+
const idSuffix = iconLang ? `;${iconLang}` : "";
|
|
67
|
+
iconIdMap.set(basename(path), idPrefix + iconId + idSuffix);
|
|
68
|
+
// read SVG source image, insert into spriter instance
|
|
69
|
+
const markup = await this.readText(path);
|
|
70
|
+
spriter.add(path, null, markup);
|
|
71
|
+
}
|
|
72
|
+
// compile the SVG sprite
|
|
73
|
+
const result = (await spriter.compileAsync()).result;
|
|
74
|
+
const spriteMarkup = result.symbol.sprite.contents.toString("utf8");
|
|
75
|
+
const moduleCode = `export default ${JSON.stringify(spriteMarkup)};`;
|
|
76
|
+
this.info("sprite %f generated successfully", spriteName);
|
|
77
|
+
return moduleCode;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// plugin =====================================================================
|
|
82
|
+
/**
|
|
83
|
+
* A plugin for Vite to generate an SVG sprite file from multiple SVG source
|
|
84
|
+
* files.
|
|
85
|
+
*
|
|
86
|
+
* @param index
|
|
87
|
+
* URL of the index source file of this plugin.
|
|
88
|
+
*
|
|
89
|
+
* @param options
|
|
90
|
+
* Plugin configuration options.
|
|
91
|
+
*
|
|
92
|
+
* @returns
|
|
93
|
+
* The plugin instance.
|
|
94
|
+
*/
|
|
95
|
+
export function svgSpritePlugin(index, options) {
|
|
96
|
+
// helper instance for file system access, logging, etc.
|
|
97
|
+
const helper = new SvgSpritePluginHelper(index, options);
|
|
98
|
+
// create and return the plugin object
|
|
99
|
+
return {
|
|
100
|
+
name: "@open-xchange/vite-plugin-icon-sprite/svg",
|
|
101
|
+
// initialize file system cache for generated modules
|
|
102
|
+
async configResolved(viteConfig) {
|
|
103
|
+
await helper.initializeCache(viteConfig);
|
|
104
|
+
},
|
|
105
|
+
// pick matching imports
|
|
106
|
+
resolveId(source) {
|
|
107
|
+
return helper.resolveVirtualModuleId(source);
|
|
108
|
+
},
|
|
109
|
+
// load the SVG icon files, generate final SVG sprite
|
|
110
|
+
async load(moduleId) {
|
|
111
|
+
// only handle the output file specified in "resolveId"
|
|
112
|
+
if (!helper.matchVirtualModuleId(moduleId)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
// create an ES module that exports the SVG markup of the sprite
|
|
116
|
+
return await helper.generateSpriteModule();
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-xchange/vite-plugin-icon-sprite",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Vite plugin that builds icon sprites from SVG or PNG icon files",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://gitlab.open-xchange.com/fspd/npm-packages/vite-plugin-icon-sprite"
|
|
7
|
+
},
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": "18.18.0 || ^20.9.0 || >=21.1.0"
|
|
11
|
+
},
|
|
12
|
+
"packageManager": "yarn@4.4.0",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"exports": "./dist/index.js",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"prepare": "husky",
|
|
17
|
+
"prepack": "yarn build && yarn lint",
|
|
18
|
+
"build": "npx --yes rimraf dist && tsc && npx --yes cpy-cli --flat src/*.json dist/",
|
|
19
|
+
"lint": "eslint ."
|
|
20
|
+
},
|
|
21
|
+
"lint-staged": {
|
|
22
|
+
"*.{js,ts,json}": "yarn lint"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@open-xchange/vite-helper": "0.1.0",
|
|
26
|
+
"jimp": "0.22.12",
|
|
27
|
+
"svg-sprite": "2.0.4"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@open-xchange/linter-presets": "0.4.3",
|
|
31
|
+
"@types/node": "22.1.0",
|
|
32
|
+
"@types/svg-sprite": "0.0.39",
|
|
33
|
+
"@types/vinyl": "2.0.12",
|
|
34
|
+
"eslint": "9.8.0",
|
|
35
|
+
"husky": "9.1.4",
|
|
36
|
+
"typescript": "5.5.4",
|
|
37
|
+
"vite": "5.4.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"vite": "^5.3"
|
|
41
|
+
},
|
|
42
|
+
"resolutions": {
|
|
43
|
+
"semver": "^7.6.2"
|
|
44
|
+
}
|
|
45
|
+
}
|