@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.
Files changed (45) hide show
  1. package/bin/svg-matrix.js +310 -61
  2. package/bin/svglinter.cjs +102 -3
  3. package/bin/svgm.js +236 -27
  4. package/package.json +1 -1
  5. package/src/animation-optimization.js +137 -17
  6. package/src/animation-references.js +123 -6
  7. package/src/arc-length.js +213 -4
  8. package/src/bezier-analysis.js +217 -21
  9. package/src/bezier-intersections.js +275 -12
  10. package/src/browser-verify.js +237 -4
  11. package/src/clip-path-resolver.js +168 -0
  12. package/src/convert-path-data.js +479 -28
  13. package/src/css-specificity.js +73 -10
  14. package/src/douglas-peucker.js +219 -2
  15. package/src/flatten-pipeline.js +284 -26
  16. package/src/geometry-to-path.js +250 -25
  17. package/src/gjk-collision.js +236 -33
  18. package/src/index.js +261 -3
  19. package/src/inkscape-support.js +86 -28
  20. package/src/logger.js +48 -3
  21. package/src/marker-resolver.js +278 -74
  22. package/src/mask-resolver.js +265 -66
  23. package/src/matrix.js +44 -5
  24. package/src/mesh-gradient.js +352 -102
  25. package/src/off-canvas-detection.js +382 -13
  26. package/src/path-analysis.js +192 -18
  27. package/src/path-data-plugins.js +309 -5
  28. package/src/path-optimization.js +129 -5
  29. package/src/path-simplification.js +188 -32
  30. package/src/pattern-resolver.js +454 -106
  31. package/src/polygon-clip.js +324 -1
  32. package/src/svg-boolean-ops.js +226 -9
  33. package/src/svg-collections.js +7 -5
  34. package/src/svg-flatten.js +386 -62
  35. package/src/svg-parser.js +179 -8
  36. package/src/svg-rendering-context.js +235 -6
  37. package/src/svg-toolbox.js +45 -8
  38. package/src/svg2-polyfills.js +40 -10
  39. package/src/transform-decomposition.js +258 -32
  40. package/src/transform-optimization.js +259 -13
  41. package/src/transforms2d.js +82 -9
  42. package/src/transforms3d.js +62 -10
  43. package/src/use-symbol-resolver.js +286 -42
  44. package/src/vector.js +64 -8
  45. 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
- const values = parts.map((s) => D(parseFloat(s)));
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) => D(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
- if (filterDef.x !== undefined) extentX = D(filterDef.x).abs();
407
- if (filterDef.y !== undefined) extentY = D(filterDef.y).abs();
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) {
@@ -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 = maxDistance + 1;
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(precision);
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
- return opacity ? parseFloat(opacity) : 1.0;
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
- return [
5039
- parseInt(hexValue.slice(0, 2), 16),
5040
- parseInt(hexValue.slice(2, 4), 16),
5041
- parseInt(hexValue.slice(4, 6), 16),
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);
@@ -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
- if (!doc) return { meshGradients: [], hatches: [], contextPaint: false, autoStartReverse: false };
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 (svg.children && svg.children.length > 0) {
265
- // Find first non-defs element to insert before
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 (svg.children[i].tagName === 'defs') {
269
- insertIdx = i + 1;
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 if (svg.children) {
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 (child.tagName === 'script') {
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')) {