@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
@@ -55,11 +55,14 @@ export function findLayers(doc) {
55
55
  layers.push({
56
56
  element: el,
57
57
  label: getLayerLabel(el),
58
- id: el.getAttribute('id')
58
+ id: (typeof el.getAttribute === 'function' ? el.getAttribute('id') : null)
59
59
  });
60
60
  }
61
- for (const child of el.children) {
62
- walk(child);
61
+ // Safety check: ensure children is an array before iteration
62
+ if (Array.isArray(el.children)) {
63
+ for (const child of el.children) {
64
+ walk(child);
65
+ }
63
66
  }
64
67
  };
65
68
 
@@ -75,6 +78,9 @@ export function findLayers(doc) {
75
78
  * @returns {Object|null} Named view settings or null if not found
76
79
  */
77
80
  export function getNamedViewSettings(doc) {
81
+ // Validate doc parameter
82
+ if (!doc) return null;
83
+
78
84
  // Find namedview element - may be direct child or nested
79
85
  let namedview = null;
80
86
 
@@ -84,7 +90,7 @@ export function getNamedViewSettings(doc) {
84
90
  namedview = el;
85
91
  return;
86
92
  }
87
- if (el.children) {
93
+ if (el.children && Array.isArray(el.children)) {
88
94
  for (const child of el.children) {
89
95
  findNamedview(child);
90
96
  if (namedview) return;
@@ -93,7 +99,7 @@ export function getNamedViewSettings(doc) {
93
99
  };
94
100
 
95
101
  findNamedview(doc);
96
- if (!namedview) return null;
102
+ if (!namedview || typeof namedview.getAttribute !== 'function') return null;
97
103
 
98
104
  return {
99
105
  pagecolor: namedview.getAttribute('pagecolor'),
@@ -118,21 +124,27 @@ export function getNamedViewSettings(doc) {
118
124
  * @returns {Array<{position: string, orientation: string, id: string|null}>} Array of guide info
119
125
  */
120
126
  export function findGuides(doc) {
127
+ // Validate doc parameter
128
+ if (!doc) return [];
129
+
121
130
  const guides = [];
122
131
 
123
132
  const walk = (el) => {
124
133
  if (!el || !el.children) return;
125
134
  if (el.tagName === 'sodipodi:guide') {
126
135
  guides.push({
127
- position: el.getAttribute('position'),
128
- orientation: el.getAttribute('orientation'),
129
- id: el.getAttribute('id'),
130
- inkscapeColor: el.getAttribute('inkscape:color'),
131
- inkscapeLabel: el.getAttribute('inkscape:label')
136
+ position: el.getAttribute?.('position') || null,
137
+ orientation: el.getAttribute?.('orientation') || null,
138
+ id: el.getAttribute?.('id') || null,
139
+ inkscapeColor: el.getAttribute?.('inkscape:color') || null,
140
+ inkscapeLabel: el.getAttribute?.('inkscape:label') || null
132
141
  });
133
142
  }
134
- for (const child of el.children) {
135
- walk(child);
143
+ // Safety check: ensure children is an array before iteration
144
+ if (Array.isArray(el.children)) {
145
+ for (const child of el.children) {
146
+ walk(child);
147
+ }
136
148
  }
137
149
  };
138
150
 
@@ -148,7 +160,7 @@ export function findGuides(doc) {
148
160
  * @returns {Object|null} Arc parameters or null if not an arc
149
161
  */
150
162
  export function getArcParameters(element) {
151
- if (!element) return null;
163
+ if (!element || typeof element.getAttribute !== 'function') return null;
152
164
 
153
165
  const type = element.getAttribute('sodipodi:type');
154
166
  if (type !== 'arc') return null;
@@ -183,7 +195,7 @@ export function getNodeTypes(element) {
183
195
  * @returns {Object|null} Export settings or null if not set
184
196
  */
185
197
  export function getExportSettings(element) {
186
- if (!element) return null;
198
+ if (!element || typeof element.getAttribute !== 'function') return null;
187
199
 
188
200
  const filename = element.getAttribute('inkscape:export-filename');
189
201
  const xdpi = element.getAttribute('inkscape:export-xdpi');
@@ -191,10 +203,14 @@ export function getExportSettings(element) {
191
203
 
192
204
  if (!filename && !xdpi && !ydpi) return null;
193
205
 
206
+ // Parse DPI values and handle NaN by returning null
207
+ const parsedXdpi = xdpi ? parseFloat(xdpi) : null;
208
+ const parsedYdpi = ydpi ? parseFloat(ydpi) : null;
209
+
194
210
  return {
195
211
  filename,
196
- xdpi: xdpi ? parseFloat(xdpi) : null,
197
- ydpi: ydpi ? parseFloat(ydpi) : null
212
+ xdpi: (parsedXdpi !== null && !isNaN(parsedXdpi)) ? parsedXdpi : null,
213
+ ydpi: (parsedYdpi !== null && !isNaN(parsedYdpi)) ? parsedYdpi : null
198
214
  };
199
215
  }
200
216
 
@@ -205,7 +221,8 @@ export function getExportSettings(element) {
205
221
  * @returns {boolean} True if element is a tiled clone
206
222
  */
207
223
  export function isTiledClone(element) {
208
- return element?.hasAttribute('inkscape:tiled-clone-of') || false;
224
+ if (!element || typeof element.hasAttribute !== 'function') return false;
225
+ return element.hasAttribute('inkscape:tiled-clone-of');
209
226
  }
210
227
 
211
228
  /**
@@ -241,6 +258,9 @@ export function hasInkscapeNamespaces(doc) {
241
258
  * @returns {Object} The document (modified in place)
242
259
  */
243
260
  export function ensureInkscapeNamespaces(doc) {
261
+ // Validate doc parameter
262
+ if (!doc) return doc;
263
+
244
264
  const svg = doc.documentElement || doc;
245
265
 
246
266
  // Safety check: ensure getAttribute and setAttribute methods exist
@@ -271,6 +291,9 @@ export function ensureInkscapeNamespaces(doc) {
271
291
  * @returns {Set<string>} Set of referenced IDs
272
292
  */
273
293
  export function findReferencedIds(element) {
294
+ // Validate element parameter
295
+ if (!element) return new Set();
296
+
274
297
  const ids = new Set();
275
298
 
276
299
  // Attributes that can contain url(#id) references
@@ -284,14 +307,14 @@ export function findReferencedIds(element) {
284
307
  const hrefAttrs = ['href', 'xlink:href'];
285
308
 
286
309
  const extractUrlId = (value) => {
287
- if (!value) return null;
310
+ if (!value || typeof value !== 'string') return null;
288
311
  // Match url(#id) or url("#id")
289
312
  const match = value.match(/url\(["']?#([^"')]+)["']?\)/);
290
313
  return match ? match[1] : null;
291
314
  };
292
315
 
293
316
  const extractHrefId = (value) => {
294
- if (!value) return null;
317
+ if (!value || typeof value !== 'string') return null;
295
318
  // Match #id references
296
319
  if (value.startsWith('#')) {
297
320
  return value.slice(1);
@@ -324,7 +347,7 @@ export function findReferencedIds(element) {
324
347
  }
325
348
 
326
349
  // Recurse into children
327
- if (el.children) {
350
+ if (el.children && Array.isArray(el.children)) {
328
351
  for (const child of el.children) {
329
352
  walk(child);
330
353
  }
@@ -342,6 +365,9 @@ export function findReferencedIds(element) {
342
365
  * @returns {Map<string, Object>} Map of ID to element
343
366
  */
344
367
  export function buildDefsMapFromDefs(doc) {
368
+ // Validate doc parameter
369
+ if (!doc) return new Map();
370
+
345
371
  const defsMap = new Map();
346
372
 
347
373
  const walk = (el) => {
@@ -354,7 +380,7 @@ export function buildDefsMapFromDefs(doc) {
354
380
  }
355
381
 
356
382
  // Recurse
357
- if (el.children) {
383
+ if (el.children && Array.isArray(el.children)) {
358
384
  for (const child of el.children) {
359
385
  walk(child);
360
386
  }
@@ -367,7 +393,7 @@ export function buildDefsMapFromDefs(doc) {
367
393
  if (el.tagName === 'defs') {
368
394
  walk(el);
369
395
  }
370
- if (el.children) {
396
+ if (el.children && Array.isArray(el.children)) {
371
397
  for (const child of el.children) {
372
398
  findDefs(child);
373
399
  }
@@ -387,6 +413,14 @@ export function buildDefsMapFromDefs(doc) {
387
413
  * @returns {Set<string>} Complete set of IDs including all nested dependencies
388
414
  */
389
415
  export function resolveDefsDependencies(initialIds, defsMap) {
416
+ // Validate parameters
417
+ if (!initialIds || !(initialIds instanceof Set)) {
418
+ throw new Error('initialIds must be a Set');
419
+ }
420
+ if (!defsMap || !(defsMap instanceof Map)) {
421
+ throw new Error('defsMap must be a Map');
422
+ }
423
+
390
424
  const resolved = new Set();
391
425
  const toProcess = [...initialIds];
392
426
 
@@ -424,7 +458,7 @@ export function cloneElement(element) {
424
458
  const attrs = {};
425
459
  if (element._attributes) {
426
460
  Object.assign(attrs, element._attributes);
427
- } else if (element.getAttributeNames) {
461
+ } else if (typeof element.getAttributeNames === 'function') {
428
462
  for (const name of element.getAttributeNames()) {
429
463
  attrs[name] = element.getAttribute(name);
430
464
  }
@@ -432,7 +466,7 @@ export function cloneElement(element) {
432
466
 
433
467
  // Clone children recursively
434
468
  const clonedChildren = [];
435
- if (element.children) {
469
+ if (element.children && Array.isArray(element.children)) {
436
470
  for (const child of element.children) {
437
471
  const clonedChild = cloneElement(child);
438
472
  if (clonedChild) {
@@ -464,6 +498,11 @@ export function cloneElement(element) {
464
498
  * @returns {{svg: SVGElement, layerInfo: {id: string, label: string}}} Extracted SVG and layer info
465
499
  */
466
500
  export function extractLayer(doc, layerOrId, options = {}) {
501
+ // Validate doc parameter
502
+ if (!doc) {
503
+ throw new Error('doc parameter is required');
504
+ }
505
+
467
506
  const { preserveTransform = true } = options;
468
507
 
469
508
  // Find the layer element
@@ -471,11 +510,17 @@ export function extractLayer(doc, layerOrId, options = {}) {
471
510
  if (typeof layerOrId === 'string') {
472
511
  const layers = findLayers(doc);
473
512
  const found = layers.find(l => l.id === layerOrId || l.label === layerOrId);
474
- if (!found || !found.element) {
475
- throw new Error(`Layer not found or invalid: ${layerOrId}`);
513
+ if (!found) {
514
+ throw new Error(`Layer not found: ${layerOrId}`);
515
+ }
516
+ if (!found.element) {
517
+ throw new Error(`Layer element is invalid for: ${layerOrId}`);
476
518
  }
477
519
  layer = found.element;
478
520
  } else {
521
+ if (!layerOrId) {
522
+ throw new Error('layerOrId parameter is required');
523
+ }
479
524
  layer = layerOrId;
480
525
  }
481
526
 
@@ -499,7 +544,7 @@ export function extractLayer(doc, layerOrId, options = {}) {
499
544
  const svgAttrs = {};
500
545
  if (svgRoot._attributes) {
501
546
  Object.assign(svgAttrs, svgRoot._attributes);
502
- } else if (svgRoot.getAttributeNames) {
547
+ } else if (typeof svgRoot.getAttributeNames === 'function') {
503
548
  for (const name of svgRoot.getAttributeNames()) {
504
549
  svgAttrs[name] = svgRoot.getAttribute(name);
505
550
  }
@@ -538,7 +583,7 @@ export function extractLayer(doc, layerOrId, options = {}) {
538
583
 
539
584
  // Get layer info
540
585
  const layerInfo = {
541
- id: layer.getAttribute('id'),
586
+ id: (typeof layer.getAttribute === 'function' ? layer.getAttribute('id') : null),
542
587
  label: getLayerLabel(layer)
543
588
  };
544
589
 
@@ -555,6 +600,11 @@ export function extractLayer(doc, layerOrId, options = {}) {
555
600
  * @returns {Array<{svg: Object, layerInfo: {id: string, label: string}}>} Array of extracted SVGs
556
601
  */
557
602
  export function extractAllLayers(doc, options = {}) {
603
+ // Validate doc parameter
604
+ if (!doc) {
605
+ throw new Error('doc parameter is required');
606
+ }
607
+
558
608
  const { includeHidden = false } = options;
559
609
  const layers = findLayers(doc);
560
610
  const results = [];
@@ -564,6 +614,9 @@ export function extractAllLayers(doc, options = {}) {
564
614
 
565
615
  // Skip hidden layers unless requested
566
616
  if (!includeHidden) {
617
+ // Validate getAttribute method exists
618
+ if (typeof layer.getAttribute !== 'function') continue;
619
+
567
620
  const style = layer.getAttribute('style') || '';
568
621
  const display = layer.getAttribute('display');
569
622
  const visibility = layer.getAttribute('visibility');
@@ -596,6 +649,11 @@ export function extractAllLayers(doc, options = {}) {
596
649
  * @returns {Object} Summary of shared resources
597
650
  */
598
651
  export function analyzeLayerDependencies(doc) {
652
+ // Validate doc parameter
653
+ if (!doc) {
654
+ throw new Error('doc parameter is required');
655
+ }
656
+
599
657
  const layers = findLayers(doc);
600
658
  const defsMap = buildDefsMapFromDefs(doc);
601
659
  const layerRefs = new Map(); // layer ID -> Set of referenced def IDs
package/src/logger.js CHANGED
@@ -126,11 +126,21 @@ export const Logger = {
126
126
  * @private
127
127
  */
128
128
  _format(level, message) {
129
+ // Why: Validate parameters to prevent crashes from invalid inputs
130
+ if (typeof level !== 'string' || !level) {
131
+ throw new Error('Logger._format: level must be a non-empty string');
132
+ }
133
+ if (message === null || message === undefined) {
134
+ throw new Error('Logger._format: message cannot be null or undefined');
135
+ }
136
+ // Why: Convert message to string to handle non-string inputs gracefully
137
+ const messageStr = String(message);
138
+
129
139
  if (this.timestamps) {
130
140
  const ts = new Date().toISOString();
131
- return `[${ts}] [${level}] ${message}`;
141
+ return `[${ts}] [${level}] ${messageStr}`;
132
142
  }
133
- return `[${level}] ${message}`;
143
+ return `[${level}] ${messageStr}`;
134
144
  },
135
145
 
136
146
  /**
@@ -144,7 +154,14 @@ export const Logger = {
144
154
  _bufferWrite(message) {
145
155
  if (!this.logToFile) return;
146
156
 
147
- this._buffer.push(message);
157
+ // Why: Validate message parameter to prevent buffer corruption
158
+ if (message === null || message === undefined) {
159
+ throw new Error('Logger._bufferWrite: message cannot be null or undefined');
160
+ }
161
+ // Why: Convert to string to handle non-string inputs gracefully
162
+ const messageStr = String(message);
163
+
164
+ this._buffer.push(messageStr);
148
165
 
149
166
  // Why: Flush when buffer is full to prevent unbounded memory growth
150
167
  if (this._buffer.length >= LOG_BUFFER_SIZE) {
@@ -188,6 +205,10 @@ export const Logger = {
188
205
  * @param {...any} args - Additional arguments
189
206
  */
190
207
  error(message, ...args) {
208
+ // Why: Validate message parameter to prevent crashes
209
+ if (message === null || message === undefined) {
210
+ throw new Error('Logger.error: message cannot be null or undefined');
211
+ }
191
212
  if (this.level >= LogLevel.ERROR) {
192
213
  const formatted = this._format('ERROR', message);
193
214
  console.error(formatted, ...args);
@@ -203,6 +224,10 @@ export const Logger = {
203
224
  * @param {...any} args - Additional arguments
204
225
  */
205
226
  warn(message, ...args) {
227
+ // Why: Validate message parameter to prevent crashes
228
+ if (message === null || message === undefined) {
229
+ throw new Error('Logger.warn: message cannot be null or undefined');
230
+ }
206
231
  if (this.level >= LogLevel.WARN) {
207
232
  const formatted = this._format('WARN', message);
208
233
  console.warn(formatted, ...args);
@@ -216,6 +241,10 @@ export const Logger = {
216
241
  * @param {...any} args - Additional arguments
217
242
  */
218
243
  info(message, ...args) {
244
+ // Why: Validate message parameter to prevent crashes
245
+ if (message === null || message === undefined) {
246
+ throw new Error('Logger.info: message cannot be null or undefined');
247
+ }
219
248
  if (this.level >= LogLevel.INFO) {
220
249
  const formatted = this._format('INFO', message);
221
250
  console.log(formatted, ...args);
@@ -229,6 +258,10 @@ export const Logger = {
229
258
  * @param {...any} args - Additional arguments
230
259
  */
231
260
  debug(message, ...args) {
261
+ // Why: Validate message parameter to prevent crashes
262
+ if (message === null || message === undefined) {
263
+ throw new Error('Logger.debug: message cannot be null or undefined');
264
+ }
232
265
  if (this.level >= LogLevel.DEBUG) {
233
266
  const formatted = this._format('DEBUG', message);
234
267
  console.log(formatted, ...args);
@@ -257,6 +290,10 @@ export const Logger = {
257
290
  * @param {number} level - Log level from LogLevel enum
258
291
  */
259
292
  export function setLogLevel(level) {
293
+ // Why: Validate level is a number to catch type errors
294
+ if (typeof level !== 'number' || !Number.isFinite(level)) {
295
+ throw new Error(`setLogLevel: level must be a finite number, got ${typeof level}`);
296
+ }
260
297
  // Why: Validate level is within valid range to catch typos
261
298
  if (level < LogLevel.SILENT || level > LogLevel.DEBUG) {
262
299
  throw new Error(`Invalid log level: ${level}. Use LogLevel.SILENT (0) through LogLevel.DEBUG (4)`);
@@ -281,10 +318,18 @@ export function getLogLevel() {
281
318
  * @param {boolean} [withTimestamps=true] - Include timestamps
282
319
  */
283
320
  export function enableFileLogging(filePath, withTimestamps = true) {
321
+ // Why: Validate filePath type to catch type errors
322
+ if (typeof filePath !== 'string') {
323
+ throw new Error(`enableFileLogging: filePath must be a string, got ${typeof filePath}`);
324
+ }
284
325
  // Why: Don't accept empty/null paths - would cause confusing errors later
285
326
  if (!filePath) {
286
327
  throw new Error('File path required for enableFileLogging()');
287
328
  }
329
+ // Why: Validate withTimestamps type to catch type errors
330
+ if (typeof withTimestamps !== 'boolean') {
331
+ throw new Error(`enableFileLogging: withTimestamps must be a boolean, got ${typeof withTimestamps}`);
332
+ }
288
333
  Logger.logToFile = filePath;
289
334
  Logger.timestamps = withTimestamps;
290
335
  }