@emasoft/svg-matrix 1.0.30 → 1.0.31
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/bin/svg-matrix.js +310 -61
- package/bin/svglinter.cjs +102 -3
- package/bin/svgm.js +236 -27
- package/package.json +1 -1
- package/src/animation-optimization.js +137 -17
- package/src/animation-references.js +123 -6
- package/src/arc-length.js +213 -4
- package/src/bezier-analysis.js +217 -21
- package/src/bezier-intersections.js +275 -12
- package/src/browser-verify.js +237 -4
- package/src/clip-path-resolver.js +168 -0
- package/src/convert-path-data.js +479 -28
- package/src/css-specificity.js +73 -10
- package/src/douglas-peucker.js +219 -2
- package/src/flatten-pipeline.js +284 -26
- package/src/geometry-to-path.js +250 -25
- package/src/gjk-collision.js +236 -33
- package/src/index.js +261 -3
- package/src/inkscape-support.js +86 -28
- package/src/logger.js +48 -3
- package/src/marker-resolver.js +278 -74
- package/src/mask-resolver.js +265 -66
- package/src/matrix.js +44 -5
- package/src/mesh-gradient.js +352 -102
- package/src/off-canvas-detection.js +382 -13
- package/src/path-analysis.js +192 -18
- package/src/path-data-plugins.js +309 -5
- package/src/path-optimization.js +129 -5
- package/src/path-simplification.js +188 -32
- package/src/pattern-resolver.js +454 -106
- package/src/polygon-clip.js +324 -1
- package/src/svg-boolean-ops.js +226 -9
- package/src/svg-collections.js +7 -5
- package/src/svg-flatten.js +386 -62
- package/src/svg-parser.js +179 -8
- package/src/svg-rendering-context.js +235 -6
- package/src/svg-toolbox.js +45 -8
- package/src/svg2-polyfills.js +40 -10
- package/src/transform-decomposition.js +258 -32
- package/src/transform-optimization.js +259 -13
- package/src/transforms2d.js +82 -9
- package/src/transforms3d.js +62 -10
- package/src/use-symbol-resolver.js +286 -42
- package/src/vector.js +64 -8
- package/src/verification.js +392 -1
|
@@ -44,6 +44,22 @@ export const STANDARD_EASINGS = {
|
|
|
44
44
|
* @returns {string} Optimized number string
|
|
45
45
|
*/
|
|
46
46
|
export function formatSplineValue(value, precision = 3) {
|
|
47
|
+
// Validate value parameter
|
|
48
|
+
if (value === null || value === undefined) {
|
|
49
|
+
throw new Error('formatSplineValue: value parameter is required');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Validate precision parameter
|
|
53
|
+
if (typeof precision !== 'number' || precision < 0 || !Number.isFinite(precision)) {
|
|
54
|
+
throw new Error('formatSplineValue: precision must be a non-negative finite number');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check for NaN/Infinity before creating Decimal
|
|
58
|
+
const numValue = typeof value === 'number' ? value : parseFloat(value);
|
|
59
|
+
if (!Number.isFinite(numValue)) {
|
|
60
|
+
throw new Error(`formatSplineValue: value must be a finite number, got ${value}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
47
63
|
const num = new Decimal(value);
|
|
48
64
|
|
|
49
65
|
// Round to precision
|
|
@@ -82,11 +98,20 @@ export function parseKeySplines(keySplines) {
|
|
|
82
98
|
// Split by semicolon to get individual splines
|
|
83
99
|
const splines = keySplines.split(';').map(s => s.trim()).filter(s => s);
|
|
84
100
|
|
|
85
|
-
return splines
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
return splines
|
|
102
|
+
.map(spline => {
|
|
103
|
+
// Split by whitespace or comma to get control points
|
|
104
|
+
const values = spline.split(/[\s,]+/)
|
|
105
|
+
.map(v => parseFloat(v))
|
|
106
|
+
.filter(v => Number.isFinite(v)); // Filter out NaN and Infinity
|
|
107
|
+
|
|
108
|
+
// Each spline must have exactly 4 control points
|
|
109
|
+
if (values.length !== 4) {
|
|
110
|
+
throw new Error(`parseKeySplines: invalid spline "${spline}", expected 4 values, got ${values.length}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return values;
|
|
114
|
+
});
|
|
90
115
|
}
|
|
91
116
|
|
|
92
117
|
/**
|
|
@@ -96,7 +121,25 @@ export function parseKeySplines(keySplines) {
|
|
|
96
121
|
* @returns {string} keySplines attribute value
|
|
97
122
|
*/
|
|
98
123
|
export function serializeKeySplines(splines, precision = 3) {
|
|
99
|
-
|
|
124
|
+
// Validate splines parameter
|
|
125
|
+
if (!Array.isArray(splines)) {
|
|
126
|
+
throw new Error('serializeKeySplines: splines must be an array');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Validate precision parameter
|
|
130
|
+
if (typeof precision !== 'number' || precision < 0 || !Number.isFinite(precision)) {
|
|
131
|
+
throw new Error('serializeKeySplines: precision must be a non-negative finite number');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return splines.map((spline, index) => {
|
|
135
|
+
// Validate each spline is an array with 4 values
|
|
136
|
+
if (!Array.isArray(spline)) {
|
|
137
|
+
throw new Error(`serializeKeySplines: spline at index ${index} must be an array`);
|
|
138
|
+
}
|
|
139
|
+
if (spline.length !== 4) {
|
|
140
|
+
throw new Error(`serializeKeySplines: spline at index ${index} must have exactly 4 values, got ${spline.length}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
100
143
|
return spline.map(v => formatSplineValue(v, precision)).join(' ');
|
|
101
144
|
}).join('; ');
|
|
102
145
|
}
|
|
@@ -108,9 +151,21 @@ export function serializeKeySplines(splines, precision = 3) {
|
|
|
108
151
|
* @returns {boolean} True if spline is linear
|
|
109
152
|
*/
|
|
110
153
|
export function isLinearSpline(spline, tolerance = 0.001) {
|
|
111
|
-
|
|
154
|
+
// Validate spline parameter
|
|
155
|
+
if (!Array.isArray(spline) || spline.length !== 4) return false;
|
|
156
|
+
|
|
157
|
+
// Validate tolerance parameter
|
|
158
|
+
if (typeof tolerance !== 'number' || tolerance < 0 || !Number.isFinite(tolerance)) {
|
|
159
|
+
throw new Error('isLinearSpline: tolerance must be a non-negative finite number');
|
|
160
|
+
}
|
|
112
161
|
|
|
113
162
|
const [x1, y1, x2, y2] = spline;
|
|
163
|
+
|
|
164
|
+
// Check for NaN values in spline
|
|
165
|
+
if (!Number.isFinite(x1) || !Number.isFinite(y1) || !Number.isFinite(x2) || !Number.isFinite(y2)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
114
169
|
return (
|
|
115
170
|
Math.abs(x1) < tolerance &&
|
|
116
171
|
Math.abs(y1) < tolerance &&
|
|
@@ -138,7 +193,16 @@ export function areAllSplinesLinear(keySplines) {
|
|
|
138
193
|
* @returns {string|null} Easing name or null if not standard
|
|
139
194
|
*/
|
|
140
195
|
export function identifyStandardEasing(spline, tolerance = 0.01) {
|
|
141
|
-
|
|
196
|
+
// Validate spline parameter
|
|
197
|
+
if (!Array.isArray(spline) || spline.length !== 4) return null;
|
|
198
|
+
|
|
199
|
+
// Validate tolerance parameter
|
|
200
|
+
if (typeof tolerance !== 'number' || tolerance < 0 || !Number.isFinite(tolerance)) {
|
|
201
|
+
throw new Error('identifyStandardEasing: tolerance must be a non-negative finite number');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check for NaN values in spline
|
|
205
|
+
if (!spline.every(val => Number.isFinite(val))) return null;
|
|
142
206
|
|
|
143
207
|
for (const [name, standard] of Object.entries(STANDARD_EASINGS)) {
|
|
144
208
|
const matches = spline.every((val, i) => Math.abs(val - standard[i]) < tolerance);
|
|
@@ -189,7 +253,7 @@ export function optimizeKeySplines(keySplines, options = {}) {
|
|
|
189
253
|
*/
|
|
190
254
|
export function parseKeyTimes(keyTimes) {
|
|
191
255
|
if (!keyTimes || typeof keyTimes !== 'string') return [];
|
|
192
|
-
return keyTimes.split(';').map(s => parseFloat(s.trim())).filter(v =>
|
|
256
|
+
return keyTimes.split(';').map(s => parseFloat(s.trim())).filter(v => Number.isFinite(v));
|
|
193
257
|
}
|
|
194
258
|
|
|
195
259
|
/**
|
|
@@ -199,6 +263,19 @@ export function parseKeyTimes(keyTimes) {
|
|
|
199
263
|
* @returns {string} keyTimes attribute value
|
|
200
264
|
*/
|
|
201
265
|
export function serializeKeyTimes(times, precision = 3) {
|
|
266
|
+
// Validate times parameter
|
|
267
|
+
if (!Array.isArray(times)) {
|
|
268
|
+
throw new Error('serializeKeyTimes: times must be an array');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Validate precision parameter
|
|
272
|
+
if (typeof precision !== 'number' || precision < 0 || !Number.isFinite(precision)) {
|
|
273
|
+
throw new Error('serializeKeyTimes: precision must be a non-negative finite number');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Return empty string for empty array
|
|
277
|
+
if (times.length === 0) return '';
|
|
278
|
+
|
|
202
279
|
return times.map(t => formatSplineValue(t, precision)).join('; ');
|
|
203
280
|
}
|
|
204
281
|
|
|
@@ -209,6 +286,16 @@ export function serializeKeyTimes(times, precision = 3) {
|
|
|
209
286
|
* @returns {string} Optimized keyTimes value
|
|
210
287
|
*/
|
|
211
288
|
export function optimizeKeyTimes(keyTimes, precision = 3) {
|
|
289
|
+
// Validate keyTimes parameter
|
|
290
|
+
if (keyTimes === null || keyTimes === undefined) {
|
|
291
|
+
throw new Error('optimizeKeyTimes: keyTimes parameter is required');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Validate precision parameter
|
|
295
|
+
if (typeof precision !== 'number' || precision < 0 || !Number.isFinite(precision)) {
|
|
296
|
+
throw new Error('optimizeKeyTimes: precision must be a non-negative finite number');
|
|
297
|
+
}
|
|
298
|
+
|
|
212
299
|
const times = parseKeyTimes(keyTimes);
|
|
213
300
|
if (times.length === 0) return keyTimes;
|
|
214
301
|
return serializeKeyTimes(times, precision);
|
|
@@ -224,9 +311,17 @@ export function optimizeKeyTimes(keyTimes, precision = 3) {
|
|
|
224
311
|
export function optimizeAnimationValues(values, precision = 3) {
|
|
225
312
|
if (!values || typeof values !== 'string') return values;
|
|
226
313
|
|
|
314
|
+
// Validate precision parameter
|
|
315
|
+
if (typeof precision !== 'number' || precision < 0 || !Number.isFinite(precision)) {
|
|
316
|
+
throw new Error('optimizeAnimationValues: precision must be a non-negative finite number');
|
|
317
|
+
}
|
|
318
|
+
|
|
227
319
|
// Split by semicolon
|
|
228
320
|
const parts = values.split(';');
|
|
229
321
|
|
|
322
|
+
// Handle empty values string
|
|
323
|
+
if (parts.length === 0) return values;
|
|
324
|
+
|
|
230
325
|
const optimized = parts.map(part => {
|
|
231
326
|
const trimmed = part.trim();
|
|
232
327
|
|
|
@@ -236,10 +331,10 @@ export function optimizeAnimationValues(values, precision = 3) {
|
|
|
236
331
|
}
|
|
237
332
|
|
|
238
333
|
// Try to parse as numbers (could be space-separated like "0 0" for translate)
|
|
239
|
-
const nums = trimmed.split(/[\s,]+/);
|
|
334
|
+
const nums = trimmed.split(/[\s,]+/).filter(n => n); // Filter empty strings
|
|
240
335
|
const optimizedNums = nums.map(n => {
|
|
241
336
|
const num = parseFloat(n);
|
|
242
|
-
if (
|
|
337
|
+
if (!Number.isFinite(num)) return n; // Not a finite number, preserve as-is
|
|
243
338
|
return formatSplineValue(num, precision);
|
|
244
339
|
});
|
|
245
340
|
|
|
@@ -256,10 +351,23 @@ export function optimizeAnimationValues(values, precision = 3) {
|
|
|
256
351
|
* @returns {{modified: boolean, changes: string[]}}
|
|
257
352
|
*/
|
|
258
353
|
export function optimizeElementTiming(el, options = {}) {
|
|
354
|
+
// Validate el parameter
|
|
355
|
+
if (!el || typeof el !== 'object') {
|
|
356
|
+
throw new Error('optimizeElementTiming: el parameter must be a valid element');
|
|
357
|
+
}
|
|
358
|
+
if (typeof el.getAttribute !== 'function' || typeof el.setAttribute !== 'function' || typeof el.removeAttribute !== 'function') {
|
|
359
|
+
throw new Error('optimizeElementTiming: el parameter must have getAttribute, setAttribute, and removeAttribute methods');
|
|
360
|
+
}
|
|
361
|
+
|
|
259
362
|
const precision = options.precision ?? 3;
|
|
260
363
|
const removeLinearSplines = options.removeLinearSplines !== false;
|
|
261
364
|
const optimizeValues = options.optimizeValues !== false;
|
|
262
365
|
|
|
366
|
+
// Validate precision from options
|
|
367
|
+
if (typeof precision !== 'number' || precision < 0 || !Number.isFinite(precision)) {
|
|
368
|
+
throw new Error('optimizeElementTiming: options.precision must be a non-negative finite number');
|
|
369
|
+
}
|
|
370
|
+
|
|
263
371
|
const changes = [];
|
|
264
372
|
let modified = false;
|
|
265
373
|
|
|
@@ -295,11 +403,10 @@ export function optimizeElementTiming(el, options = {}) {
|
|
|
295
403
|
}
|
|
296
404
|
}
|
|
297
405
|
|
|
298
|
-
// Optimize values (
|
|
406
|
+
// Optimize values (optimizeAnimationValues internally preserves ID refs)
|
|
299
407
|
if (optimizeValues) {
|
|
300
408
|
const values = el.getAttribute('values');
|
|
301
|
-
if (values
|
|
302
|
-
// Only optimize if no ID references
|
|
409
|
+
if (values) {
|
|
303
410
|
const optimized = optimizeAnimationValues(values, precision);
|
|
304
411
|
if (optimized !== values) {
|
|
305
412
|
el.setAttribute('values', optimized);
|
|
@@ -309,10 +416,10 @@ export function optimizeElementTiming(el, options = {}) {
|
|
|
309
416
|
}
|
|
310
417
|
}
|
|
311
418
|
|
|
312
|
-
// Optimize from/to
|
|
419
|
+
// Optimize from/to/by (optimizeAnimationValues internally preserves ID refs)
|
|
313
420
|
for (const attr of ['from', 'to', 'by']) {
|
|
314
421
|
const val = el.getAttribute(attr);
|
|
315
|
-
if (val
|
|
422
|
+
if (val) {
|
|
316
423
|
const optimized = optimizeAnimationValues(val, precision);
|
|
317
424
|
if (optimized !== val) {
|
|
318
425
|
el.setAttribute(attr, optimized);
|
|
@@ -338,13 +445,24 @@ export const ANIMATION_ELEMENTS = ['animate', 'animatetransform', 'animatemotion
|
|
|
338
445
|
* @returns {{elementsModified: number, totalChanges: number, details: Array}}
|
|
339
446
|
*/
|
|
340
447
|
export function optimizeDocumentAnimationTiming(root, options = {}) {
|
|
448
|
+
// Validate root parameter
|
|
449
|
+
if (!root || typeof root !== 'object') {
|
|
450
|
+
throw new Error('optimizeDocumentAnimationTiming: root parameter must be a valid element');
|
|
451
|
+
}
|
|
452
|
+
|
|
341
453
|
let elementsModified = 0;
|
|
342
454
|
let totalChanges = 0;
|
|
343
455
|
const details = [];
|
|
344
456
|
|
|
345
457
|
const processElement = (el) => {
|
|
458
|
+
// Skip if not a valid element
|
|
459
|
+
if (!el || typeof el !== 'object') return;
|
|
460
|
+
|
|
346
461
|
const tagName = el.tagName?.toLowerCase();
|
|
347
462
|
|
|
463
|
+
// Skip if no tagName
|
|
464
|
+
if (!tagName) return;
|
|
465
|
+
|
|
348
466
|
if (ANIMATION_ELEMENTS.includes(tagName)) {
|
|
349
467
|
const result = optimizeElementTiming(el, options);
|
|
350
468
|
if (result.modified) {
|
|
@@ -358,7 +476,9 @@ export function optimizeDocumentAnimationTiming(root, options = {}) {
|
|
|
358
476
|
}
|
|
359
477
|
}
|
|
360
478
|
|
|
361
|
-
|
|
479
|
+
// Process children safely
|
|
480
|
+
const children = el.children || [];
|
|
481
|
+
for (const child of children) {
|
|
362
482
|
processElement(child);
|
|
363
483
|
}
|
|
364
484
|
};
|
|
@@ -197,6 +197,20 @@ export function parseJavaScriptIds(js) {
|
|
|
197
197
|
* @returns {{static: Set<string>, animation: Set<string>, css: Set<string>, js: Set<string>}}
|
|
198
198
|
*/
|
|
199
199
|
export function collectElementReferences(el) {
|
|
200
|
+
// Validate input parameter
|
|
201
|
+
if (!el || typeof el !== "object" || !(el instanceof SVGElement)) {
|
|
202
|
+
throw new TypeError(
|
|
203
|
+
"collectElementReferences: el must be a valid SVGElement",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Validate element has required methods
|
|
208
|
+
if (typeof el.getAttributeNames !== "function") {
|
|
209
|
+
throw new TypeError(
|
|
210
|
+
"collectElementReferences: el must have getAttributeNames method",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
200
214
|
const refs = {
|
|
201
215
|
static: new Set(),
|
|
202
216
|
animation: new Set(),
|
|
@@ -207,7 +221,14 @@ export function collectElementReferences(el) {
|
|
|
207
221
|
const tagName = el.tagName?.toLowerCase() || "";
|
|
208
222
|
const isAnimationElement = ANIMATION_ELEMENTS.includes(tagName);
|
|
209
223
|
|
|
210
|
-
|
|
224
|
+
const attributeNames = el.getAttributeNames();
|
|
225
|
+
if (!Array.isArray(attributeNames)) {
|
|
226
|
+
throw new TypeError(
|
|
227
|
+
"collectElementReferences: getAttributeNames must return an array",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const attrName of attributeNames) {
|
|
211
232
|
const value = el.getAttribute(attrName);
|
|
212
233
|
if (!value) continue;
|
|
213
234
|
|
|
@@ -253,8 +274,22 @@ export function collectElementReferences(el) {
|
|
|
253
274
|
|
|
254
275
|
// Check <mpath> element inside <animateMotion>
|
|
255
276
|
if (tagName === "animatemotion") {
|
|
277
|
+
if (typeof el.getElementsByTagName !== "function") {
|
|
278
|
+
throw new TypeError(
|
|
279
|
+
"collectElementReferences: el must have getElementsByTagName method",
|
|
280
|
+
);
|
|
281
|
+
}
|
|
256
282
|
const mpaths = el.getElementsByTagName("mpath");
|
|
283
|
+
// Validate mpaths is iterable
|
|
284
|
+
if (!mpaths || typeof mpaths[Symbol.iterator] !== "function") {
|
|
285
|
+
throw new TypeError(
|
|
286
|
+
"collectElementReferences: getElementsByTagName must return an iterable",
|
|
287
|
+
);
|
|
288
|
+
}
|
|
257
289
|
for (const mpath of mpaths) {
|
|
290
|
+
// Validate mpath has getAttribute method
|
|
291
|
+
if (!mpath || typeof mpath.getAttribute !== "function") continue;
|
|
292
|
+
|
|
258
293
|
for (const attr of HREF_ATTRIBUTES) {
|
|
259
294
|
const value = mpath.getAttribute(attr);
|
|
260
295
|
const id = parseHrefId(value);
|
|
@@ -281,6 +316,13 @@ export function collectElementReferences(el) {
|
|
|
281
316
|
* }}
|
|
282
317
|
*/
|
|
283
318
|
export function collectAllReferences(root) {
|
|
319
|
+
// Validate input parameter
|
|
320
|
+
if (!root || typeof root !== "object" || !(root instanceof SVGElement)) {
|
|
321
|
+
throw new TypeError(
|
|
322
|
+
"collectAllReferences: root must be a valid SVGElement",
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
284
326
|
const result = {
|
|
285
327
|
all: new Set(),
|
|
286
328
|
static: new Set(),
|
|
@@ -339,9 +381,11 @@ export function collectAllReferences(root) {
|
|
|
339
381
|
}
|
|
340
382
|
|
|
341
383
|
// Recurse to children
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
384
|
+
if (el.children && typeof el.children[Symbol.iterator] === "function") {
|
|
385
|
+
for (const child of el.children) {
|
|
386
|
+
if (child instanceof SVGElement) {
|
|
387
|
+
processElement(child, currentPath);
|
|
388
|
+
}
|
|
345
389
|
}
|
|
346
390
|
}
|
|
347
391
|
};
|
|
@@ -358,6 +402,14 @@ export function collectAllReferences(root) {
|
|
|
358
402
|
* @returns {boolean} True if the ID is referenced
|
|
359
403
|
*/
|
|
360
404
|
export function isIdReferenced(root, id) {
|
|
405
|
+
// Validate input parameters
|
|
406
|
+
if (!root || typeof root !== "object" || !(root instanceof SVGElement)) {
|
|
407
|
+
throw new TypeError("isIdReferenced: root must be a valid SVGElement");
|
|
408
|
+
}
|
|
409
|
+
if (!id || typeof id !== "string") {
|
|
410
|
+
throw new TypeError("isIdReferenced: id must be a non-empty string");
|
|
411
|
+
}
|
|
412
|
+
|
|
361
413
|
const refs = collectAllReferences(root);
|
|
362
414
|
return refs.all.has(id);
|
|
363
415
|
}
|
|
@@ -369,6 +421,14 @@ export function isIdReferenced(root, id) {
|
|
|
369
421
|
* @returns {{referenced: boolean, type: string|null, sources: string[]}}
|
|
370
422
|
*/
|
|
371
423
|
export function getIdReferenceInfo(root, id) {
|
|
424
|
+
// Validate input parameters
|
|
425
|
+
if (!root || typeof root !== "object" || !(root instanceof SVGElement)) {
|
|
426
|
+
throw new TypeError("getIdReferenceInfo: root must be a valid SVGElement");
|
|
427
|
+
}
|
|
428
|
+
if (!id || typeof id !== "string") {
|
|
429
|
+
throw new TypeError("getIdReferenceInfo: id must be a non-empty string");
|
|
430
|
+
}
|
|
431
|
+
|
|
372
432
|
const refs = collectAllReferences(root);
|
|
373
433
|
const details = refs.details.get(id);
|
|
374
434
|
|
|
@@ -387,6 +447,11 @@ export function getIdReferenceInfo(root, id) {
|
|
|
387
447
|
* @returns {{safeToRemove: string[], referenced: string[], animationReferenced: string[]}}
|
|
388
448
|
*/
|
|
389
449
|
export function findUnreferencedDefs(root) {
|
|
450
|
+
// Validate input parameter
|
|
451
|
+
if (!root || typeof root !== "object" || !(root instanceof SVGElement)) {
|
|
452
|
+
throw new TypeError("findUnreferencedDefs: root must be a valid SVGElement");
|
|
453
|
+
}
|
|
454
|
+
|
|
390
455
|
const refs = collectAllReferences(root);
|
|
391
456
|
|
|
392
457
|
const safeToRemove = [];
|
|
@@ -394,8 +459,26 @@ export function findUnreferencedDefs(root) {
|
|
|
394
459
|
const animationReferenced = [];
|
|
395
460
|
|
|
396
461
|
// Scan all defs
|
|
462
|
+
if (typeof root.getElementsByTagName !== "function") {
|
|
463
|
+
throw new TypeError(
|
|
464
|
+
"findUnreferencedDefs: root must have getElementsByTagName method",
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
397
468
|
const defsElements = root.getElementsByTagName("defs");
|
|
469
|
+
// Validate defsElements is iterable
|
|
470
|
+
if (!defsElements || typeof defsElements[Symbol.iterator] !== "function") {
|
|
471
|
+
throw new TypeError(
|
|
472
|
+
"findUnreferencedDefs: getElementsByTagName must return an iterable",
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
398
476
|
for (const defs of defsElements) {
|
|
477
|
+
// Validate defs.children exists and is iterable
|
|
478
|
+
if (!defs.children || typeof defs.children[Symbol.iterator] !== "function") {
|
|
479
|
+
continue; // Skip this defs element if children is not iterable
|
|
480
|
+
}
|
|
481
|
+
|
|
399
482
|
for (const child of defs.children) {
|
|
400
483
|
if (child instanceof SVGElement) {
|
|
401
484
|
const id = child.getAttribute("id");
|
|
@@ -423,20 +506,54 @@ export function findUnreferencedDefs(root) {
|
|
|
423
506
|
* @returns {{removed: string[], kept: string[], keptForAnimation: string[]}}
|
|
424
507
|
*/
|
|
425
508
|
export function removeUnreferencedDefsSafe(root) {
|
|
509
|
+
// Validate input parameter
|
|
510
|
+
if (!root || typeof root !== "object" || !(root instanceof SVGElement)) {
|
|
511
|
+
throw new TypeError(
|
|
512
|
+
"removeUnreferencedDefsSafe: root must be a valid SVGElement",
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
426
516
|
const { safeToRemove, referenced, animationReferenced } =
|
|
427
517
|
findUnreferencedDefs(root);
|
|
428
518
|
|
|
429
519
|
const removed = [];
|
|
430
520
|
|
|
431
521
|
// Only remove elements that are truly unreferenced
|
|
522
|
+
if (typeof root.getElementsByTagName !== "function") {
|
|
523
|
+
throw new TypeError(
|
|
524
|
+
"removeUnreferencedDefsSafe: root must have getElementsByTagName method",
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
432
528
|
const defsElements = root.getElementsByTagName("defs");
|
|
529
|
+
// Validate defsElements is iterable
|
|
530
|
+
if (!defsElements || typeof defsElements[Symbol.iterator] !== "function") {
|
|
531
|
+
throw new TypeError(
|
|
532
|
+
"removeUnreferencedDefsSafe: getElementsByTagName must return an iterable",
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
433
536
|
for (const defs of defsElements) {
|
|
537
|
+
// Validate defs.children exists and is iterable before spreading
|
|
538
|
+
if (!defs.children || typeof defs.children[Symbol.iterator] !== "function") {
|
|
539
|
+
continue; // Skip this defs element if children is not iterable
|
|
540
|
+
}
|
|
541
|
+
|
|
434
542
|
for (const child of [...defs.children]) {
|
|
435
543
|
if (child instanceof SVGElement) {
|
|
436
544
|
const id = child.getAttribute("id");
|
|
437
545
|
if (id && safeToRemove.includes(id)) {
|
|
438
|
-
|
|
439
|
-
|
|
546
|
+
try {
|
|
547
|
+
defs.removeChild(child);
|
|
548
|
+
removed.push(id);
|
|
549
|
+
} catch (error) {
|
|
550
|
+
// If removeChild fails (child not a direct child of defs), skip it
|
|
551
|
+
// This prevents the function from crashing on edge cases
|
|
552
|
+
console.warn(
|
|
553
|
+
`removeUnreferencedDefsSafe: Failed to remove child with id="${id}":`,
|
|
554
|
+
error.message,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
440
557
|
}
|
|
441
558
|
}
|
|
442
559
|
}
|