@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
|
@@ -120,11 +120,23 @@ export class SVGRenderingContext {
|
|
|
120
120
|
* @param {Map} defsMap - Map of definitions (gradients, markers, clipPaths, etc.)
|
|
121
121
|
*/
|
|
122
122
|
constructor(element, inherited = {}, defsMap = null) {
|
|
123
|
+
// Validate element parameter - Why: prevent crashes from null/undefined element
|
|
124
|
+
if (!element) {
|
|
125
|
+
throw new Error("SVGRenderingContext: element parameter is required");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Validate inherited parameter - Why: ensure we can safely spread it
|
|
129
|
+
if (inherited !== null && typeof inherited !== "object") {
|
|
130
|
+
throw new Error(
|
|
131
|
+
"SVGRenderingContext: inherited must be an object or null",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
123
135
|
this.element = element;
|
|
124
136
|
this.defsMap = defsMap || new Map();
|
|
125
137
|
|
|
126
138
|
// Extract all properties with inheritance
|
|
127
|
-
this.properties = this._extractProperties(element, inherited);
|
|
139
|
+
this.properties = this._extractProperties(element, inherited || {});
|
|
128
140
|
|
|
129
141
|
// Parse stroke-dasharray into array
|
|
130
142
|
this.dashArray = this._parseDashArray(this.properties["stroke-dasharray"]);
|
|
@@ -157,6 +169,11 @@ export class SVGRenderingContext {
|
|
|
157
169
|
* @private
|
|
158
170
|
*/
|
|
159
171
|
_extractProperties(element, inherited) {
|
|
172
|
+
// Validate inherited is an object - Why: prevent spreading non-objects
|
|
173
|
+
if (typeof inherited !== "object" || inherited === null) {
|
|
174
|
+
throw new Error("_extractProperties: inherited must be an object");
|
|
175
|
+
}
|
|
176
|
+
|
|
160
177
|
const props = { ...SVG_DEFAULTS, ...inherited };
|
|
161
178
|
|
|
162
179
|
// Get attributes from element
|
|
@@ -206,6 +223,9 @@ export class SVGRenderingContext {
|
|
|
206
223
|
|
|
207
224
|
const declarations = style.split(";");
|
|
208
225
|
for (const decl of declarations) {
|
|
226
|
+
// Validate declaration has a colon - Why: prevent crashes on malformed CSS
|
|
227
|
+
if (!decl.includes(":")) continue;
|
|
228
|
+
|
|
209
229
|
const [prop, value] = decl.split(":").map((s) => s.trim());
|
|
210
230
|
if (prop && value) {
|
|
211
231
|
props[prop] = value;
|
|
@@ -225,7 +245,24 @@ export class SVGRenderingContext {
|
|
|
225
245
|
.toString()
|
|
226
246
|
.split(/[\s,]+/)
|
|
227
247
|
.filter((s) => s);
|
|
228
|
-
|
|
248
|
+
|
|
249
|
+
// Validate each value is a valid number - Why: prevent NaN values in array
|
|
250
|
+
const values = [];
|
|
251
|
+
for (const part of parts) {
|
|
252
|
+
const num = parseFloat(part);
|
|
253
|
+
if (isNaN(num) || !isFinite(num)) {
|
|
254
|
+
throw new Error(`_parseDashArray: invalid dash value '${part}'`);
|
|
255
|
+
}
|
|
256
|
+
// Validate non-negative - Why: SVG spec requires non-negative dash values
|
|
257
|
+
if (num < 0) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`_parseDashArray: dash values must be non-negative, got ${num}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
values.push(D(num));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (values.length === 0) return null;
|
|
229
266
|
|
|
230
267
|
// Per SVG spec, if odd number of values, duplicate the array
|
|
231
268
|
if (values.length % 2 === 1) {
|
|
@@ -332,6 +369,33 @@ export class SVGRenderingContext {
|
|
|
332
369
|
* @returns {Object} Expanded bounding box
|
|
333
370
|
*/
|
|
334
371
|
expandBBoxForStroke(bbox) {
|
|
372
|
+
// Validate bbox parameter - Why: prevent crashes from missing properties
|
|
373
|
+
if (!bbox || typeof bbox !== "object") {
|
|
374
|
+
throw new Error("expandBBoxForStroke: bbox must be an object");
|
|
375
|
+
}
|
|
376
|
+
// Use 'in' operator to check for properties - Why: allow 0 values which are falsy but valid
|
|
377
|
+
if (
|
|
378
|
+
!("x" in bbox) ||
|
|
379
|
+
!("y" in bbox) ||
|
|
380
|
+
!("width" in bbox) ||
|
|
381
|
+
!("height" in bbox)
|
|
382
|
+
) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
"expandBBoxForStroke: bbox must have x, y, width, height properties",
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
// Validate bbox properties are Decimal-like - Why: prevent crashes from calling Decimal methods on numbers
|
|
388
|
+
if (
|
|
389
|
+
typeof bbox.x.minus !== "function" ||
|
|
390
|
+
typeof bbox.y.minus !== "function" ||
|
|
391
|
+
typeof bbox.width.plus !== "function" ||
|
|
392
|
+
typeof bbox.height.plus !== "function"
|
|
393
|
+
) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
"expandBBoxForStroke: bbox properties must be Decimal instances",
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
335
399
|
if (!this.hasStroke) return bbox;
|
|
336
400
|
|
|
337
401
|
const extent = this.getStrokeExtent();
|
|
@@ -354,15 +418,54 @@ export class SVGRenderingContext {
|
|
|
354
418
|
* @returns {Object} Expanded bounding box
|
|
355
419
|
*/
|
|
356
420
|
expandBBoxForMarkers(bbox, markerSizes = null) {
|
|
421
|
+
// Validate bbox parameter - Why: prevent crashes from missing properties
|
|
422
|
+
if (!bbox || typeof bbox !== "object") {
|
|
423
|
+
throw new Error("expandBBoxForMarkers: bbox must be an object");
|
|
424
|
+
}
|
|
425
|
+
// Use 'in' operator to check for properties - Why: allow 0 values which are falsy but valid
|
|
426
|
+
if (
|
|
427
|
+
!("x" in bbox) ||
|
|
428
|
+
!("y" in bbox) ||
|
|
429
|
+
!("width" in bbox) ||
|
|
430
|
+
!("height" in bbox)
|
|
431
|
+
) {
|
|
432
|
+
throw new Error(
|
|
433
|
+
"expandBBoxForMarkers: bbox must have x, y, width, height properties",
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
// Validate bbox properties are Decimal-like - Why: prevent crashes from calling Decimal methods on numbers
|
|
437
|
+
if (
|
|
438
|
+
typeof bbox.x.minus !== "function" ||
|
|
439
|
+
typeof bbox.y.minus !== "function" ||
|
|
440
|
+
typeof bbox.width.plus !== "function" ||
|
|
441
|
+
typeof bbox.height.plus !== "function"
|
|
442
|
+
) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
"expandBBoxForMarkers: bbox properties must be Decimal instances",
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
357
448
|
if (!this.hasMarkers) return bbox;
|
|
358
449
|
|
|
359
450
|
// If marker sizes provided, use them; otherwise estimate from marker definitions
|
|
360
451
|
let maxMarkerSize = D(0);
|
|
361
452
|
|
|
362
453
|
if (markerSizes) {
|
|
454
|
+
// Validate markerSizes is an object - Why: prevent crashes when accessing properties
|
|
455
|
+
if (typeof markerSizes !== "object") {
|
|
456
|
+
throw new Error("expandBBoxForMarkers: markerSizes must be an object");
|
|
457
|
+
}
|
|
458
|
+
|
|
363
459
|
const sizes = [markerSizes.start, markerSizes.mid, markerSizes.end]
|
|
364
|
-
.filter((s) => s)
|
|
365
|
-
.map((s) =>
|
|
460
|
+
.filter((s) => s !== null && s !== undefined)
|
|
461
|
+
.map((s) => {
|
|
462
|
+
const num = typeof s === "number" ? s : parseFloat(s);
|
|
463
|
+
if (isNaN(num) || !isFinite(num)) {
|
|
464
|
+
throw new Error(`expandBBoxForMarkers: invalid marker size '${s}'`);
|
|
465
|
+
}
|
|
466
|
+
return D(num);
|
|
467
|
+
});
|
|
468
|
+
|
|
366
469
|
if (sizes.length > 0) {
|
|
367
470
|
maxMarkerSize = Decimal.max(...sizes);
|
|
368
471
|
}
|
|
@@ -393,6 +496,33 @@ export class SVGRenderingContext {
|
|
|
393
496
|
* @returns {Object} Expanded bounding box
|
|
394
497
|
*/
|
|
395
498
|
expandBBoxForFilter(bbox, filterDef = null) {
|
|
499
|
+
// Validate bbox parameter - Why: prevent crashes from missing properties
|
|
500
|
+
if (!bbox || typeof bbox !== "object") {
|
|
501
|
+
throw new Error("expandBBoxForFilter: bbox must be an object");
|
|
502
|
+
}
|
|
503
|
+
// Use 'in' operator to check for properties - Why: allow 0 values which are falsy but valid
|
|
504
|
+
if (
|
|
505
|
+
!("x" in bbox) ||
|
|
506
|
+
!("y" in bbox) ||
|
|
507
|
+
!("width" in bbox) ||
|
|
508
|
+
!("height" in bbox)
|
|
509
|
+
) {
|
|
510
|
+
throw new Error(
|
|
511
|
+
"expandBBoxForFilter: bbox must have x, y, width, height properties",
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
// Validate bbox properties are Decimal-like - Why: prevent crashes from calling Decimal methods on numbers
|
|
515
|
+
if (
|
|
516
|
+
typeof bbox.x.minus !== "function" ||
|
|
517
|
+
typeof bbox.y.minus !== "function" ||
|
|
518
|
+
typeof bbox.width.times !== "function" ||
|
|
519
|
+
typeof bbox.height.times !== "function"
|
|
520
|
+
) {
|
|
521
|
+
throw new Error(
|
|
522
|
+
"expandBBoxForFilter: bbox properties must be Decimal instances",
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
396
526
|
const filterRef = this.properties.filter;
|
|
397
527
|
if (!filterRef || filterRef === "none") return bbox;
|
|
398
528
|
|
|
@@ -403,8 +533,29 @@ export class SVGRenderingContext {
|
|
|
403
533
|
|
|
404
534
|
// If filter definition provided with explicit bounds, use those
|
|
405
535
|
if (filterDef) {
|
|
406
|
-
|
|
407
|
-
if (filterDef
|
|
536
|
+
// Validate filterDef is an object - Why: prevent crashes when accessing properties
|
|
537
|
+
if (typeof filterDef !== "object") {
|
|
538
|
+
throw new Error("expandBBoxForFilter: filterDef must be an object");
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (filterDef.x !== undefined) {
|
|
542
|
+
const xVal = parseFloat(filterDef.x);
|
|
543
|
+
if (isNaN(xVal) || !isFinite(xVal)) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`expandBBoxForFilter: invalid filterDef.x value '${filterDef.x}'`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
extentX = D(xVal).abs();
|
|
549
|
+
}
|
|
550
|
+
if (filterDef.y !== undefined) {
|
|
551
|
+
const yVal = parseFloat(filterDef.y);
|
|
552
|
+
if (isNaN(yVal) || !isFinite(yVal)) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`expandBBoxForFilter: invalid filterDef.y value '${filterDef.y}'`,
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
extentY = D(yVal).abs();
|
|
558
|
+
}
|
|
408
559
|
}
|
|
409
560
|
|
|
410
561
|
return {
|
|
@@ -423,6 +574,36 @@ export class SVGRenderingContext {
|
|
|
423
574
|
* @returns {Object} Full rendered bounding box
|
|
424
575
|
*/
|
|
425
576
|
getRenderedBBox(geometryBBox, options = {}) {
|
|
577
|
+
// Validate geometryBBox parameter - Why: prevent crashes from missing properties
|
|
578
|
+
if (!geometryBBox || typeof geometryBBox !== "object") {
|
|
579
|
+
throw new Error("getRenderedBBox: geometryBBox must be an object");
|
|
580
|
+
}
|
|
581
|
+
// Use 'in' operator to check for properties - Why: allow 0 values which are falsy but valid
|
|
582
|
+
if (
|
|
583
|
+
!("x" in geometryBBox) ||
|
|
584
|
+
!("y" in geometryBBox) ||
|
|
585
|
+
!("width" in geometryBBox) ||
|
|
586
|
+
!("height" in geometryBBox)
|
|
587
|
+
) {
|
|
588
|
+
throw new Error(
|
|
589
|
+
"getRenderedBBox: geometryBBox must have x, y, width, height properties",
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
// Validate geometryBBox properties are Decimal-like - Why: prevent crashes from calling Decimal methods on numbers
|
|
593
|
+
if (
|
|
594
|
+
typeof geometryBBox.x.minus !== "function" ||
|
|
595
|
+
typeof geometryBBox.y.minus !== "function"
|
|
596
|
+
) {
|
|
597
|
+
throw new Error(
|
|
598
|
+
"getRenderedBBox: geometryBBox properties must be Decimal instances",
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Validate options parameter - Why: prevent crashes when accessing properties
|
|
603
|
+
if (options !== null && typeof options !== "object") {
|
|
604
|
+
throw new Error("getRenderedBBox: options must be an object or null");
|
|
605
|
+
}
|
|
606
|
+
|
|
426
607
|
let bbox = { ...geometryBBox };
|
|
427
608
|
|
|
428
609
|
// Expand for stroke
|
|
@@ -444,6 +625,14 @@ export class SVGRenderingContext {
|
|
|
444
625
|
* @returns {Array} Polygon(s) representing the full rendered area
|
|
445
626
|
*/
|
|
446
627
|
getFilledArea(polygon) {
|
|
628
|
+
// Validate polygon parameter - Why: prevent crashes from non-array or empty polygon
|
|
629
|
+
if (!Array.isArray(polygon)) {
|
|
630
|
+
throw new Error("getFilledArea: polygon must be an array");
|
|
631
|
+
}
|
|
632
|
+
if (polygon.length === 0) {
|
|
633
|
+
throw new Error("getFilledArea: polygon must not be empty");
|
|
634
|
+
}
|
|
635
|
+
|
|
447
636
|
const areas = [];
|
|
448
637
|
|
|
449
638
|
// Add fill area (if filled)
|
|
@@ -484,6 +673,24 @@ export class SVGRenderingContext {
|
|
|
484
673
|
* @returns {boolean} True if point is inside rendered area
|
|
485
674
|
*/
|
|
486
675
|
isPointInRenderedArea(point, polygon) {
|
|
676
|
+
// Validate point parameter - Why: prevent crashes from missing x, y properties
|
|
677
|
+
if (!point || typeof point !== "object") {
|
|
678
|
+
throw new Error("isPointInRenderedArea: point must be an object");
|
|
679
|
+
}
|
|
680
|
+
if (point.x === undefined || point.y === undefined) {
|
|
681
|
+
throw new Error(
|
|
682
|
+
"isPointInRenderedArea: point must have x and y properties",
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Validate polygon parameter - Why: prevent crashes from non-array or empty polygon
|
|
687
|
+
if (!Array.isArray(polygon)) {
|
|
688
|
+
throw new Error("isPointInRenderedArea: polygon must be an array");
|
|
689
|
+
}
|
|
690
|
+
if (polygon.length === 0) {
|
|
691
|
+
throw new Error("isPointInRenderedArea: polygon must not be empty");
|
|
692
|
+
}
|
|
693
|
+
|
|
487
694
|
// Check fill area
|
|
488
695
|
if (this.hasFill) {
|
|
489
696
|
const fillRule =
|
|
@@ -523,6 +730,13 @@ export class SVGRenderingContext {
|
|
|
523
730
|
* @returns {Object} {canMerge: boolean, reason: string}
|
|
524
731
|
*/
|
|
525
732
|
canMergeWith(other) {
|
|
733
|
+
// Validate other parameter - Why: prevent crashes from non-SVGRenderingContext instances
|
|
734
|
+
if (!(other instanceof SVGRenderingContext)) {
|
|
735
|
+
throw new Error(
|
|
736
|
+
"canMergeWith: other must be an instance of SVGRenderingContext",
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
|
|
526
740
|
// Fill rules must match
|
|
527
741
|
if (this.fillRule !== other.fillRule) {
|
|
528
742
|
return { canMerge: false, reason: "Different fill-rule" };
|
|
@@ -608,6 +822,11 @@ export function createRenderingContext(
|
|
|
608
822
|
inherited = {},
|
|
609
823
|
defsMap = null,
|
|
610
824
|
) {
|
|
825
|
+
// Validate parameters - Why: delegate validation to constructor which has proper error messages
|
|
826
|
+
if (!element) {
|
|
827
|
+
throw new Error("createRenderingContext: element parameter is required");
|
|
828
|
+
}
|
|
829
|
+
|
|
611
830
|
return new SVGRenderingContext(element, inherited, defsMap);
|
|
612
831
|
}
|
|
613
832
|
|
|
@@ -618,6 +837,11 @@ export function createRenderingContext(
|
|
|
618
837
|
* @returns {Object} Inherited properties
|
|
619
838
|
*/
|
|
620
839
|
export function getInheritedProperties(element) {
|
|
840
|
+
// Validate element parameter - Why: prevent crashes from null/undefined element
|
|
841
|
+
if (!element) {
|
|
842
|
+
throw new Error("getInheritedProperties: element parameter is required");
|
|
843
|
+
}
|
|
844
|
+
|
|
621
845
|
const inherited = {};
|
|
622
846
|
|
|
623
847
|
// Inheritable properties per SVG spec
|
|
@@ -644,6 +868,11 @@ export function getInheritedProperties(element) {
|
|
|
644
868
|
"font-weight",
|
|
645
869
|
];
|
|
646
870
|
|
|
871
|
+
// Check if element has parentNode - Why: prevent crashes when accessing parentNode
|
|
872
|
+
if (!element.parentNode) {
|
|
873
|
+
return inherited;
|
|
874
|
+
}
|
|
875
|
+
|
|
647
876
|
let current = element.parentNode;
|
|
648
877
|
while (current && current.tagName) {
|
|
649
878
|
for (const prop of inheritableProps) {
|
package/src/svg-toolbox.js
CHANGED
|
@@ -597,6 +597,10 @@ const levenshteinCache = new Map();
|
|
|
597
597
|
* @returns {number} Edit distance
|
|
598
598
|
*/
|
|
599
599
|
const levenshteinDistance = (a, b) => {
|
|
600
|
+
// Validate parameters
|
|
601
|
+
if (typeof a !== "string" || typeof b !== "string") {
|
|
602
|
+
throw new Error("Both parameters must be strings");
|
|
603
|
+
}
|
|
600
604
|
// Check cache first
|
|
601
605
|
const key = `${a}|${b}`;
|
|
602
606
|
if (levenshteinCache.has(key)) return levenshteinCache.get(key);
|
|
@@ -639,9 +643,12 @@ const resetLevenshteinCache = () => levenshteinCache.clear();
|
|
|
639
643
|
* @returns {string|null} Closest match or null if none within threshold
|
|
640
644
|
*/
|
|
641
645
|
const findClosestMatch = (name, validSet, maxDistance = 2) => {
|
|
646
|
+
if (!name || typeof name !== "string") return null;
|
|
647
|
+
if (!validSet || !(validSet instanceof Set) || validSet.size === 0) return null;
|
|
648
|
+
const validMaxDistance = (typeof maxDistance !== "number" || maxDistance < 0) ? 2 : maxDistance;
|
|
642
649
|
const lower = name.toLowerCase();
|
|
643
650
|
let closest = null;
|
|
644
|
-
let minDist =
|
|
651
|
+
let minDist = validMaxDistance + 1;
|
|
645
652
|
|
|
646
653
|
for (const valid of validSet) {
|
|
647
654
|
const dist = levenshteinDistance(lower, valid.toLowerCase());
|
|
@@ -706,6 +713,7 @@ function hasCircularReference(startId, getNextId, maxDepth = 100) {
|
|
|
706
713
|
* @returns {string} SVG string with proper CDATA sections
|
|
707
714
|
*/
|
|
708
715
|
function fixCDATASections(svgString) {
|
|
716
|
+
if (!svgString || typeof svgString !== "string") return svgString || "";
|
|
709
717
|
// Pattern to find script/style elements marked with data-cdata-pending
|
|
710
718
|
// and fix their CDATA wrapping
|
|
711
719
|
return svgString.replace(
|
|
@@ -809,9 +817,15 @@ function isElement(obj) {
|
|
|
809
817
|
* @returns {string} Formatted string without trailing zeros
|
|
810
818
|
*/
|
|
811
819
|
export function formatPrecision(value, precision = DEFAULT_PRECISION) {
|
|
820
|
+
if (value === null || value === undefined) return "0";
|
|
821
|
+
let validPrecision = precision;
|
|
822
|
+
if (typeof precision !== "number" || isNaN(precision) || precision < 0) {
|
|
823
|
+
validPrecision = DEFAULT_PRECISION;
|
|
824
|
+
}
|
|
825
|
+
if (validPrecision > MAX_PRECISION) validPrecision = MAX_PRECISION;
|
|
812
826
|
const d = D(value);
|
|
813
827
|
// Round to precision, then remove trailing zeros
|
|
814
|
-
const fixed = d.toFixed(
|
|
828
|
+
const fixed = d.toFixed(validPrecision);
|
|
815
829
|
// Remove trailing zeros after decimal point
|
|
816
830
|
if (fixed.includes(".")) {
|
|
817
831
|
return fixed.replace(/\.?0+$/, "");
|
|
@@ -871,6 +885,9 @@ export const OutputFormat = {
|
|
|
871
885
|
* @returns {string} Input type from InputType enum
|
|
872
886
|
*/
|
|
873
887
|
export function detectInputType(input) {
|
|
888
|
+
if (input === null || input === undefined) {
|
|
889
|
+
throw new Error("Input cannot be null or undefined");
|
|
890
|
+
}
|
|
874
891
|
if (typeof input === "string") {
|
|
875
892
|
const trimmed = input.trim();
|
|
876
893
|
if (trimmed.startsWith("<")) {
|
|
@@ -2674,6 +2691,11 @@ export const removeViewBox = createOperation((doc, _options = {}) => {
|
|
|
2674
2691
|
const w = parseFloat(width);
|
|
2675
2692
|
const h = parseFloat(height);
|
|
2676
2693
|
|
|
2694
|
+
// Validate parsed values are not NaN
|
|
2695
|
+
if (isNaN(w) || isNaN(h) || vb.some((v) => isNaN(v))) {
|
|
2696
|
+
return doc; // Skip if any value is invalid
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2677
2699
|
// Use epsilon comparison for floating point values to avoid precision issues
|
|
2678
2700
|
const epsilon = 1e-6;
|
|
2679
2701
|
if (
|
|
@@ -3260,6 +3282,11 @@ export const convertEllipseToCircle = createOperation((doc, _options = {}) => {
|
|
|
3260
3282
|
const rx = parseFloat(ellipse.getAttribute("rx") || "0");
|
|
3261
3283
|
const ry = parseFloat(ellipse.getAttribute("ry") || "0");
|
|
3262
3284
|
|
|
3285
|
+
// Validate parsed values are not NaN
|
|
3286
|
+
if (isNaN(rx) || isNaN(ry)) {
|
|
3287
|
+
continue; // Skip if any value is invalid
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3263
3290
|
if (Math.abs(rx - ry) < 0.001) {
|
|
3264
3291
|
const circle = new SVGElement("circle", {});
|
|
3265
3292
|
|
|
@@ -4942,7 +4969,9 @@ function getStopOpacity(stop) {
|
|
|
4942
4969
|
}
|
|
4943
4970
|
}
|
|
4944
4971
|
|
|
4945
|
-
|
|
4972
|
+
// Parse opacity value and validate it's not NaN
|
|
4973
|
+
const parsedOpacity = opacity ? parseFloat(opacity) : 1.0;
|
|
4974
|
+
return isNaN(parsedOpacity) ? 1.0 : parsedOpacity;
|
|
4946
4975
|
}
|
|
4947
4976
|
|
|
4948
4977
|
/**
|
|
@@ -4952,6 +4981,10 @@ function getStopOpacity(stop) {
|
|
|
4952
4981
|
* @returns {string} Hex color string (e.g., "#ff8800")
|
|
4953
4982
|
*/
|
|
4954
4983
|
function normalizeColor(color) {
|
|
4984
|
+
// Validate color parameter is not null/undefined
|
|
4985
|
+
if (!color || typeof color !== "string") {
|
|
4986
|
+
return "#000000"; // Return default black if invalid
|
|
4987
|
+
}
|
|
4955
4988
|
const colorValue = color.trim().toLowerCase();
|
|
4956
4989
|
|
|
4957
4990
|
// Already hex format
|
|
@@ -5035,11 +5068,15 @@ function interpolateColors(color1, color2, t) {
|
|
|
5035
5068
|
hexValue[2] +
|
|
5036
5069
|
hexValue[2];
|
|
5037
5070
|
}
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
]
|
|
5071
|
+
// Parse RGB components and validate they're not NaN
|
|
5072
|
+
const r = parseInt(hexValue.slice(0, 2), 16);
|
|
5073
|
+
const g = parseInt(hexValue.slice(2, 4), 16);
|
|
5074
|
+
const b = parseInt(hexValue.slice(4, 6), 16);
|
|
5075
|
+
// Return default black [0, 0, 0] if any component is NaN
|
|
5076
|
+
if (isNaN(r) || isNaN(g) || isNaN(b)) {
|
|
5077
|
+
return [0, 0, 0];
|
|
5078
|
+
}
|
|
5079
|
+
return [r, g, b];
|
|
5043
5080
|
};
|
|
5044
5081
|
|
|
5045
5082
|
const [r1, g1, b1] = parseHex(color1);
|
package/src/svg2-polyfills.js
CHANGED
|
@@ -63,6 +63,9 @@ let useMinifiedPolyfills = true;
|
|
|
63
63
|
* @param {boolean} minify - True to use minified (default), false for full version
|
|
64
64
|
*/
|
|
65
65
|
export function setPolyfillMinification(minify) {
|
|
66
|
+
if (typeof minify !== 'boolean') {
|
|
67
|
+
throw new Error('setPolyfillMinification: minify parameter must be a boolean');
|
|
68
|
+
}
|
|
66
69
|
useMinifiedPolyfills = minify;
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -83,7 +86,10 @@ export const SVG2_FEATURES = {
|
|
|
83
86
|
* @returns {{meshGradients: string[], hatches: string[], contextPaint: boolean, autoStartReverse: boolean}} Detected features
|
|
84
87
|
*/
|
|
85
88
|
export function detectSVG2Features(doc) {
|
|
86
|
-
|
|
89
|
+
// WHY: Validate doc is an object before attempting to traverse it
|
|
90
|
+
if (!doc || typeof doc !== 'object') {
|
|
91
|
+
return { meshGradients: [], hatches: [], contextPaint: false, autoStartReverse: false };
|
|
92
|
+
}
|
|
87
93
|
|
|
88
94
|
const features = {
|
|
89
95
|
meshGradients: [],
|
|
@@ -123,7 +129,7 @@ export function detectSVG2Features(doc) {
|
|
|
123
129
|
}
|
|
124
130
|
|
|
125
131
|
// Recurse into children
|
|
126
|
-
if (el.children) {
|
|
132
|
+
if (el.children && Array.isArray(el.children)) {
|
|
127
133
|
for (const child of el.children) {
|
|
128
134
|
walk(child);
|
|
129
135
|
}
|
|
@@ -206,6 +212,24 @@ function generateHatchPolyfillCode() {
|
|
|
206
212
|
* @returns {string|null} Complete polyfill script or null if none needed
|
|
207
213
|
*/
|
|
208
214
|
export function generatePolyfillScript(features) {
|
|
215
|
+
// WHY: Explicit null check before typeof check prevents null passing as object
|
|
216
|
+
if (!features || typeof features !== 'object') {
|
|
217
|
+
throw new Error('generatePolyfillScript: features parameter must be an object');
|
|
218
|
+
}
|
|
219
|
+
if (!Array.isArray(features.meshGradients)) {
|
|
220
|
+
throw new Error('generatePolyfillScript: features.meshGradients must be an array');
|
|
221
|
+
}
|
|
222
|
+
if (!Array.isArray(features.hatches)) {
|
|
223
|
+
throw new Error('generatePolyfillScript: features.hatches must be an array');
|
|
224
|
+
}
|
|
225
|
+
// WHY: Validate boolean properties to prevent undefined/wrong type usage
|
|
226
|
+
if (typeof features.contextPaint !== 'boolean') {
|
|
227
|
+
throw new Error('generatePolyfillScript: features.contextPaint must be a boolean');
|
|
228
|
+
}
|
|
229
|
+
if (typeof features.autoStartReverse !== 'boolean') {
|
|
230
|
+
throw new Error('generatePolyfillScript: features.autoStartReverse must be a boolean');
|
|
231
|
+
}
|
|
232
|
+
|
|
209
233
|
const parts = [];
|
|
210
234
|
|
|
211
235
|
parts.push('/* SVG 2.0 Polyfills - Generated by svg-matrix */');
|
|
@@ -261,17 +285,22 @@ export function injectPolyfills(doc, options = {}) {
|
|
|
261
285
|
}, [], script);
|
|
262
286
|
|
|
263
287
|
// Insert script at beginning of SVG (after defs if present, else at start)
|
|
264
|
-
if (
|
|
265
|
-
//
|
|
288
|
+
if (!Array.isArray(svg.children)) {
|
|
289
|
+
// Initialize children array if missing
|
|
290
|
+
svg.children = [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (svg.children.length > 0) {
|
|
294
|
+
// Find position after the last defs element to insert the script
|
|
266
295
|
let insertIdx = 0;
|
|
267
296
|
for (let i = 0; i < svg.children.length; i++) {
|
|
268
|
-
if
|
|
269
|
-
|
|
270
|
-
break
|
|
297
|
+
// WHY: Optional chaining prevents errors if array contains null/undefined elements
|
|
298
|
+
if (svg.children[i]?.tagName === 'defs') {
|
|
299
|
+
insertIdx = i + 1; // Position after this defs (don't break - continue to find last defs)
|
|
271
300
|
}
|
|
272
301
|
}
|
|
273
302
|
svg.children.splice(insertIdx, 0, scriptEl);
|
|
274
|
-
} else
|
|
303
|
+
} else {
|
|
275
304
|
svg.children.push(scriptEl);
|
|
276
305
|
}
|
|
277
306
|
|
|
@@ -288,11 +317,12 @@ export function removePolyfills(doc) {
|
|
|
288
317
|
if (!doc) return doc;
|
|
289
318
|
|
|
290
319
|
const walk = (el) => {
|
|
291
|
-
if (!el || !el.children) return;
|
|
320
|
+
if (!el || !el.children || !Array.isArray(el.children)) return;
|
|
292
321
|
|
|
293
322
|
// Remove script elements that are svg-matrix polyfills
|
|
294
323
|
el.children = el.children.filter(child => {
|
|
295
|
-
if
|
|
324
|
+
// WHY: Optional chaining prevents errors if child is null/undefined
|
|
325
|
+
if (child?.tagName === 'script') {
|
|
296
326
|
const content = child.textContent || '';
|
|
297
327
|
if (content.includes('SVG 2.0 Polyfill') ||
|
|
298
328
|
content.includes('Generated by svg-matrix')) {
|