@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
package/src/svg-parser.js CHANGED
@@ -18,6 +18,13 @@ Decimal.set({ precision: 80 });
18
18
  * @returns {void}
19
19
  */
20
20
  function setOwnerDocument(el, doc) {
21
+ // Validation: Check for null/undefined parameters
22
+ if (!el || !doc) {
23
+ throw new Error("setOwnerDocument: element and document are required");
24
+ }
25
+ if (!el.children || !Array.isArray(el.children)) {
26
+ throw new Error("setOwnerDocument: element must have a children array");
27
+ }
21
28
  el.ownerDocument = doc;
22
29
  for (const child of el.children) {
23
30
  // eslint-disable-next-line no-use-before-define -- Class hoisting is intentional
@@ -33,11 +40,11 @@ function setOwnerDocument(el, doc) {
33
40
  * @returns {SVGElement} Root element with DOM-like interface
34
41
  */
35
42
  export function parseSVG(svgString) {
36
- if (typeof svgString !== 'string') {
37
- throw new Error('parseSVG: input must be a string');
43
+ if (typeof svgString !== "string") {
44
+ throw new Error("parseSVG: input must be a string");
38
45
  }
39
46
  if (svgString.trim().length === 0) {
40
- throw new Error('parseSVG: input cannot be empty');
47
+ throw new Error("parseSVG: input cannot be empty");
41
48
  }
42
49
 
43
50
  // Normalize whitespace but preserve content
@@ -79,6 +86,10 @@ class SVGDocument {
79
86
  * @returns {SVGElement}
80
87
  */
81
88
  createElement(tagName) {
89
+ // Validation: Ensure tagName is a valid string
90
+ if (!tagName || typeof tagName !== "string") {
91
+ throw new Error("createElement: tagName must be a non-empty string");
92
+ }
82
93
  // eslint-disable-next-line no-use-before-define -- Class hoisting is intentional
83
94
  const el = new SVGElement(tagName);
84
95
  el.ownerDocument = this;
@@ -92,6 +103,10 @@ class SVGDocument {
92
103
  * @returns {SVGElement}
93
104
  */
94
105
  createElementNS(namespace, tagName) {
106
+ // Validation: Ensure namespace is a string (though unused, for API compatibility)
107
+ if (namespace !== null && namespace !== undefined && typeof namespace !== "string") {
108
+ throw new Error("createElementNS: namespace must be a string");
109
+ }
95
110
  return this.createElement(tagName);
96
111
  }
97
112
  }
@@ -118,6 +133,22 @@ export class SVGElement {
118
133
  * @param {string} textContent - Text content of the element
119
134
  */
120
135
  constructor(tagName, attributes = {}, children = [], textContent = "") {
136
+ // Validation: Ensure tagName is a non-empty string
137
+ if (!tagName || typeof tagName !== "string") {
138
+ throw new Error("SVGElement: tagName must be a non-empty string");
139
+ }
140
+ // Validation: Ensure attributes is an object
141
+ if (attributes !== null && typeof attributes !== "object") {
142
+ throw new Error("SVGElement: attributes must be an object");
143
+ }
144
+ // Validation: Ensure children is an array
145
+ if (!Array.isArray(children)) {
146
+ throw new Error("SVGElement: children must be an array");
147
+ }
148
+ // Validation: Ensure textContent is a string
149
+ if (typeof textContent !== "string") {
150
+ throw new Error("SVGElement: textContent must be a string");
151
+ }
121
152
  // Preserve original case for W3C SVG compliance (linearGradient, clipPath, etc.)
122
153
  this.tagName = tagName;
123
154
  this.nodeName = tagName.toUpperCase();
@@ -142,6 +173,10 @@ export class SVGElement {
142
173
  * @returns {string|null} Attribute value or null
143
174
  */
144
175
  getAttribute(name) {
176
+ // Validation: Ensure name is a string
177
+ if (typeof name !== "string") {
178
+ throw new Error("getAttribute: name must be a string");
179
+ }
145
180
  return this._attributes[name] ?? null;
146
181
  }
147
182
 
@@ -151,7 +186,16 @@ export class SVGElement {
151
186
  * @param {string} value - Attribute value
152
187
  */
153
188
  setAttribute(name, value) {
154
- this._attributes[name] = value;
189
+ // Validation: Ensure name is a string
190
+ if (typeof name !== "string") {
191
+ throw new Error("setAttribute: name must be a string");
192
+ }
193
+ // Validation: Convert value to string if not already
194
+ if (value !== null && value !== undefined) {
195
+ this._attributes[name] = String(value);
196
+ } else {
197
+ this._attributes[name] = "";
198
+ }
155
199
  }
156
200
 
157
201
  /**
@@ -160,6 +204,10 @@ export class SVGElement {
160
204
  * @returns {boolean}
161
205
  */
162
206
  hasAttribute(name) {
207
+ // Validation: Ensure name is a string
208
+ if (typeof name !== "string") {
209
+ throw new Error("hasAttribute: name must be a string");
210
+ }
163
211
  return name in this._attributes;
164
212
  }
165
213
 
@@ -168,6 +216,10 @@ export class SVGElement {
168
216
  * @param {string} name - Attribute name
169
217
  */
170
218
  removeAttribute(name) {
219
+ // Validation: Ensure name is a string
220
+ if (typeof name !== "string") {
221
+ throw new Error("removeAttribute: name must be a string");
222
+ }
171
223
  delete this._attributes[name];
172
224
  }
173
225
 
@@ -185,6 +237,10 @@ export class SVGElement {
185
237
  * @returns {SVGElement[]}
186
238
  */
187
239
  getElementsByTagName(tagName) {
240
+ // Validation: Ensure tagName is a string
241
+ if (typeof tagName !== "string") {
242
+ throw new Error("getElementsByTagName: tagName must be a string");
243
+ }
188
244
  const tag = tagName.toLowerCase();
189
245
  const results = [];
190
246
 
@@ -210,6 +266,10 @@ export class SVGElement {
210
266
  * @returns {SVGElement|null}
211
267
  */
212
268
  getElementById(id) {
269
+ // Validation: Ensure id is a string
270
+ if (typeof id !== "string") {
271
+ throw new Error("getElementById: id must be a string");
272
+ }
213
273
  const search = (el) => {
214
274
  if (el.getAttribute("id") === id) {
215
275
  return el;
@@ -233,6 +293,10 @@ export class SVGElement {
233
293
  * @returns {SVGElement|null}
234
294
  */
235
295
  querySelector(selector) {
296
+ // Validation: Ensure selector is a string
297
+ if (typeof selector !== "string") {
298
+ throw new Error("querySelector: selector must be a string");
299
+ }
236
300
  const results = this.querySelectorAll(selector);
237
301
  return results[0] || null;
238
302
  }
@@ -243,6 +307,10 @@ export class SVGElement {
243
307
  * @returns {SVGElement[]}
244
308
  */
245
309
  querySelectorAll(selector) {
310
+ // Validation: Ensure selector is a string
311
+ if (typeof selector !== "string") {
312
+ throw new Error("querySelectorAll: selector must be a string");
313
+ }
246
314
  const results = [];
247
315
  const matchers = parseSelector(selector);
248
316
 
@@ -267,6 +335,10 @@ export class SVGElement {
267
335
  * @returns {boolean}
268
336
  */
269
337
  matches(selector) {
338
+ // Validation: Ensure selector is a string
339
+ if (typeof selector !== "string") {
340
+ throw new Error("matches: selector must be a string");
341
+ }
270
342
  const matchers = parseSelector(selector);
271
343
  return matchesAllSelectors(this, matchers);
272
344
  }
@@ -278,6 +350,10 @@ export class SVGElement {
278
350
  * @returns {SVGElement}
279
351
  */
280
352
  cloneNode(deep = true) {
353
+ // Validation: Ensure deep is a boolean
354
+ if (typeof deep !== "boolean") {
355
+ throw new Error("cloneNode: deep must be a boolean");
356
+ }
281
357
  const clonedChildren = deep
282
358
  ? // eslint-disable-next-line no-confusing-arrow -- Prettier multiline format
283
359
  this.children.map((c) =>
@@ -310,6 +386,10 @@ export class SVGElement {
310
386
  * @returns {SVGElement} The appended child
311
387
  */
312
388
  appendChild(child) {
389
+ // Validation: Ensure child is not null/undefined
390
+ if (child === null || child === undefined) {
391
+ throw new Error("appendChild: child cannot be null or undefined");
392
+ }
313
393
  // DOM spec: Remove child from its current parent before appending
314
394
  if (child instanceof SVGElement && child.parentNode) {
315
395
  child.parentNode.removeChild(child);
@@ -328,6 +408,10 @@ export class SVGElement {
328
408
  * @returns {SVGElement} The removed child
329
409
  */
330
410
  removeChild(child) {
411
+ // Validation: Ensure child is not null/undefined
412
+ if (child === null || child === undefined) {
413
+ throw new Error("removeChild: child cannot be null or undefined");
414
+ }
331
415
  const idx = this.children.indexOf(child);
332
416
  if (idx >= 0) {
333
417
  this.children.splice(idx, 1);
@@ -347,6 +431,14 @@ export class SVGElement {
347
431
  * @returns {SVGElement} The inserted child
348
432
  */
349
433
  insertBefore(newChild, refChild) {
434
+ // Validation: Ensure newChild is not null/undefined
435
+ if (newChild === null || newChild === undefined) {
436
+ throw new Error("insertBefore: newChild cannot be null or undefined");
437
+ }
438
+ // Validation: Ensure refChild is not null/undefined
439
+ if (refChild === null || refChild === undefined) {
440
+ throw new Error("insertBefore: refChild cannot be null or undefined");
441
+ }
350
442
  // DOM spec: Remove newChild from its current parent first
351
443
  if (newChild instanceof SVGElement && newChild.parentNode) {
352
444
  newChild.parentNode.removeChild(newChild);
@@ -372,6 +464,14 @@ export class SVGElement {
372
464
  * @returns {SVGElement} The replaced (old) child
373
465
  */
374
466
  replaceChild(newChild, oldChild) {
467
+ // Validation: Ensure newChild is not null/undefined
468
+ if (newChild === null || newChild === undefined) {
469
+ throw new Error("replaceChild: newChild cannot be null or undefined");
470
+ }
471
+ // Validation: Ensure oldChild is not null/undefined
472
+ if (oldChild === null || oldChild === undefined) {
473
+ throw new Error("replaceChild: oldChild cannot be null or undefined");
474
+ }
375
475
  // DOM spec: Remove newChild from its current parent first
376
476
  if (newChild instanceof SVGElement && newChild.parentNode) {
377
477
  newChild.parentNode.removeChild(newChild);
@@ -398,9 +498,14 @@ export class SVGElement {
398
498
  const styleAttr = this.getAttribute("style") || "";
399
499
  const styles = {};
400
500
  styleAttr.split(";").forEach((pair) => {
401
- const [key, val] = pair.split(":").map((s) => s.trim());
402
- if (key && val) {
403
- styles[key] = val;
501
+ // Edge case: Handle malformed styles, multiple colons, and empty pairs
502
+ const colonIdx = pair.indexOf(":");
503
+ if (colonIdx > 0) {
504
+ const key = pair.slice(0, colonIdx).trim();
505
+ const val = pair.slice(colonIdx + 1).trim();
506
+ if (key && val) {
507
+ styles[key] = val;
508
+ }
404
509
  }
405
510
  });
406
511
  return styles;
@@ -451,6 +556,14 @@ export class SVGElement {
451
556
  * @returns {string}
452
557
  */
453
558
  serialize(indent = 0, minify = false) {
559
+ // Validation: Ensure indent is a valid number
560
+ if (typeof indent !== "number" || !Number.isFinite(indent) || indent < 0) {
561
+ throw new Error("serialize: indent must be a non-negative finite number");
562
+ }
563
+ // Validation: Ensure minify is a boolean
564
+ if (typeof minify !== "boolean") {
565
+ throw new Error("serialize: minify must be a boolean");
566
+ }
454
567
  const pad = minify ? "" : " ".repeat(indent);
455
568
 
456
569
  // Check for CDATA pending marker (used by embedExternalDependencies for scripts)
@@ -545,6 +658,12 @@ export class SVGElement {
545
658
  * @returns {boolean} True if whitespace should be preserved, false otherwise
546
659
  */
547
660
  function _shouldPreserveWhitespace(element) {
661
+ // Validation: Ensure element is not null/undefined
662
+ if (!element) {
663
+ throw new Error(
664
+ "_shouldPreserveWhitespace: element cannot be null or undefined",
665
+ );
666
+ }
548
667
  let current = element;
549
668
  while (current) {
550
669
  const xmlSpace = current.getAttribute("xml:space");
@@ -564,6 +683,22 @@ function _shouldPreserveWhitespace(element) {
564
683
  * @returns {{element: SVGElement|null, endPos: number}} Parsed element and final position
565
684
  */
566
685
  function parseElement(str, pos, inheritPreserveSpace = false) {
686
+ // Validation: Ensure str is a string
687
+ if (typeof str !== "string") {
688
+ throw new Error("parseElement: str must be a string");
689
+ }
690
+ // Validation: Ensure pos is a valid number
691
+ if (typeof pos !== "number" || !Number.isFinite(pos) || pos < 0) {
692
+ throw new Error("parseElement: pos must be a non-negative finite number");
693
+ }
694
+ // Validation: Ensure str is not empty
695
+ if (str.length === 0) {
696
+ return { element: null, endPos: 0 };
697
+ }
698
+ // Bounds check: Ensure pos is within string bounds
699
+ if (pos >= str.length) {
700
+ return { element: null, endPos: pos };
701
+ }
567
702
  // Use local variable to avoid reassigning parameter (ESLint no-param-reassign)
568
703
  let position = pos;
569
704
 
@@ -787,6 +922,10 @@ function parseElement(str, pos, inheritPreserveSpace = false) {
787
922
  * @returns {Array<{tag: string|null, id: string|null, classes: Array<string>, attrs: Array<{name: string, value: string|undefined}>}>} Array of matcher objects
788
923
  */
789
924
  function parseSelector(selector) {
925
+ // Validation: Ensure selector is a string
926
+ if (typeof selector !== "string") {
927
+ throw new Error("parseSelector: selector must be a string");
928
+ }
790
929
  const matchers = [];
791
930
  const parts = selector.trim().split(/\s*,\s*/);
792
931
 
@@ -838,6 +977,14 @@ function parseSelector(selector) {
838
977
  * @returns {boolean} True if element matches any matcher
839
978
  */
840
979
  function matchesAllSelectors(el, matchers) {
980
+ // Validation: Ensure el is a valid SVGElement
981
+ if (!(el instanceof SVGElement)) {
982
+ throw new Error("matchesAllSelectors: el must be an SVGElement");
983
+ }
984
+ // Validation: Ensure matchers is an array
985
+ if (!Array.isArray(matchers)) {
986
+ throw new Error("matchesAllSelectors: matchers must be an array");
987
+ }
841
988
  return matchers.some((matcher) => {
842
989
  // Case-insensitive tag matching (tagName preserves original case)
843
990
  if (matcher.tag && el.tagName.toLowerCase() !== matcher.tag) return false;
@@ -878,6 +1025,8 @@ function escapeText(str) {
878
1025
  * @returns {string} Escaped attribute value
879
1026
  */
880
1027
  function escapeAttr(str) {
1028
+ // Edge case: Handle null/undefined by converting to empty string
1029
+ if (str === null || str === undefined) return "";
881
1030
  if (typeof str !== "string") return String(str);
882
1031
  return str
883
1032
  .replace(/&/g, "&amp;")
@@ -1027,6 +1176,14 @@ function unescapeText(str) {
1027
1176
  * @returns {Map<string, SVGElement>}
1028
1177
  */
1029
1178
  export function buildDefsMap(svgRoot) {
1179
+ // Validation: Ensure svgRoot is not null/undefined
1180
+ if (!svgRoot) {
1181
+ throw new Error("buildDefsMap: svgRoot cannot be null or undefined");
1182
+ }
1183
+ // Validation: Ensure svgRoot is an SVGElement with children
1184
+ if (!(svgRoot instanceof SVGElement)) {
1185
+ throw new Error("buildDefsMap: svgRoot must be an SVGElement");
1186
+ }
1030
1187
  const defsMap = new Map();
1031
1188
 
1032
1189
  // Find all elements with IDs
@@ -1053,6 +1210,20 @@ export function buildDefsMap(svgRoot) {
1053
1210
  * @returns {SVGElement[]}
1054
1211
  */
1055
1212
  export function findElementsWithAttribute(root, attrName) {
1213
+ // Validation: Ensure root is not null/undefined
1214
+ if (!root) {
1215
+ throw new Error(
1216
+ "findElementsWithAttribute: root cannot be null or undefined",
1217
+ );
1218
+ }
1219
+ // Validation: Ensure root is an SVGElement
1220
+ if (!(root instanceof SVGElement)) {
1221
+ throw new Error("findElementsWithAttribute: root must be an SVGElement");
1222
+ }
1223
+ // Validation: Ensure attrName is a string
1224
+ if (typeof attrName !== "string") {
1225
+ throw new Error("findElementsWithAttribute: attrName must be a string");
1226
+ }
1056
1227
  const results = [];
1057
1228
 
1058
1229
  const search = (el) => {
@@ -1090,7 +1261,7 @@ export function parseUrlReference(urlValue) {
1090
1261
  */
1091
1262
  export function serializeSVG(root, options = {}) {
1092
1263
  if (!root) {
1093
- throw new Error('serializeSVG: document is required');
1264
+ throw new Error("serializeSVG: document is required");
1094
1265
  }
1095
1266
  // Validate that root is a proper SVGElement with serialize method
1096
1267
  if (typeof root.serialize !== "function") {