svg-icon-baker 1.2.1 → 2.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/README.md CHANGED
@@ -2,49 +2,196 @@
2
2
 
3
3
  > Bake the `svg` icon into `symbol` 🍪
4
4
 
5
- The core library for transforming SVG icons into optimized SVG symbol sprites.
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
- If you like this project, please give it a [Star](https://github.com/yangxu52/svg-icon-baker).
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
- const results = bakeIcons([source])
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 `bakeIcons` are synchronous.
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 inferred option set.
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 | default | description |
36
- | ------------- | ------------- | ------- | ------------------------------------------------ |
37
- | `optimize` | `boolean` | `true` | Enable default safe SVGO optimization preset. |
38
- | `svgoOptions` | `SvgoOptions` | `{}` | Custom SVGO options into the optimization layer. |
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: 'minified', 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'` | `'minified'` | Choose readable ids such as `icon_home`, short ids such as `icon_a`, or hashed ids such as `icon_dgxZM` (base62 characters). |
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
- Notes:
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
- - Behavior matrix:
43
- - `optimize: true` + no `svgoOptions`: run default safe optimization.
44
- - `optimize: true` + `svgoOptions`: run default safe optimization, then merge custom options.
45
- - `optimize: false` + no `svgoOptions`: skip optimization layer.
46
- - `optimize: false` + `svgoOptions`: run custom optimization settings only.
47
- - When `svgoOptions.plugins` is provided, custom plugins run after default safe optimization plugins.
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: 'minified', 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: 'minified', 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,28 +1,599 @@
1
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';
2
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
+ const BASE52_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
459
+ function encodeIndex(index) {
460
+ let current = index;
461
+ let result = "";
462
+ do {
463
+ result = BASE52_ALPHABET[current % 52] + result;
464
+ current = Math.floor(current / 52) - 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
+ return toBase62(digest_djb2(value));
481
+ }
482
+ function digest_djb2(str) {
483
+ let hash = 5381;
484
+ for (const char of str) {
485
+ hash = (hash << 5) + hash ^ char.charCodeAt(0);
486
+ }
487
+ return hash >>> 0;
488
+ }
489
+ const BASE62_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
490
+ function toBase62(value) {
491
+ let current = value;
492
+ let result = "";
493
+ do {
494
+ result = BASE62_ALPHABET[current % 62] + result;
495
+ current = Math.floor(current / 62);
496
+ } while (current > 0);
497
+ return result;
498
+ }
499
+ function createUnresolvedMappedId(sourceId, prefix, unresolvedIdMap, nextIndex, options) {
500
+ const existing = unresolvedIdMap.get(sourceId);
501
+ if (existing) {
502
+ return existing;
503
+ }
504
+ const generated = createMappedId(sourceId, prefix, nextIndex(), options);
505
+ unresolvedIdMap.set(sourceId, generated);
506
+ return generated;
507
+ }
508
+ function rewriteUnresolvedReference(targetId, originalValue, prefix, unresolvedIdMap, nextIndex, options, issues, format) {
509
+ pushIssue(issues, {
510
+ code: "ResolveReferenceFailed",
511
+ message: `Resolve reference failed for local target "${targetId}"; reference was ${options.unresolved === "prefix" ? "prefixed" : "preserved"}.`,
512
+ targetId
513
+ });
514
+ if (options.unresolved === "preserve") {
515
+ return originalValue;
516
+ }
517
+ return format(createUnresolvedMappedId(targetId, prefix, unresolvedIdMap, nextIndex, options));
518
+ }
519
+ function resolveDelim(options) {
520
+ if (options.delim != null) {
521
+ return options.delim;
522
+ }
523
+ return "_";
524
+ }
525
+ function collectIssues(state, issues) {
526
+ for (const [id, definitions] of state.definedIds) {
527
+ if (definitions.length > 1) {
528
+ pushIssue(issues, {
529
+ code: "DetectDefinitionDuplicate",
530
+ message: `Detect definition duplicate for local target "${id}"; first occurrence was kept.`,
531
+ targetId: id
532
+ });
533
+ }
534
+ }
535
+ if (state.styleParseFailureCount > 0) {
536
+ pushIssue(issues, {
537
+ code: "ParseStyleFailed",
538
+ message: "Parse style failed for one or more <style> blocks; original content was preserved."
539
+ });
540
+ }
541
+ }
542
+ function pushIssue(issues, issue) {
543
+ const exists = issues.some((current) => current.code === issue.code && current.targetId === issue.targetId && current.message === issue.message);
544
+ if (!exists) {
545
+ issues.push(issue);
546
+ }
547
+ }
548
+ function unquote(value) {
549
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
550
+ return value.slice(1, -1);
551
+ }
552
+ return value;
553
+ }
554
+
555
+ function bakeSymbol(content, name, options) {
556
+ const document = parseSvg(content);
557
+ const issues = [];
558
+ if (options.rewrite) {
559
+ rewriteIds(document, name, options, issues);
560
+ }
561
+ rewriteRoot(document, name);
562
+ return {
563
+ name,
564
+ content: buildSvg(document).trim(),
565
+ issues
566
+ };
567
+ }
568
+
569
+ const PRESET_OVERRIDE_BLOCKED_PLUGINS = [
570
+ "cleanupIds",
571
+ "removeUselessDefs",
572
+ "removeHiddenElems",
573
+ "removeUnknownsAndDefaults",
574
+ "collapseGroups",
575
+ "mergePaths",
576
+ "convertShapeToPath",
577
+ "removeEmptyContainers"
578
+ ];
579
+ const FILTER_ONLY_BLOCKED_PLUGINS = ["prefixIds", "reusePaths", "convertOneStopGradients"];
580
+ const USER_FILTER_BLOCKED_PLUGINS = ["preset-default", ...PRESET_OVERRIDE_BLOCKED_PLUGINS, ...FILTER_ONLY_BLOCKED_PLUGINS];
3
581
  const DEFAULT_SAFE_PRESET = {
4
582
  name: "preset-default",
5
583
  params: {
6
- overrides: {
7
- removeUselessDefs: false,
8
- removeHiddenElems: false,
9
- removeUnknownsAndDefaults: false,
10
- collapseGroups: false,
11
- mergePaths: false,
12
- convertShapeToPath: false
13
- }
584
+ overrides: Object.fromEntries(PRESET_OVERRIDE_BLOCKED_PLUGINS.map((name) => [name, false]))
14
585
  }
15
586
  };
16
587
  const DEFAULT_SAFE_PLUGINS = [{ name: "removeTitle" }, { name: "removeXMLNS" }, { name: "removeXlink" }];
17
- const CORE_PLUGIN_BLOCKLIST = /* @__PURE__ */ new Set(["prefixIds"]);
18
588
  function resolveOptions(userOption) {
19
589
  const userObject = userOption ?? {};
20
590
  return {
21
591
  optimize: userObject.optimize ?? true,
22
- svgoOptions: userObject.svgoOptions ?? {}
592
+ svgoOptions: userObject.svgoOptions ?? {},
593
+ idPolicy: resolveIdPolicyOptions(userObject.idPolicy)
23
594
  };
24
595
  }
25
- function createSvgoConfig(sourceName, options) {
596
+ function createSvgoConfig(options) {
26
597
  const plugins = [];
27
598
  if (options.optimize) {
28
599
  plugins.push(DEFAULT_SAFE_PRESET);
@@ -31,7 +602,6 @@ function createSvgoConfig(sourceName, options) {
31
602
  if (options.svgoOptions.plugins != null) {
32
603
  plugins.push(...filterPlugins(options.svgoOptions.plugins));
33
604
  }
34
- plugins.push(...createCorePlugins(sourceName));
35
605
  return {
36
606
  multipass: options.svgoOptions.multipass,
37
607
  floatPrecision: options.svgoOptions.floatPrecision,
@@ -45,7 +615,7 @@ function filterPlugins(plugins) {
45
615
  if (name == null) {
46
616
  return true;
47
617
  }
48
- return !CORE_PLUGIN_BLOCKLIST.has(name);
618
+ return !USER_FILTER_BLOCKED_PLUGINS.includes(name);
49
619
  });
50
620
  }
51
621
  function resolvePluginName(plugin) {
@@ -57,57 +627,84 @@ function resolvePluginName(plugin) {
57
627
  }
58
628
  return null;
59
629
  }
60
- function createCorePlugins(sourceName) {
61
- return [{ name: "removeDimensions" }, { name: "prefixIds", params: { prefix: `${sourceName}-`, delim: "" } }];
630
+ function resolveIdPolicyOptions(idPolicy) {
631
+ return {
632
+ rewrite: idPolicy?.rewrite ?? true,
633
+ unresolved: idPolicy?.unresolved ?? "prefix",
634
+ idStyle: idPolicy?.idStyle ?? "minified",
635
+ delim: idPolicy?.delim ?? "_"
636
+ };
637
+ }
638
+
639
+ class BakeError extends Error {
640
+ code;
641
+ cause;
642
+ constructor(code, message, options) {
643
+ super(message, options);
644
+ this.name = "BakeError";
645
+ this.code = code;
646
+ this.cause = options?.cause;
647
+ }
62
648
  }
63
649
 
64
650
  function bakeIcon(source, options) {
65
- const resolvedOptions = resolveOptions(options);
66
- return {
67
- name: source.name,
68
- content: convertToSymbol(source, resolvedOptions)
69
- };
651
+ return createBaker(options).bakeIcon(source);
70
652
  }
71
653
  function bakeIcons(sources, options) {
654
+ return createBaker(options).bakeIcons(sources);
655
+ }
656
+ function createBaker(options) {
72
657
  const resolvedOptions = resolveOptions(options);
73
- return sources.map((source) => ({
74
- name: source.name,
75
- content: convertToSymbol(source, resolvedOptions)
76
- }));
658
+ const svgoConfig = createSvgoConfig(resolvedOptions);
659
+ return {
660
+ bakeIcon(source) {
661
+ return convertSource(source, resolvedOptions, svgoConfig);
662
+ },
663
+ bakeIcons(sources) {
664
+ return sources.map((source) => {
665
+ return convertSource(source, resolvedOptions, svgoConfig);
666
+ });
667
+ }
668
+ };
77
669
  }
78
- function convertToSymbol(source, options) {
670
+ function convertSource(source, options, svgoConfig) {
79
671
  if (!source || !source.name || !source.content) {
80
- throw new TypeError("Property name and content are required.");
672
+ throw new BakeError("ValidateSourceInvalid", "Property name and content are required.");
81
673
  }
82
674
  if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(source.name)) {
83
- throw new TypeError("Invalid name. Use letters, numbers, dash, or underscore, starting with a letter.");
675
+ throw new BakeError("ValidateNameInvalid", "Invalid name. Use letters, numbers, dash, or underscore, starting with a letter.");
84
676
  }
85
- const normalizedSource = stripLeadingSvgPreamble(source.content);
677
+ const normalizedSource = trimSvgPreamble(source.content);
86
678
  if (!/^\s*<svg\b/i.test(normalizedSource)) {
87
- throw new Error("Parsing failed. Input must start with an <svg> root element.");
679
+ throw new BakeError("ValidateSvgRootInvalid", "Input must start with an <svg> root element.");
88
680
  }
89
- let result;
681
+ const optimized = optimizeSvg(normalizedSource, svgoConfig);
90
682
  try {
91
- result = optimize(normalizedSource, createSvgoConfig(source.name, options));
92
- } catch (err) {
93
- throw new Error(`Parsing failed. ${String(err)}`);
683
+ return bakeSymbol(optimized, source.name, options.idPolicy);
684
+ } catch (cause) {
685
+ if (cause instanceof BakeError) {
686
+ throw cause;
687
+ }
688
+ if (cause instanceof Error && cause.message === "Cannot determine viewBox. Provide an SVG with viewBox or width/height attributes.") {
689
+ throw new BakeError("ResolveViewBoxFailed", cause.message);
690
+ }
691
+ if (cause instanceof Error && cause.message === "Input must start with an <svg> root element.") {
692
+ throw new BakeError("ValidateSvgRootInvalid", cause.message);
693
+ }
694
+ throw new BakeError("ParseSvgFailed", "SVG parsing failed during id rewrite.", { cause });
94
695
  }
95
- const viewBox = result.data.match(/viewBox="([^"]+)"/)?.[1];
96
- if (!viewBox) {
97
- throw new Error("Cannot determine viewBox. Provide an SVG with viewBox or width/height attributes.");
696
+ }
697
+ function optimizeSvg(content, svgoConfig) {
698
+ let result;
699
+ try {
700
+ result = optimize(content, svgoConfig);
701
+ } catch (cause) {
702
+ throw new BakeError("OptimizeSvgFailed", "SVGO optimization failed.", { cause });
98
703
  }
99
- const cleanedSvg = stripLeadingSvgPreamble(result.data);
100
- return toSymbolRootTag(cleanedSvg, source.name, viewBox);
704
+ return result.data;
101
705
  }
102
- function stripLeadingSvgPreamble(content) {
706
+ function trimSvgPreamble(content) {
103
707
  return content.replace(/^(?:\uFEFF|\s|<\?xml[\s\S]*?\?>|<!--[\s\S]*?-->|<!DOCTYPE[\s\S]*?>)+/i, "");
104
708
  }
105
- function toSymbolRootTag(svg, symbolId, viewBox) {
106
- const rootOpenTag = svg.match(/^\s*<svg\b[^>]*>/i)[0];
107
- const preservedAttrs = rootOpenTag.replace(/^\s*<svg\b/i, "").replace(/>\s*$/i, "").replace(/\s+id=(['"])[^'"]*\1/gi, "").replace(/\s+viewBox=(['"])[^'"]*\1/gi, "").replace(/\s+width=(['"])[^'"]*\1/gi, "").replace(/\s+height=(['"])[^'"]*\1/gi, "").trim();
108
- const attrs = preservedAttrs ? ` ${preservedAttrs}` : "";
109
- const symbolOpenTag = `<symbol id="${symbolId}" viewBox="${viewBox}"${attrs}>`;
110
- return svg.replace(/^\s*<svg\b[^>]*>/i, symbolOpenTag).replace(/<\/svg>\s*$/i, "</symbol>").trim();
111
- }
112
709
 
113
- export { bakeIcon, bakeIcons };
710
+ export { BakeError, bakeIcon, bakeIcons, createBaker };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svg-icon-baker",
3
- "version": "1.2.1",
3
+ "version": "2.0.1",
4
4
  "description": "A delightful toolkit for baking raw SVG icons into delicious SVG symbol sprites",
5
5
  "keywords": [
6
6
  "svg",
@@ -37,6 +37,9 @@
37
37
  "dist"
38
38
  ],
39
39
  "dependencies": {
40
+ "css-tree": "^3.2.1",
41
+ "fast-xml-builder": "^1.1.5",
42
+ "fast-xml-parser": "^5.7.2",
40
43
  "svgo": "^4.0.1"
41
44
  },
42
45
  "devDependencies": {
@@ -54,6 +57,6 @@
54
57
  "test": "vitest run",
55
58
  "coverage": "vitest run --coverage",
56
59
  "clean": "rimraf dist",
57
- "log": "conventional-changelog -p angular --commit-path . -t svg-icon-baker@v"
60
+ "release": "commit-and-tag-version --path . -t svg-icon-baker@"
58
61
  }
59
62
  }