svg-icon-baker 1.2.0 → 2.0.0
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/LICENSE +0 -39
- package/README.md +190 -26
- package/dist/index.d.mts +33 -2
- package/dist/index.d.ts +33 -2
- package/dist/index.mjs +697 -1
- package/package.json +13 -7
package/LICENSE
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
# svg-icon-baker license
|
|
2
|
-
|
|
3
|
-
svg-icon-baker is released under the MIT license:
|
|
4
|
-
|
|
5
1
|
MIT License
|
|
6
2
|
|
|
7
3
|
Copyright (c) 2025-present, yangxu52
|
|
@@ -23,38 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
23
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
21
|
SOFTWARE.
|
|
26
|
-
|
|
27
|
-
# Licenses of bundled dependencies
|
|
28
|
-
|
|
29
|
-
The published svg-icon-baker artifact additionally contains code with the following licenses:
|
|
30
|
-
MIT
|
|
31
|
-
|
|
32
|
-
# Bundled dependencies:
|
|
33
|
-
|
|
34
|
-
## fast-glob
|
|
35
|
-
|
|
36
|
-
License: MIT
|
|
37
|
-
by: Kir Belevich
|
|
38
|
-
Repository: https://github.com/svg/svgo
|
|
39
|
-
|
|
40
|
-
> MIT License
|
|
41
|
-
>
|
|
42
|
-
> Copyright (c) Kir Belevich
|
|
43
|
-
>
|
|
44
|
-
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
45
|
-
> of this software and associated documentation files (the "Software"), to deal
|
|
46
|
-
> in the Software without restriction, including without limitation the rights
|
|
47
|
-
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
48
|
-
> copies of the Software, and to permit persons to whom the Software is
|
|
49
|
-
> furnished to do so, subject to the following conditions:
|
|
50
|
-
>
|
|
51
|
-
> The above copyright notice and this permission notice shall be included in all
|
|
52
|
-
> copies or substantial portions of the Software.
|
|
53
|
-
>
|
|
54
|
-
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
55
|
-
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
56
|
-
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
57
|
-
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
58
|
-
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
59
|
-
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
60
|
-
> SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,49 +2,196 @@
|
|
|
2
2
|
|
|
3
3
|
> Bake the `svg` icon into `symbol` 🍪
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`svg-icon-baker` is the core library for transforming SVG icons into optimized SVG symbol sprites.
|
|
6
|
+
It converts SVG content into `<symbol>` markup for SVG sprite usage while preserving structure and handling local id references safely across multiple icons.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pnpm add svg-icon-baker
|
|
12
|
+
```
|
|
8
13
|
|
|
9
14
|
## Usage
|
|
10
15
|
|
|
11
16
|
```ts
|
|
12
|
-
import { bakeIcon, bakeIcons } from 'svg-icon-baker'
|
|
17
|
+
import { bakeIcon, bakeIcons, createBaker } from 'svg-icon-baker'
|
|
18
|
+
|
|
19
|
+
const source = {
|
|
20
|
+
name: 'icon-home',
|
|
21
|
+
content: '<svg viewBox="0 0 16 16"><path d="..."/></svg>',
|
|
22
|
+
}
|
|
13
23
|
|
|
14
|
-
const source = { name: 'home', content: '<svg viewBox="0 0 16 16">...</svg>' }
|
|
15
24
|
const result = bakeIcon(source)
|
|
16
|
-
// result: { name: 'home', content: '<symbol id="home" viewBox="0 0 16 16">...</symbol>' }
|
|
17
25
|
|
|
18
|
-
|
|
26
|
+
console.log(result)
|
|
27
|
+
// {
|
|
28
|
+
// name: 'icon-home',
|
|
29
|
+
// content: '<symbol id="icon-home" viewBox="0 0 16 16">...</symbol>',
|
|
30
|
+
// issues: []
|
|
31
|
+
// }
|
|
32
|
+
|
|
33
|
+
const batch = bakeIcons([source])
|
|
34
|
+
|
|
35
|
+
const baker = createBaker({
|
|
36
|
+
idPolicy: { idStyle: 'named', delim: '_' },
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const reused = baker.bakeIcon(source)
|
|
19
40
|
```
|
|
20
41
|
|
|
21
|
-
`bakeIcon` and `
|
|
42
|
+
`bakeIcon`, `bakeIcons`, and `createBaker(...).bakeIcon()` are synchronous.
|
|
22
43
|
|
|
23
44
|
## API
|
|
24
45
|
|
|
25
46
|
### `bakeIcon(source: BakeSource, options?: Options): BakeResult`
|
|
26
47
|
|
|
27
|
-
Convert one SVG into one symbol result.
|
|
48
|
+
Convert one SVG input into one symbol result.
|
|
28
49
|
|
|
29
50
|
### `bakeIcons(sources: BakeSource[], options?: Options): BakeResult[]`
|
|
30
51
|
|
|
31
|
-
Convert multiple SVG inputs with one
|
|
52
|
+
Convert multiple SVG inputs with one resolved option set.
|
|
53
|
+
|
|
54
|
+
### `createBaker(options?: Options): Baker`
|
|
55
|
+
|
|
56
|
+
Create a reusable baker instance with one resolved option set.
|
|
57
|
+
This is the preferred API when the same options are applied to many icons.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
type Baker = {
|
|
61
|
+
bakeIcon(source: BakeSource): BakeResult
|
|
62
|
+
bakeIcons(sources: BakeSource[]): BakeResult[]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Return Value
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
type BakeResult = {
|
|
70
|
+
name: string
|
|
71
|
+
content: string
|
|
72
|
+
issues: BakeIssue[]
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- `name` is the original source name.
|
|
77
|
+
- `content` is the generated `<symbol>...</symbol>` markup.
|
|
78
|
+
- `issues` contains non-fatal diagnostics. Conversion still succeeds when issues are reported.
|
|
32
79
|
|
|
33
80
|
## Options
|
|
34
81
|
|
|
35
|
-
| name | type
|
|
36
|
-
| ------------- |
|
|
37
|
-
| `optimize` | `boolean`
|
|
38
|
-
| `svgoOptions` | `SvgoOptions`
|
|
82
|
+
| name | type | default | description |
|
|
83
|
+
| ------------- | ----------------- | ----------------------------------------------------------------------- | -------------------------------------------- |
|
|
84
|
+
| `optimize` | `boolean` | `true` | Enable the built-in safe SVGO optimization. |
|
|
85
|
+
| `svgoOptions` | `SvgoOptions` | `{}` | Merge custom SVGO options into optimization. |
|
|
86
|
+
| `idPolicy` | `IdPolicyOptions` | `{ rewrite: true, unresolved: 'prefix', idStyle: 'named', delim: '_' }` | Control local id rewriting for sprite use. |
|
|
87
|
+
|
|
88
|
+
### `idPolicy`
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
type IdPolicyOptions = {
|
|
92
|
+
rewrite?: boolean
|
|
93
|
+
unresolved?: 'prefix' | 'preserve'
|
|
94
|
+
idStyle?: 'named' | 'minified' | 'hashed'
|
|
95
|
+
delim?: '-' | '_'
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
| name | type | default | description |
|
|
100
|
+
| ------------ | ----------------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
101
|
+
| `rewrite` | `boolean` | `true` | Rewrite local ids and local references for sprite safety. |
|
|
102
|
+
| `unresolved` | `'prefix' \| 'preserve'` | `'prefix'` | For unresolved local references, either rewrite them into the icon namespace or keep them unchanged. Both behaviors report an issue. |
|
|
103
|
+
| `idStyle` | `'named' \| 'minified' \| 'hashed'` | `'named'` | Choose readable ids such as `icon_home`, short ids such as `icon_a`, or hashed ids such as `icon_k9x2m`. |
|
|
104
|
+
| `delim` | `'-' \| '_'` | `'_'` | Separator between the icon name and the generated id part. |
|
|
105
|
+
|
|
106
|
+
## Diagnostics
|
|
107
|
+
|
|
108
|
+
### Issues
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
type BakeIssue = {
|
|
112
|
+
code: 'ResolveReferenceFailed' | 'DetectDefinitionDuplicate' | 'DetectReferenceCarrierUnsupported' | 'ParseStyleFailed'
|
|
113
|
+
message: string
|
|
114
|
+
targetId?: string
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Issues are returned in `BakeResult.issues` instead of throwing.
|
|
119
|
+
Currently emitted issue codes include:
|
|
120
|
+
|
|
121
|
+
- unresolved local references
|
|
122
|
+
- duplicate local definitions
|
|
123
|
+
- style content that cannot be safely parsed
|
|
124
|
+
|
|
125
|
+
`ParseStyleFailed` means one or more `<style>` blocks could not be safely parsed.
|
|
126
|
+
In that case, the original style content is preserved and conversion still succeeds.
|
|
127
|
+
|
|
128
|
+
`DetectReferenceCarrierUnsupported` is reserved for unsupported local reference carriers.
|
|
129
|
+
It is part of the public type contract, but it is not currently emitted by the implementation.
|
|
130
|
+
|
|
131
|
+
Example:
|
|
39
132
|
|
|
40
|
-
|
|
133
|
+
```ts
|
|
134
|
+
const result = bakeIcon(
|
|
135
|
+
{
|
|
136
|
+
name: 'icon-alert',
|
|
137
|
+
content: '<svg viewBox="0 0 16 16"><use href="#ghost"/></svg>',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
idPolicy: { unresolved: 'preserve' },
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
console.log(result.issues)
|
|
145
|
+
// [
|
|
146
|
+
// {
|
|
147
|
+
// code: 'ResolveReferenceFailed',
|
|
148
|
+
// message: 'Resolve reference failed for local target "ghost"; reference was preserved.',
|
|
149
|
+
// targetId: 'ghost'
|
|
150
|
+
// }
|
|
151
|
+
// ]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Errors
|
|
155
|
+
|
|
156
|
+
Fatal failures throw `BakeError`.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
class BakeError extends Error {
|
|
160
|
+
code: 'ValidateSourceInvalid' | 'ValidateNameInvalid' | 'ValidateSvgRootInvalid' | 'ParseSvgFailed' | 'OptimizeSvgFailed' | 'ResolveViewBoxFailed'
|
|
161
|
+
cause?: unknown
|
|
162
|
+
}
|
|
163
|
+
```
|
|
41
164
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
165
|
+
Typical fatal cases:
|
|
166
|
+
|
|
167
|
+
- missing `name` or `content`
|
|
168
|
+
- invalid icon names
|
|
169
|
+
- input that does not start with an `<svg>` root
|
|
170
|
+
- malformed SVG that cannot be parsed for rewriting
|
|
171
|
+
- SVGO plugin failures
|
|
172
|
+
- output where `viewBox` cannot be determined
|
|
173
|
+
|
|
174
|
+
## SVGO Behavior
|
|
175
|
+
|
|
176
|
+
`svg-icon-baker` uses SVGO as an optimization layer after conversion preparation.
|
|
177
|
+
|
|
178
|
+
- When `optimize` is `true`, the package applies a built-in safe preset.
|
|
179
|
+
- User `svgoOptions` are merged into that optimization step.
|
|
180
|
+
- `removeDimensions` is always applied so the output keeps `viewBox` as the sizing contract.
|
|
181
|
+
- User-supplied `prefixIds` and `cleanupIds` plugins are ignored to keep id rewriting behavior stable.
|
|
182
|
+
|
|
183
|
+
This means local id handling is controlled through `idPolicy`, not through external SVGO id plugins.
|
|
184
|
+
|
|
185
|
+
## Output Guarantees
|
|
186
|
+
|
|
187
|
+
On success, the output keeps these behaviors:
|
|
188
|
+
|
|
189
|
+
- the root is always `<symbol>...</symbol>`
|
|
190
|
+
- the symbol root `id` equals `source.name`
|
|
191
|
+
- `viewBox` is required in the final output
|
|
192
|
+
- root `width` and `height` are removed
|
|
193
|
+
- non-structural root attributes such as `fill` are preserved
|
|
194
|
+
- XML declarations, BOM, comments, and doctype preamble are stripped
|
|
48
195
|
|
|
49
196
|
## Type Definitions
|
|
50
197
|
|
|
@@ -59,22 +206,39 @@ type BakeSource = {
|
|
|
59
206
|
type BakeResult = {
|
|
60
207
|
name: string
|
|
61
208
|
content: string
|
|
209
|
+
issues: BakeIssue[]
|
|
62
210
|
}
|
|
63
211
|
|
|
212
|
+
type BakeIssue = {
|
|
213
|
+
code: 'ResolveReferenceFailed' | 'DetectDefinitionDuplicate' | 'DetectReferenceCarrierUnsupported' | 'ParseStyleFailed'
|
|
214
|
+
message: string
|
|
215
|
+
targetId?: string
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
type BakeErrorCode =
|
|
219
|
+
| 'ValidateSourceInvalid'
|
|
220
|
+
| 'ValidateNameInvalid'
|
|
221
|
+
| 'ValidateSvgRootInvalid'
|
|
222
|
+
| 'ParseSvgFailed'
|
|
223
|
+
| 'OptimizeSvgFailed'
|
|
224
|
+
| 'ResolveViewBoxFailed'
|
|
225
|
+
|
|
64
226
|
type SvgoOptions = Pick<Config, 'multipass' | 'floatPrecision' | 'js2svg' | 'plugins'>
|
|
65
227
|
|
|
228
|
+
type IdPolicyOptions = {
|
|
229
|
+
rewrite?: boolean
|
|
230
|
+
unresolved?: 'prefix' | 'preserve'
|
|
231
|
+
idStyle?: 'named' | 'minified' | 'hashed'
|
|
232
|
+
delim?: '-' | '_'
|
|
233
|
+
}
|
|
234
|
+
|
|
66
235
|
type Options = {
|
|
67
236
|
optimize?: boolean
|
|
68
237
|
svgoOptions?: SvgoOptions
|
|
238
|
+
idPolicy?: IdPolicyOptions
|
|
69
239
|
}
|
|
70
240
|
```
|
|
71
241
|
|
|
72
|
-
## Features
|
|
73
|
-
|
|
74
|
-
- 🎯 Optimization: Reduce file size, and improve efficiency through `SVGO`
|
|
75
|
-
- 🔗 Reference Handling: ID and reference prefixing for sprite safety
|
|
76
|
-
- 🎨 Size Unify: `viewBox` preservation or inference from root dimensions
|
|
77
|
-
|
|
78
242
|
## License
|
|
79
243
|
|
|
80
244
|
MIT © [yangxu52](https://github.com/yangxu52/svg-icon-baker/blob/main/LICENSE)
|
package/dist/index.d.mts
CHANGED
|
@@ -4,9 +4,34 @@ type BakeSource = {
|
|
|
4
4
|
name: string;
|
|
5
5
|
content: string;
|
|
6
6
|
};
|
|
7
|
+
type BakeIssueCode = 'ResolveReferenceFailed' | 'DetectDefinitionDuplicate' | 'DetectReferenceCarrierUnsupported' | 'ParseStyleFailed';
|
|
8
|
+
type BakeIssue = {
|
|
9
|
+
code: BakeIssueCode;
|
|
10
|
+
message: string;
|
|
11
|
+
targetId?: string;
|
|
12
|
+
};
|
|
13
|
+
type BakeErrorCode = 'ValidateSourceInvalid' | 'ValidateNameInvalid' | 'ValidateSvgRootInvalid' | 'ParseSvgFailed' | 'OptimizeSvgFailed' | 'ResolveViewBoxFailed';
|
|
14
|
+
declare class BakeError extends Error {
|
|
15
|
+
readonly code: BakeErrorCode;
|
|
16
|
+
readonly cause?: unknown;
|
|
17
|
+
constructor(code: BakeErrorCode, message: string, options?: {
|
|
18
|
+
cause?: unknown;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
type IdPolicyOptions = {
|
|
22
|
+
rewrite?: boolean;
|
|
23
|
+
unresolved?: 'prefix' | 'preserve';
|
|
24
|
+
idStyle?: 'named' | 'minified' | 'hashed';
|
|
25
|
+
delim?: '-' | '_';
|
|
26
|
+
};
|
|
7
27
|
type BakeResult = {
|
|
8
28
|
name: string;
|
|
9
29
|
content: string;
|
|
30
|
+
issues: BakeIssue[];
|
|
31
|
+
};
|
|
32
|
+
type Baker = {
|
|
33
|
+
bakeIcon(source: BakeSource): BakeResult;
|
|
34
|
+
bakeIcons(sources: BakeSource[]): BakeResult[];
|
|
10
35
|
};
|
|
11
36
|
type SvgoOptions = Pick<Config, 'multipass' | 'floatPrecision' | 'js2svg' | 'plugins'>;
|
|
12
37
|
type Options = {
|
|
@@ -19,10 +44,16 @@ type Options = {
|
|
|
19
44
|
* custom svgo options merged into optimizer
|
|
20
45
|
*/
|
|
21
46
|
svgoOptions?: SvgoOptions;
|
|
47
|
+
/**
|
|
48
|
+
* sprite-safe local id rewriting
|
|
49
|
+
* @default { rewrite: true, unresolved: 'prefix', idStyle: 'named', delim: '_' }
|
|
50
|
+
*/
|
|
51
|
+
idPolicy?: IdPolicyOptions;
|
|
22
52
|
};
|
|
23
53
|
|
|
24
54
|
declare function bakeIcon(source: BakeSource, options?: Options): BakeResult;
|
|
25
55
|
declare function bakeIcons(sources: BakeSource[], options?: Options): BakeResult[];
|
|
56
|
+
declare function createBaker(options?: Options): Baker;
|
|
26
57
|
|
|
27
|
-
export { bakeIcon, bakeIcons };
|
|
28
|
-
export type { BakeResult, BakeSource, Options, SvgoOptions };
|
|
58
|
+
export { BakeError, bakeIcon, bakeIcons, createBaker };
|
|
59
|
+
export type { BakeErrorCode, BakeIssue, BakeIssueCode, BakeResult, BakeSource, Baker, IdPolicyOptions, Options, SvgoOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -4,9 +4,34 @@ type BakeSource = {
|
|
|
4
4
|
name: string;
|
|
5
5
|
content: string;
|
|
6
6
|
};
|
|
7
|
+
type BakeIssueCode = 'ResolveReferenceFailed' | 'DetectDefinitionDuplicate' | 'DetectReferenceCarrierUnsupported' | 'ParseStyleFailed';
|
|
8
|
+
type BakeIssue = {
|
|
9
|
+
code: BakeIssueCode;
|
|
10
|
+
message: string;
|
|
11
|
+
targetId?: string;
|
|
12
|
+
};
|
|
13
|
+
type BakeErrorCode = 'ValidateSourceInvalid' | 'ValidateNameInvalid' | 'ValidateSvgRootInvalid' | 'ParseSvgFailed' | 'OptimizeSvgFailed' | 'ResolveViewBoxFailed';
|
|
14
|
+
declare class BakeError extends Error {
|
|
15
|
+
readonly code: BakeErrorCode;
|
|
16
|
+
readonly cause?: unknown;
|
|
17
|
+
constructor(code: BakeErrorCode, message: string, options?: {
|
|
18
|
+
cause?: unknown;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
type IdPolicyOptions = {
|
|
22
|
+
rewrite?: boolean;
|
|
23
|
+
unresolved?: 'prefix' | 'preserve';
|
|
24
|
+
idStyle?: 'named' | 'minified' | 'hashed';
|
|
25
|
+
delim?: '-' | '_';
|
|
26
|
+
};
|
|
7
27
|
type BakeResult = {
|
|
8
28
|
name: string;
|
|
9
29
|
content: string;
|
|
30
|
+
issues: BakeIssue[];
|
|
31
|
+
};
|
|
32
|
+
type Baker = {
|
|
33
|
+
bakeIcon(source: BakeSource): BakeResult;
|
|
34
|
+
bakeIcons(sources: BakeSource[]): BakeResult[];
|
|
10
35
|
};
|
|
11
36
|
type SvgoOptions = Pick<Config, 'multipass' | 'floatPrecision' | 'js2svg' | 'plugins'>;
|
|
12
37
|
type Options = {
|
|
@@ -19,10 +44,16 @@ type Options = {
|
|
|
19
44
|
* custom svgo options merged into optimizer
|
|
20
45
|
*/
|
|
21
46
|
svgoOptions?: SvgoOptions;
|
|
47
|
+
/**
|
|
48
|
+
* sprite-safe local id rewriting
|
|
49
|
+
* @default { rewrite: true, unresolved: 'prefix', idStyle: 'named', delim: '_' }
|
|
50
|
+
*/
|
|
51
|
+
idPolicy?: IdPolicyOptions;
|
|
22
52
|
};
|
|
23
53
|
|
|
24
54
|
declare function bakeIcon(source: BakeSource, options?: Options): BakeResult;
|
|
25
55
|
declare function bakeIcons(sources: BakeSource[], options?: Options): BakeResult[];
|
|
56
|
+
declare function createBaker(options?: Options): Baker;
|
|
26
57
|
|
|
27
|
-
export { bakeIcon, bakeIcons };
|
|
28
|
-
export type { BakeResult, BakeSource, Options, SvgoOptions };
|
|
58
|
+
export { BakeError, bakeIcon, bakeIcons, createBaker };
|
|
59
|
+
export type { BakeErrorCode, BakeIssue, BakeIssueCode, BakeResult, BakeSource, Baker, IdPolicyOptions, Options, SvgoOptions };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1,697 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { optimize } from 'svgo';
|
|
2
|
+
import XMLBuilder from 'fast-xml-builder';
|
|
3
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
4
|
+
import * as csstree from 'css-tree';
|
|
5
|
+
|
|
6
|
+
const parser = new XMLParser({
|
|
7
|
+
preserveOrder: true,
|
|
8
|
+
ignoreAttributes: false,
|
|
9
|
+
attributeNamePrefix: "",
|
|
10
|
+
trimValues: false,
|
|
11
|
+
parseTagValue: false,
|
|
12
|
+
parseAttributeValue: false,
|
|
13
|
+
commentPropName: "#comment",
|
|
14
|
+
cdataPropName: "#cdata"
|
|
15
|
+
});
|
|
16
|
+
const builder = new XMLBuilder({
|
|
17
|
+
preserveOrder: true,
|
|
18
|
+
ignoreAttributes: false,
|
|
19
|
+
attributeNamePrefix: "",
|
|
20
|
+
suppressEmptyNode: true,
|
|
21
|
+
commentPropName: "#comment",
|
|
22
|
+
cdataPropName: "#cdata"
|
|
23
|
+
});
|
|
24
|
+
function parseSvg(code) {
|
|
25
|
+
return parser.parse(code);
|
|
26
|
+
}
|
|
27
|
+
function buildSvg(document) {
|
|
28
|
+
return builder.build(document);
|
|
29
|
+
}
|
|
30
|
+
function visitElements(document, visit) {
|
|
31
|
+
for (const node of document) {
|
|
32
|
+
visitNode(node, visit);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function visitNode(node, visit) {
|
|
36
|
+
const entry = getElementEntry(node);
|
|
37
|
+
if (!entry) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
visit(entry.name, entry.node, entry.attrs);
|
|
41
|
+
for (const child of entry.children) {
|
|
42
|
+
visitNode(child, visit);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function getElementEntry(node) {
|
|
46
|
+
if (isMetaNode(node)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const elementNode = node;
|
|
50
|
+
for (const [key, value] of Object.entries(elementNode)) {
|
|
51
|
+
if (key === ":@" || key.startsWith("#")) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(value)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const attrs = elementNode[":@"] ?? {};
|
|
58
|
+
return {
|
|
59
|
+
name: key,
|
|
60
|
+
node: elementNode,
|
|
61
|
+
attrs,
|
|
62
|
+
children: value
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function getRootElementEntry(document) {
|
|
68
|
+
for (const node of document) {
|
|
69
|
+
const entry = getElementEntry(node);
|
|
70
|
+
if (entry) {
|
|
71
|
+
return entry;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function isMetaNode(node) {
|
|
77
|
+
return "#text" in node || "#cdata" in node || "#comment" in node;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function rewriteRoot(document, symbolId) {
|
|
81
|
+
const rootEntry = getRootElementEntry(document);
|
|
82
|
+
if (!rootEntry || rootEntry.name !== "svg") {
|
|
83
|
+
throw new Error("Input must start with an <svg> root element.");
|
|
84
|
+
}
|
|
85
|
+
const viewBox = resolveViewBox(rootEntry.attrs);
|
|
86
|
+
if (!viewBox) {
|
|
87
|
+
throw new Error("Cannot determine viewBox. Provide an SVG with viewBox or width/height attributes.");
|
|
88
|
+
}
|
|
89
|
+
const children = rootEntry.node[rootEntry.name];
|
|
90
|
+
delete rootEntry.node[rootEntry.name];
|
|
91
|
+
rootEntry.node.symbol = children;
|
|
92
|
+
rootEntry.attrs.id = symbolId;
|
|
93
|
+
rootEntry.attrs.viewBox = viewBox;
|
|
94
|
+
delete rootEntry.attrs.width;
|
|
95
|
+
delete rootEntry.attrs.height;
|
|
96
|
+
}
|
|
97
|
+
function resolveViewBox(attrs) {
|
|
98
|
+
const explicitViewBox = attrs.viewBox?.trim();
|
|
99
|
+
if (explicitViewBox) {
|
|
100
|
+
return explicitViewBox;
|
|
101
|
+
}
|
|
102
|
+
const width = resolveDimension(attrs.width);
|
|
103
|
+
const height = resolveDimension(attrs.height);
|
|
104
|
+
if (width == null || height == null) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return `0 0 ${width} ${height}`;
|
|
108
|
+
}
|
|
109
|
+
function resolveDimension(value) {
|
|
110
|
+
if (!value) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const normalized = value.trim();
|
|
114
|
+
if (/^-?\d+(?:\.\d+)?$/.test(normalized)) {
|
|
115
|
+
return String(Number(normalized));
|
|
116
|
+
}
|
|
117
|
+
if (/^-?\d+(?:\.\d+)?px$/i.test(normalized)) {
|
|
118
|
+
return String(Number(normalized.slice(0, -2)));
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const URL_REFERENCE_RE$1 = /\burl\((["'])?#(.+?)\1\)/gi;
|
|
124
|
+
const HREF_REFERENCE_RE$1 = /^#(.+)$/;
|
|
125
|
+
const SMIL_REFERENCE_RE$1 = /^([A-Za-z0-9_-]+)\.(begin|end)([+-].+)?$/;
|
|
126
|
+
function collectIds(document) {
|
|
127
|
+
const elements = [];
|
|
128
|
+
const definedIds = /* @__PURE__ */ new Map();
|
|
129
|
+
const referenceIds = [];
|
|
130
|
+
let styleParseFailureCount = 0;
|
|
131
|
+
let order = 0;
|
|
132
|
+
visitElements(document, (name, node, attrs) => {
|
|
133
|
+
const element = { name, node, attrs };
|
|
134
|
+
elements.push(element);
|
|
135
|
+
for (const attrName of ["id", "xml:id"]) {
|
|
136
|
+
const id = attrs[attrName];
|
|
137
|
+
if (!id) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const records = definedIds.get(id) ?? [];
|
|
141
|
+
records.push({
|
|
142
|
+
id,
|
|
143
|
+
attrName,
|
|
144
|
+
element,
|
|
145
|
+
order
|
|
146
|
+
});
|
|
147
|
+
definedIds.set(id, records);
|
|
148
|
+
order += 1;
|
|
149
|
+
}
|
|
150
|
+
const attributeRefs = collectAttributeReferences(attrs);
|
|
151
|
+
const styleResult = collectStyleReferences(element);
|
|
152
|
+
const styleRefs = styleResult.references;
|
|
153
|
+
styleParseFailureCount += styleResult.parseFailureCount;
|
|
154
|
+
if (attributeRefs.length === 0 && styleRefs.length === 0) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
referenceIds.push({ element, attributeRefs, styleRefs });
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
document,
|
|
161
|
+
elements,
|
|
162
|
+
definedIds,
|
|
163
|
+
referenceIds,
|
|
164
|
+
styleParseFailureCount
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function collectAttributeReferences(attrs) {
|
|
168
|
+
const refs = [];
|
|
169
|
+
for (const [attrName, value] of Object.entries(attrs)) {
|
|
170
|
+
if (!value) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (attrName === "href" || attrName === "xlink:href") {
|
|
174
|
+
const match = HREF_REFERENCE_RE$1.exec(value);
|
|
175
|
+
if (match) {
|
|
176
|
+
refs.push({
|
|
177
|
+
kind: "href",
|
|
178
|
+
attrName,
|
|
179
|
+
targetIds: [decodeURI(match[1])]
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (isUrlReferenceAttribute(attrName)) {
|
|
185
|
+
const targetIds = collectUrlReferences(value);
|
|
186
|
+
if (targetIds.length > 0) {
|
|
187
|
+
refs.push({
|
|
188
|
+
kind: attrName === "style" ? "style-attr" : "url",
|
|
189
|
+
attrName,
|
|
190
|
+
targetIds
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (attrName === "begin" || attrName === "end") {
|
|
196
|
+
const targetIds = collectSmilReferences(value);
|
|
197
|
+
if (targetIds.length > 0) {
|
|
198
|
+
refs.push({
|
|
199
|
+
kind: attrName,
|
|
200
|
+
attrName,
|
|
201
|
+
targetIds
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (attrName === "aria-labelledby" || attrName === "aria-describedby") {
|
|
207
|
+
const targetIds = value.split(/\s+/).filter(Boolean);
|
|
208
|
+
if (targetIds.length > 0) {
|
|
209
|
+
refs.push({
|
|
210
|
+
kind: attrName,
|
|
211
|
+
attrName,
|
|
212
|
+
targetIds
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return refs;
|
|
218
|
+
}
|
|
219
|
+
function collectStyleReferences(element) {
|
|
220
|
+
if (element.name !== "style") {
|
|
221
|
+
return { references: [], parseFailureCount: 0 };
|
|
222
|
+
}
|
|
223
|
+
const entry = getElementEntry(element.node);
|
|
224
|
+
if (!entry) {
|
|
225
|
+
return { references: [], parseFailureCount: 0 };
|
|
226
|
+
}
|
|
227
|
+
const refs = [];
|
|
228
|
+
let parseFailureCount = 0;
|
|
229
|
+
for (const child of entry.children) {
|
|
230
|
+
if (!("#text" in child) || typeof child["#text"] !== "string") {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const result = collectStyleTextReferences(child["#text"]);
|
|
234
|
+
parseFailureCount += result.parseFailureCount;
|
|
235
|
+
const targetIds = result.targetIds;
|
|
236
|
+
if (targetIds.length === 0) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
refs.push({
|
|
240
|
+
kind: "style-selector",
|
|
241
|
+
textNode: child,
|
|
242
|
+
targetIds
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
references: refs,
|
|
247
|
+
parseFailureCount
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function collectUrlReferences(value) {
|
|
251
|
+
const ids = [];
|
|
252
|
+
for (const match of value.matchAll(URL_REFERENCE_RE$1)) {
|
|
253
|
+
ids.push(decodeURI(match[2]));
|
|
254
|
+
}
|
|
255
|
+
return ids;
|
|
256
|
+
}
|
|
257
|
+
function collectSmilReferences(value) {
|
|
258
|
+
return value.split(/\s*;\s*/).map((part) => SMIL_REFERENCE_RE$1.exec(part)?.[1] ?? null).filter((id) => id != null);
|
|
259
|
+
}
|
|
260
|
+
function collectStyleTextReferences(value) {
|
|
261
|
+
const ids = /* @__PURE__ */ new Set();
|
|
262
|
+
let cssAst;
|
|
263
|
+
try {
|
|
264
|
+
cssAst = csstree.parse(value, {
|
|
265
|
+
parseValue: true,
|
|
266
|
+
parseCustomProperty: false
|
|
267
|
+
});
|
|
268
|
+
} catch {
|
|
269
|
+
return {
|
|
270
|
+
targetIds: [],
|
|
271
|
+
parseFailureCount: 1
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
csstree.walk(cssAst, (node) => {
|
|
275
|
+
if (node.type === "IdSelector" && typeof node.name === "string") {
|
|
276
|
+
ids.add(node.name);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (node.type === "Url" && typeof node.value === "string" && node.value.length > 0) {
|
|
280
|
+
const raw = unquote$1(node.value);
|
|
281
|
+
if (raw.startsWith("#")) {
|
|
282
|
+
ids.add(decodeURI(raw.slice(1)));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
return {
|
|
287
|
+
targetIds: [...ids],
|
|
288
|
+
parseFailureCount: 0
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function isUrlReferenceAttribute(attrName) {
|
|
292
|
+
return attrName === "clip-path" || attrName === "color-profile" || attrName === "fill" || attrName === "filter" || attrName === "marker-end" || attrName === "marker-mid" || attrName === "marker-start" || attrName === "mask" || attrName === "stroke" || attrName === "style";
|
|
293
|
+
}
|
|
294
|
+
function unquote$1(value) {
|
|
295
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
296
|
+
return value.slice(1, -1);
|
|
297
|
+
}
|
|
298
|
+
return value;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const URL_REFERENCE_RE = /\burl\((["'])?#(.+?)\1\)/gi;
|
|
302
|
+
const HREF_REFERENCE_RE = /^#(.+)$/;
|
|
303
|
+
const SMIL_REFERENCE_RE = /^([A-Za-z0-9_-]+)\.(begin|end)([+-].+)?$/;
|
|
304
|
+
function rewriteIds(document, prefix, options, issues) {
|
|
305
|
+
const state = collectIds(document);
|
|
306
|
+
collectIssues(state, issues);
|
|
307
|
+
const idMap = createIdMap(state, prefix, options);
|
|
308
|
+
const unresolvedIdMap = /* @__PURE__ */ new Map();
|
|
309
|
+
let nextGeneratedIndex = idMap.size;
|
|
310
|
+
rewriteElementIds(state, idMap);
|
|
311
|
+
rewriteReferences(state, idMap, prefix, unresolvedIdMap, () => nextGeneratedIndex++, options, issues);
|
|
312
|
+
return idMap;
|
|
313
|
+
}
|
|
314
|
+
function createIdMap(state, prefix, options) {
|
|
315
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
316
|
+
const records = [...state.definedIds.entries()].map(([id, definitions]) => ({
|
|
317
|
+
id,
|
|
318
|
+
order: Math.min(...definitions.map((definition) => definition.order))
|
|
319
|
+
})).sort((a, b) => a.order - b.order);
|
|
320
|
+
for (let counter = 0; counter < records.length; counter += 1) {
|
|
321
|
+
const { id } = records[counter];
|
|
322
|
+
idMap.set(id, createMappedId(id, prefix, counter, options));
|
|
323
|
+
}
|
|
324
|
+
return idMap;
|
|
325
|
+
}
|
|
326
|
+
function rewriteElementIds(state, idMap) {
|
|
327
|
+
for (const [id, definitions] of state.definedIds) {
|
|
328
|
+
const canonical = definitions[0];
|
|
329
|
+
canonical.element.attrs[canonical.attrName] = idMap.get(id);
|
|
330
|
+
for (const duplicate of definitions.slice(1)) {
|
|
331
|
+
delete duplicate.element.attrs[duplicate.attrName];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function rewriteReferences(state, idMap, prefix, unresolvedIdMap, nextIndex, options, issues) {
|
|
336
|
+
for (const reference of state.referenceIds) {
|
|
337
|
+
for (const attrRef of reference.attributeRefs) {
|
|
338
|
+
const value = reference.element.attrs[attrRef.attrName];
|
|
339
|
+
if (!value) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (attrRef.kind === "href") {
|
|
343
|
+
reference.element.attrs[attrRef.attrName] = rewriteHrefValue(value, idMap, prefix, unresolvedIdMap, nextIndex, options, issues);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (attrRef.kind === "url" || attrRef.kind === "style-attr" && attrRef.attrName === "style") {
|
|
347
|
+
reference.element.attrs[attrRef.attrName] = rewriteUrlValue(value, idMap, prefix, unresolvedIdMap, nextIndex, options, issues);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (attrRef.kind === "begin" || attrRef.kind === "end") {
|
|
351
|
+
reference.element.attrs[attrRef.attrName] = rewriteSmilValue(value, idMap, prefix, unresolvedIdMap, nextIndex, options, issues);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (attrRef.kind === "aria-labelledby" || attrRef.kind === "aria-describedby") {
|
|
355
|
+
reference.element.attrs[attrRef.attrName] = rewriteTokenList(value, idMap, prefix, unresolvedIdMap, nextIndex, options, issues);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
for (const styleRef of reference.styleRefs) {
|
|
359
|
+
styleRef.textNode["#text"] = rewriteStyleText(styleRef.textNode, idMap, prefix, unresolvedIdMap, nextIndex, options, issues);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function rewriteHrefValue(value, idMap, prefix, unresolvedIdMap, nextIndex, options, issues) {
|
|
364
|
+
const match = HREF_REFERENCE_RE.exec(value);
|
|
365
|
+
if (!match) {
|
|
366
|
+
return value;
|
|
367
|
+
}
|
|
368
|
+
const source = decodeURI(match[1]);
|
|
369
|
+
const next = idMap.get(source);
|
|
370
|
+
if (next) {
|
|
371
|
+
return `#${next}`;
|
|
372
|
+
}
|
|
373
|
+
return rewriteUnresolvedReference(source, value, prefix, unresolvedIdMap, nextIndex, options, issues, (resolved) => `#${resolved}`);
|
|
374
|
+
}
|
|
375
|
+
function rewriteUrlValue(value, idMap, prefix, unresolvedIdMap, nextIndex, options, issues) {
|
|
376
|
+
return value.replace(URL_REFERENCE_RE, (match, quote, body) => {
|
|
377
|
+
const source = decodeURI(body);
|
|
378
|
+
const next = idMap.get(source);
|
|
379
|
+
if (!next) {
|
|
380
|
+
return rewriteUnresolvedReference(source, match, prefix, unresolvedIdMap, nextIndex, options, issues, (resolved) => {
|
|
381
|
+
const wrappedQuote2 = quote ?? "";
|
|
382
|
+
return `url(${wrappedQuote2}#${resolved}${wrappedQuote2})`;
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
const wrappedQuote = quote ?? "";
|
|
386
|
+
return `url(${wrappedQuote}#${next}${wrappedQuote})`;
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function rewriteSmilValue(value, idMap, prefix, unresolvedIdMap, nextIndex, options, issues) {
|
|
390
|
+
const parts = value.split(/\s*;\s*/);
|
|
391
|
+
return parts.map((part) => {
|
|
392
|
+
const match = SMIL_REFERENCE_RE.exec(part);
|
|
393
|
+
if (!match) {
|
|
394
|
+
return part;
|
|
395
|
+
}
|
|
396
|
+
const next = idMap.get(match[1]);
|
|
397
|
+
if (!next) {
|
|
398
|
+
return rewriteUnresolvedReference(match[1], part, prefix, unresolvedIdMap, nextIndex, options, issues, (resolved) => {
|
|
399
|
+
const offset2 = match[3] ?? "";
|
|
400
|
+
return `${resolved}.${match[2]}${offset2}`;
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
const offset = match[3] ?? "";
|
|
404
|
+
return `${next}.${match[2]}${offset}`;
|
|
405
|
+
}).join("; ");
|
|
406
|
+
}
|
|
407
|
+
function rewriteTokenList(value, idMap, prefix, unresolvedIdMap, nextIndex, options, issues) {
|
|
408
|
+
return value.split(/\s+/).filter(Boolean).map((token) => {
|
|
409
|
+
const next = idMap.get(token);
|
|
410
|
+
if (next) {
|
|
411
|
+
return next;
|
|
412
|
+
}
|
|
413
|
+
return rewriteUnresolvedReference(token, token, prefix, unresolvedIdMap, nextIndex, options, issues, (resolved) => resolved);
|
|
414
|
+
}).join(" ");
|
|
415
|
+
}
|
|
416
|
+
function rewriteStyleText(textNode, idMap, prefix, unresolvedIdMap, nextIndex, options, issues) {
|
|
417
|
+
let cssAst;
|
|
418
|
+
try {
|
|
419
|
+
cssAst = csstree.parse(textNode["#text"], {
|
|
420
|
+
parseValue: true,
|
|
421
|
+
parseCustomProperty: false
|
|
422
|
+
});
|
|
423
|
+
} catch {
|
|
424
|
+
return textNode["#text"];
|
|
425
|
+
}
|
|
426
|
+
csstree.walk(cssAst, (node) => {
|
|
427
|
+
if (node.type === "IdSelector" && typeof node.name === "string") {
|
|
428
|
+
const next = idMap.get(node.name);
|
|
429
|
+
if (next) {
|
|
430
|
+
node.name = next;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const rewritten = rewriteUnresolvedReference(node.name, node.name, prefix, unresolvedIdMap, nextIndex, options, issues, (resolved) => resolved);
|
|
434
|
+
if (rewritten !== node.name) {
|
|
435
|
+
node.name = rewritten;
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (node.type === "Url" && typeof node.value === "string" && node.value.length > 0) {
|
|
440
|
+
const raw = unquote(node.value);
|
|
441
|
+
if (!raw.startsWith("#")) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const source = decodeURI(raw.slice(1));
|
|
445
|
+
const next = idMap.get(source);
|
|
446
|
+
if (!next) {
|
|
447
|
+
const rewritten = rewriteUnresolvedReference(source, `#${source}`, prefix, unresolvedIdMap, nextIndex, options, issues, (resolved) => `#${resolved}`);
|
|
448
|
+
if (rewritten !== `#${source}`) {
|
|
449
|
+
node.value = rewritten;
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
node.value = `#${next}`;
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
return csstree.generate(cssAst);
|
|
457
|
+
}
|
|
458
|
+
function encodeIndex(index) {
|
|
459
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
460
|
+
let current = index;
|
|
461
|
+
let result = "";
|
|
462
|
+
do {
|
|
463
|
+
result = alphabet[current % alphabet.length] + result;
|
|
464
|
+
current = Math.floor(current / alphabet.length) - 1;
|
|
465
|
+
} while (current >= 0);
|
|
466
|
+
return result;
|
|
467
|
+
}
|
|
468
|
+
function createMappedId(sourceId, prefix, index, options) {
|
|
469
|
+
const delim = resolveDelim(options);
|
|
470
|
+
const style = options.idStyle;
|
|
471
|
+
if (style === "named") {
|
|
472
|
+
return `${prefix}${delim}${sourceId}`;
|
|
473
|
+
}
|
|
474
|
+
if (style === "hashed") {
|
|
475
|
+
return `${prefix}${delim}${hashId(sourceId)}`;
|
|
476
|
+
}
|
|
477
|
+
return `${prefix}${delim}${encodeIndex(index)}`;
|
|
478
|
+
}
|
|
479
|
+
function hashId(value) {
|
|
480
|
+
let hash = 5381;
|
|
481
|
+
for (const char of value) {
|
|
482
|
+
hash = (hash << 5) + hash ^ char.charCodeAt(0);
|
|
483
|
+
}
|
|
484
|
+
return Math.abs(hash >>> 0).toString(36);
|
|
485
|
+
}
|
|
486
|
+
function createUnresolvedMappedId(sourceId, prefix, unresolvedIdMap, nextIndex, options) {
|
|
487
|
+
const existing = unresolvedIdMap.get(sourceId);
|
|
488
|
+
if (existing) {
|
|
489
|
+
return existing;
|
|
490
|
+
}
|
|
491
|
+
const generated = createMappedId(sourceId, prefix, nextIndex(), options);
|
|
492
|
+
unresolvedIdMap.set(sourceId, generated);
|
|
493
|
+
return generated;
|
|
494
|
+
}
|
|
495
|
+
function rewriteUnresolvedReference(targetId, originalValue, prefix, unresolvedIdMap, nextIndex, options, issues, format) {
|
|
496
|
+
pushIssue(issues, {
|
|
497
|
+
code: "ResolveReferenceFailed",
|
|
498
|
+
message: `Resolve reference failed for local target "${targetId}"; reference was ${options.unresolved === "prefix" ? "prefixed" : "preserved"}.`,
|
|
499
|
+
targetId
|
|
500
|
+
});
|
|
501
|
+
if (options.unresolved === "preserve") {
|
|
502
|
+
return originalValue;
|
|
503
|
+
}
|
|
504
|
+
return format(createUnresolvedMappedId(targetId, prefix, unresolvedIdMap, nextIndex, options));
|
|
505
|
+
}
|
|
506
|
+
function resolveDelim(options) {
|
|
507
|
+
if (options.delim != null) {
|
|
508
|
+
return options.delim;
|
|
509
|
+
}
|
|
510
|
+
return "_";
|
|
511
|
+
}
|
|
512
|
+
function collectIssues(state, issues) {
|
|
513
|
+
for (const [id, definitions] of state.definedIds) {
|
|
514
|
+
if (definitions.length > 1) {
|
|
515
|
+
pushIssue(issues, {
|
|
516
|
+
code: "DetectDefinitionDuplicate",
|
|
517
|
+
message: `Detect definition duplicate for local target "${id}"; first occurrence was kept.`,
|
|
518
|
+
targetId: id
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (state.styleParseFailureCount > 0) {
|
|
523
|
+
pushIssue(issues, {
|
|
524
|
+
code: "ParseStyleFailed",
|
|
525
|
+
message: "Parse style failed for one or more <style> blocks; original content was preserved."
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function pushIssue(issues, issue) {
|
|
530
|
+
const exists = issues.some((current) => current.code === issue.code && current.targetId === issue.targetId && current.message === issue.message);
|
|
531
|
+
if (!exists) {
|
|
532
|
+
issues.push(issue);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function unquote(value) {
|
|
536
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
537
|
+
return value.slice(1, -1);
|
|
538
|
+
}
|
|
539
|
+
return value;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function bakeSymbol(content, name, options) {
|
|
543
|
+
const document = parseSvg(content);
|
|
544
|
+
const issues = [];
|
|
545
|
+
if (options.rewrite) {
|
|
546
|
+
rewriteIds(document, name, options, issues);
|
|
547
|
+
}
|
|
548
|
+
rewriteRoot(document, name);
|
|
549
|
+
return {
|
|
550
|
+
name,
|
|
551
|
+
content: buildSvg(document).trim(),
|
|
552
|
+
issues
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const PRESET_OVERRIDE_BLOCKED_PLUGINS = [
|
|
557
|
+
"cleanupIds",
|
|
558
|
+
"removeUselessDefs",
|
|
559
|
+
"removeHiddenElems",
|
|
560
|
+
"removeUnknownsAndDefaults",
|
|
561
|
+
"collapseGroups",
|
|
562
|
+
"mergePaths",
|
|
563
|
+
"convertShapeToPath",
|
|
564
|
+
"removeEmptyContainers"
|
|
565
|
+
];
|
|
566
|
+
const FILTER_ONLY_BLOCKED_PLUGINS = ["prefixIds", "reusePaths", "convertOneStopGradients"];
|
|
567
|
+
const USER_FILTER_BLOCKED_PLUGINS = ["preset-default", ...PRESET_OVERRIDE_BLOCKED_PLUGINS, ...FILTER_ONLY_BLOCKED_PLUGINS];
|
|
568
|
+
const DEFAULT_SAFE_PRESET = {
|
|
569
|
+
name: "preset-default",
|
|
570
|
+
params: {
|
|
571
|
+
overrides: Object.fromEntries(PRESET_OVERRIDE_BLOCKED_PLUGINS.map((name) => [name, false]))
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
const DEFAULT_SAFE_PLUGINS = [{ name: "removeTitle" }, { name: "removeXMLNS" }, { name: "removeXlink" }];
|
|
575
|
+
function resolveOptions(userOption) {
|
|
576
|
+
const userObject = userOption ?? {};
|
|
577
|
+
return {
|
|
578
|
+
optimize: userObject.optimize ?? true,
|
|
579
|
+
svgoOptions: userObject.svgoOptions ?? {},
|
|
580
|
+
idPolicy: resolveIdPolicyOptions(userObject.idPolicy)
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function createSvgoConfig(options) {
|
|
584
|
+
const plugins = [];
|
|
585
|
+
if (options.optimize) {
|
|
586
|
+
plugins.push(DEFAULT_SAFE_PRESET);
|
|
587
|
+
plugins.push(...DEFAULT_SAFE_PLUGINS);
|
|
588
|
+
}
|
|
589
|
+
if (options.svgoOptions.plugins != null) {
|
|
590
|
+
plugins.push(...filterPlugins(options.svgoOptions.plugins));
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
multipass: options.svgoOptions.multipass,
|
|
594
|
+
floatPrecision: options.svgoOptions.floatPrecision,
|
|
595
|
+
js2svg: options.svgoOptions.js2svg,
|
|
596
|
+
plugins
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function filterPlugins(plugins) {
|
|
600
|
+
return plugins.filter((plugin) => {
|
|
601
|
+
const name = resolvePluginName(plugin);
|
|
602
|
+
if (name == null) {
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
return !USER_FILTER_BLOCKED_PLUGINS.includes(name);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function resolvePluginName(plugin) {
|
|
609
|
+
if (typeof plugin === "string") {
|
|
610
|
+
return plugin;
|
|
611
|
+
}
|
|
612
|
+
if (plugin && typeof plugin === "object" && "name" in plugin && typeof plugin.name === "string") {
|
|
613
|
+
return plugin.name;
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
function resolveIdPolicyOptions(idPolicy) {
|
|
618
|
+
return {
|
|
619
|
+
rewrite: idPolicy?.rewrite ?? true,
|
|
620
|
+
unresolved: idPolicy?.unresolved ?? "prefix",
|
|
621
|
+
idStyle: idPolicy?.idStyle ?? "named",
|
|
622
|
+
delim: idPolicy?.delim ?? "_"
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
class BakeError extends Error {
|
|
627
|
+
code;
|
|
628
|
+
cause;
|
|
629
|
+
constructor(code, message, options) {
|
|
630
|
+
super(message, options);
|
|
631
|
+
this.name = "BakeError";
|
|
632
|
+
this.code = code;
|
|
633
|
+
this.cause = options?.cause;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function bakeIcon(source, options) {
|
|
638
|
+
return createBaker(options).bakeIcon(source);
|
|
639
|
+
}
|
|
640
|
+
function bakeIcons(sources, options) {
|
|
641
|
+
return createBaker(options).bakeIcons(sources);
|
|
642
|
+
}
|
|
643
|
+
function createBaker(options) {
|
|
644
|
+
const resolvedOptions = resolveOptions(options);
|
|
645
|
+
const svgoConfig = createSvgoConfig(resolvedOptions);
|
|
646
|
+
return {
|
|
647
|
+
bakeIcon(source) {
|
|
648
|
+
return convertSource(source, resolvedOptions, svgoConfig);
|
|
649
|
+
},
|
|
650
|
+
bakeIcons(sources) {
|
|
651
|
+
return sources.map((source) => {
|
|
652
|
+
return convertSource(source, resolvedOptions, svgoConfig);
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function convertSource(source, options, svgoConfig) {
|
|
658
|
+
if (!source || !source.name || !source.content) {
|
|
659
|
+
throw new BakeError("ValidateSourceInvalid", "Property name and content are required.");
|
|
660
|
+
}
|
|
661
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(source.name)) {
|
|
662
|
+
throw new BakeError("ValidateNameInvalid", "Invalid name. Use letters, numbers, dash, or underscore, starting with a letter.");
|
|
663
|
+
}
|
|
664
|
+
const normalizedSource = trimSvgPreamble(source.content);
|
|
665
|
+
if (!/^\s*<svg\b/i.test(normalizedSource)) {
|
|
666
|
+
throw new BakeError("ValidateSvgRootInvalid", "Input must start with an <svg> root element.");
|
|
667
|
+
}
|
|
668
|
+
const optimized = optimizeSvg(normalizedSource, svgoConfig);
|
|
669
|
+
try {
|
|
670
|
+
return bakeSymbol(optimized, source.name, options.idPolicy);
|
|
671
|
+
} catch (cause) {
|
|
672
|
+
if (cause instanceof BakeError) {
|
|
673
|
+
throw cause;
|
|
674
|
+
}
|
|
675
|
+
if (cause instanceof Error && cause.message === "Cannot determine viewBox. Provide an SVG with viewBox or width/height attributes.") {
|
|
676
|
+
throw new BakeError("ResolveViewBoxFailed", cause.message);
|
|
677
|
+
}
|
|
678
|
+
if (cause instanceof Error && cause.message === "Input must start with an <svg> root element.") {
|
|
679
|
+
throw new BakeError("ValidateSvgRootInvalid", cause.message);
|
|
680
|
+
}
|
|
681
|
+
throw new BakeError("ParseSvgFailed", "SVG parsing failed during id rewrite.", { cause });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
function optimizeSvg(content, svgoConfig) {
|
|
685
|
+
let result;
|
|
686
|
+
try {
|
|
687
|
+
result = optimize(content, svgoConfig);
|
|
688
|
+
} catch (cause) {
|
|
689
|
+
throw new BakeError("OptimizeSvgFailed", "SVGO optimization failed.", { cause });
|
|
690
|
+
}
|
|
691
|
+
return result.data;
|
|
692
|
+
}
|
|
693
|
+
function trimSvgPreamble(content) {
|
|
694
|
+
return content.replace(/^(?:\uFEFF|\s|<\?xml[\s\S]*?\?>|<!--[\s\S]*?-->|<!DOCTYPE[\s\S]*?>)+/i, "");
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export { BakeError, bakeIcon, bakeIcons, createBaker };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svg-icon-baker",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A delightful toolkit for baking raw SVG icons into delicious SVG symbol sprites",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svg",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/yangxu52/svg-
|
|
20
|
+
"url": "git+https://github.com/yangxu52/vite-plugin-svg-icons-ng.git",
|
|
21
|
+
"directory": "packages/svg-icon-baker"
|
|
21
22
|
},
|
|
22
23
|
"license": "MIT",
|
|
23
24
|
"author": "yangxu52",
|
|
@@ -36,21 +37,26 @@
|
|
|
36
37
|
"dist"
|
|
37
38
|
],
|
|
38
39
|
"dependencies": {
|
|
40
|
+
"css-tree": "^3.2.1",
|
|
41
|
+
"fast-xml-builder": "^1.1.5",
|
|
42
|
+
"fast-xml-parser": "^5.7.2",
|
|
39
43
|
"svgo": "^4.0.1"
|
|
40
44
|
},
|
|
41
45
|
"devDependencies": {
|
|
42
46
|
"@vitest/coverage-v8": "^4.1.0",
|
|
47
|
+
"rimraf": "^6.1.3",
|
|
43
48
|
"unbuild": "^3.6.1",
|
|
44
49
|
"vitest": "^4.1.0"
|
|
45
50
|
},
|
|
46
51
|
"engines": {
|
|
47
|
-
"node": "^
|
|
48
|
-
"pnpm": ">=10.0.0"
|
|
52
|
+
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
|
49
53
|
},
|
|
50
54
|
"scripts": {
|
|
51
|
-
"build": "unbuild",
|
|
52
55
|
"dev": "unbuild --stub",
|
|
53
|
-
"
|
|
54
|
-
"test
|
|
56
|
+
"build": "unbuild",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"coverage": "vitest run --coverage",
|
|
59
|
+
"clean": "rimraf dist",
|
|
60
|
+
"release": "commit-and-tag-version --path . -t svg-icon-baker@"
|
|
55
61
|
}
|
|
56
62
|
}
|