@emasoft/svg-matrix 1.0.27 → 1.0.29

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 (46) hide show
  1. package/README.md +325 -0
  2. package/bin/svg-matrix.js +994 -378
  3. package/bin/svglinter.cjs +4172 -433
  4. package/bin/svgm.js +744 -184
  5. package/package.json +16 -4
  6. package/src/animation-references.js +71 -52
  7. package/src/arc-length.js +160 -96
  8. package/src/bezier-analysis.js +257 -117
  9. package/src/bezier-intersections.js +411 -148
  10. package/src/browser-verify.js +240 -100
  11. package/src/clip-path-resolver.js +350 -142
  12. package/src/convert-path-data.js +279 -134
  13. package/src/css-specificity.js +78 -70
  14. package/src/flatten-pipeline.js +751 -263
  15. package/src/geometry-to-path.js +511 -182
  16. package/src/index.js +191 -46
  17. package/src/inkscape-support.js +404 -0
  18. package/src/marker-resolver.js +278 -164
  19. package/src/mask-resolver.js +209 -98
  20. package/src/matrix.js +147 -67
  21. package/src/mesh-gradient.js +187 -96
  22. package/src/off-canvas-detection.js +201 -104
  23. package/src/path-analysis.js +187 -107
  24. package/src/path-data-plugins.js +628 -167
  25. package/src/path-simplification.js +0 -1
  26. package/src/pattern-resolver.js +125 -88
  27. package/src/polygon-clip.js +111 -66
  28. package/src/svg-boolean-ops.js +194 -118
  29. package/src/svg-collections.js +48 -19
  30. package/src/svg-flatten.js +282 -164
  31. package/src/svg-parser.js +427 -200
  32. package/src/svg-rendering-context.js +147 -104
  33. package/src/svg-toolbox.js +16411 -3298
  34. package/src/svg2-polyfills.js +114 -245
  35. package/src/transform-decomposition.js +46 -41
  36. package/src/transform-optimization.js +89 -68
  37. package/src/transforms2d.js +49 -16
  38. package/src/transforms3d.js +58 -22
  39. package/src/use-symbol-resolver.js +150 -110
  40. package/src/vector.js +67 -15
  41. package/src/vendor/README.md +110 -0
  42. package/src/vendor/inkscape-hatch-polyfill.js +401 -0
  43. package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
  44. package/src/vendor/inkscape-mesh-polyfill.js +843 -0
  45. package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
  46. package/src/verification.js +288 -124
@@ -12,24 +12,44 @@
12
12
  * @module convert-path-data
13
13
  */
14
14
 
15
- import Decimal from 'decimal.js';
15
+ import Decimal from "decimal.js";
16
16
 
17
17
  Decimal.set({ precision: 80 });
18
18
 
19
- const D = x => (x instanceof Decimal ? x : new Decimal(x));
19
+ const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
20
20
 
21
21
  // SVG path command parameters count
22
22
  const COMMAND_PARAMS = {
23
- M: 2, m: 2, L: 2, l: 2, H: 1, h: 1, V: 1, v: 1,
24
- C: 6, c: 6, S: 4, s: 4, Q: 4, q: 4, T: 2, t: 2,
25
- A: 7, a: 7, Z: 0, z: 0
23
+ M: 2,
24
+ m: 2,
25
+ L: 2,
26
+ l: 2,
27
+ H: 1,
28
+ h: 1,
29
+ V: 1,
30
+ v: 1,
31
+ C: 6,
32
+ c: 6,
33
+ S: 4,
34
+ s: 4,
35
+ Q: 4,
36
+ q: 4,
37
+ T: 2,
38
+ t: 2,
39
+ A: 7,
40
+ a: 7,
41
+ Z: 0,
42
+ z: 0,
26
43
  };
27
44
 
28
45
  /**
29
- * Parse arc arguments specially - flags are always single 0 or 1 digits
46
+ * Parse arc arguments specially - flags are always single 0 or 1 digits.
30
47
  * Arc format: rx ry x-axis-rotation large-arc-flag sweep-flag x y
31
48
  * Flags can be written without separators: "0 01 20" or "0120"
32
49
  * BUG FIX #1: Handle compact notation where flags are concatenated with next number
50
+ * BUG FIX #5: Handle invalid arc flags gracefully - return null to signal parsing failure
51
+ * @param {string} argsStr - Arc arguments string to parse
52
+ * @returns {Array<number>|null} Parsed arc parameters or null if invalid
33
53
  */
34
54
  function parseArcArgs(argsStr) {
35
55
  const args = [];
@@ -50,13 +70,14 @@ function parseArcArgs(argsStr) {
50
70
  if (paramInArc === 3 || paramInArc === 4) {
51
71
  // Flags: must be single 0 or 1 (arc flags are always exactly one character)
52
72
  // BUG FIX #1: Handle compact notation like "0120" -> "01" (flags) + "20" (next number)
53
- if (argsStr[pos] === '0' || argsStr[pos] === '1') {
54
- args.push(argsStr[pos] === '1' ? 1 : 0);
73
+ if (argsStr[pos] === "0" || argsStr[pos] === "1") {
74
+ args.push(argsStr[pos] === "1" ? 1 : 0);
55
75
  pos++;
56
76
  arcIndex++;
57
77
  } else {
58
- // BUG FIX #1: Arc flags MUST be exactly 0 or 1 - throw error for invalid values
59
- throw new Error(`Invalid arc flag at position ${pos}: expected 0 or 1, got '${argsStr[pos]}'`);
78
+ // BUG FIX #5: Arc flags MUST be exactly 0 or 1 - return null for invalid values
79
+ // This signals the caller to skip this arc command gracefully
80
+ return null;
60
81
  }
61
82
  } else {
62
83
  // Regular number
@@ -82,7 +103,7 @@ function parseArcArgs(argsStr) {
82
103
  * @returns {Array<{command: string, args: number[]}>} Parsed commands
83
104
  */
84
105
  export function parsePath(d) {
85
- if (!d || typeof d !== 'string') return [];
106
+ if (!d || typeof d !== "string") return [];
86
107
 
87
108
  const commands = [];
88
109
  const cmdRegex = /([MmLlHhVvCcSsQqTtAaZz])([^MmLlHhVvCcSsQqTtAaZz]*)/g;
@@ -92,21 +113,24 @@ export function parsePath(d) {
92
113
  const cmd = match[1];
93
114
  const argsStr = match[2].trim();
94
115
 
95
- if (cmd === 'Z' || cmd === 'z') {
96
- commands.push({ command: 'Z', args: [] });
116
+ if (cmd === "Z" || cmd === "z") {
117
+ commands.push({ command: "Z", args: [] });
97
118
  // BUG FIX #1: Check for implicit M after Z (numbers after Z should start a new subpath)
98
119
  const remainingNums = argsStr.match(/-?\d*\.?\d+(?:[eE][+-]?\d+)?/g);
99
120
  if (remainingNums && remainingNums.length >= 2) {
100
121
  // Implicit M command after Z
101
122
  commands.push({
102
- command: 'M',
103
- args: [parseFloat(remainingNums[0]), parseFloat(remainingNums[1])]
123
+ command: "M",
124
+ args: [parseFloat(remainingNums[0]), parseFloat(remainingNums[1])],
104
125
  });
105
126
  // Continue parsing remaining args as implicit L
106
127
  for (let i = 2; i + 1 < remainingNums.length; i += 2) {
107
128
  commands.push({
108
- command: 'L',
109
- args: [parseFloat(remainingNums[i]), parseFloat(remainingNums[i + 1])]
129
+ command: "L",
130
+ args: [
131
+ parseFloat(remainingNums[i]),
132
+ parseFloat(remainingNums[i + 1]),
133
+ ],
110
134
  });
111
135
  }
112
136
  }
@@ -114,15 +138,23 @@ export function parsePath(d) {
114
138
  }
115
139
 
116
140
  let nums;
117
- if (cmd === 'A' || cmd === 'a') {
141
+ if (cmd === "A" || cmd === "a") {
118
142
  // Arc commands need special parsing for flags
119
143
  nums = parseArcArgs(argsStr);
120
144
 
145
+ // BUG FIX #5: If arc parsing failed due to invalid flags, skip this command
146
+ if (nums === null) {
147
+ console.warn(
148
+ `Invalid arc command with malformed flags - skipping: ${cmd}${argsStr}`,
149
+ );
150
+ continue; // Skip this command and continue processing the rest
151
+ }
152
+
121
153
  // BUG FIX #2: Normalize negative arc radii to absolute values per SVG spec Section 8.3.8
122
154
  // Process each complete arc (7 parameters)
123
155
  for (let i = 0; i < nums.length; i += 7) {
124
156
  if (i + 6 < nums.length) {
125
- nums[i] = Math.abs(nums[i]); // rx
157
+ nums[i] = Math.abs(nums[i]); // rx
126
158
  nums[i + 1] = Math.abs(nums[i + 1]); // ry
127
159
  }
128
160
  }
@@ -144,13 +176,18 @@ export function parsePath(d) {
144
176
  for (let i = 0; i < nums.length; i += paramCount) {
145
177
  const args = nums.slice(i, i + paramCount);
146
178
  if (args.length === paramCount) {
147
- const effectiveCmd = (i > 0 && (cmd === 'M' || cmd === 'm'))
148
- ? (cmd === 'M' ? 'L' : 'l')
149
- : cmd;
179
+ const effectiveCmd =
180
+ i > 0 && (cmd === "M" || cmd === "m")
181
+ ? cmd === "M"
182
+ ? "L"
183
+ : "l"
184
+ : cmd;
150
185
  commands.push({ command: effectiveCmd, args });
151
186
  } else if (args.length > 0) {
152
187
  // BUG FIX #4: Warn when args are incomplete
153
- console.warn(`Incomplete ${cmd} command: expected ${paramCount} args, got ${args.length} - remaining args dropped`);
188
+ console.warn(
189
+ `Incomplete ${cmd} command: expected ${paramCount} args, got ${args.length} - remaining args dropped`,
190
+ );
154
191
  }
155
192
  }
156
193
  }
@@ -167,26 +204,26 @@ export function parsePath(d) {
167
204
  */
168
205
  export function formatNumber(num, precision = 3) {
169
206
  // BUG FIX #3: Handle NaN, Infinity, -Infinity
170
- if (!isFinite(num)) return '0';
171
- if (num === 0) return '0';
207
+ if (!isFinite(num)) return "0";
208
+ if (num === 0) return "0";
172
209
 
173
210
  const factor = Math.pow(10, precision);
174
211
  const rounded = Math.round(num * factor) / factor;
175
212
 
176
213
  let str = rounded.toFixed(precision);
177
214
 
178
- if (str.includes('.')) {
179
- str = str.replace(/\.?0+$/, '');
215
+ if (str.includes(".")) {
216
+ str = str.replace(/\.?0+$/, "");
180
217
  }
181
218
 
182
- if (str.startsWith('0.')) {
219
+ if (str.startsWith("0.")) {
183
220
  str = str.substring(1);
184
- } else if (str.startsWith('-0.')) {
185
- str = '-' + str.substring(2);
221
+ } else if (str.startsWith("-0.")) {
222
+ str = "-" + str.substring(2);
186
223
  }
187
224
 
188
- if (str === '' || str === '.' || str === '-.') {
189
- str = '0';
225
+ if (str === "" || str === "." || str === "-.") {
226
+ str = "0";
190
227
  }
191
228
 
192
229
  return str;
@@ -204,19 +241,37 @@ export function toAbsolute(cmd, cx, cy) {
204
241
  const absArgs = [...args];
205
242
 
206
243
  switch (command) {
207
- case 'm': case 'l': case 't':
208
- absArgs[0] += cx; absArgs[1] += cy; break;
209
- case 'h': absArgs[0] += cx; break;
210
- case 'v': absArgs[0] += cy; break;
211
- case 'c':
212
- absArgs[0] += cx; absArgs[1] += cy;
213
- absArgs[2] += cx; absArgs[3] += cy;
214
- absArgs[4] += cx; absArgs[5] += cy; break;
215
- case 's': case 'q':
216
- absArgs[0] += cx; absArgs[1] += cy;
217
- absArgs[2] += cx; absArgs[3] += cy; break;
218
- case 'a':
219
- absArgs[5] += cx; absArgs[6] += cy; break;
244
+ case "m":
245
+ case "l":
246
+ case "t":
247
+ absArgs[0] += cx;
248
+ absArgs[1] += cy;
249
+ break;
250
+ case "h":
251
+ absArgs[0] += cx;
252
+ break;
253
+ case "v":
254
+ absArgs[0] += cy;
255
+ break;
256
+ case "c":
257
+ absArgs[0] += cx;
258
+ absArgs[1] += cy;
259
+ absArgs[2] += cx;
260
+ absArgs[3] += cy;
261
+ absArgs[4] += cx;
262
+ absArgs[5] += cy;
263
+ break;
264
+ case "s":
265
+ case "q":
266
+ absArgs[0] += cx;
267
+ absArgs[1] += cy;
268
+ absArgs[2] += cx;
269
+ absArgs[3] += cy;
270
+ break;
271
+ case "a":
272
+ absArgs[5] += cx;
273
+ absArgs[6] += cy;
274
+ break;
220
275
  }
221
276
 
222
277
  return { command: absCmd, args: absArgs };
@@ -228,26 +283,44 @@ export function toAbsolute(cmd, cx, cy) {
228
283
  export function toRelative(cmd, cx, cy) {
229
284
  const { command, args } = cmd;
230
285
 
231
- if (command === command.toLowerCase() && command !== 'z') return cmd;
232
- if (command === 'Z' || command === 'z') return { command: 'z', args: [] };
286
+ if (command === command.toLowerCase() && command !== "z") return cmd;
287
+ if (command === "Z" || command === "z") return { command: "z", args: [] };
233
288
 
234
289
  const relCmd = command.toLowerCase();
235
290
  const relArgs = [...args];
236
291
 
237
292
  switch (command) {
238
- case 'M': case 'L': case 'T':
239
- relArgs[0] -= cx; relArgs[1] -= cy; break;
240
- case 'H': relArgs[0] -= cx; break;
241
- case 'V': relArgs[0] -= cy; break;
242
- case 'C':
243
- relArgs[0] -= cx; relArgs[1] -= cy;
244
- relArgs[2] -= cx; relArgs[3] -= cy;
245
- relArgs[4] -= cx; relArgs[5] -= cy; break;
246
- case 'S': case 'Q':
247
- relArgs[0] -= cx; relArgs[1] -= cy;
248
- relArgs[2] -= cx; relArgs[3] -= cy; break;
249
- case 'A':
250
- relArgs[5] -= cx; relArgs[6] -= cy; break;
293
+ case "M":
294
+ case "L":
295
+ case "T":
296
+ relArgs[0] -= cx;
297
+ relArgs[1] -= cy;
298
+ break;
299
+ case "H":
300
+ relArgs[0] -= cx;
301
+ break;
302
+ case "V":
303
+ relArgs[0] -= cy;
304
+ break;
305
+ case "C":
306
+ relArgs[0] -= cx;
307
+ relArgs[1] -= cy;
308
+ relArgs[2] -= cx;
309
+ relArgs[3] -= cy;
310
+ relArgs[4] -= cx;
311
+ relArgs[5] -= cy;
312
+ break;
313
+ case "S":
314
+ case "Q":
315
+ relArgs[0] -= cx;
316
+ relArgs[1] -= cy;
317
+ relArgs[2] -= cx;
318
+ relArgs[3] -= cy;
319
+ break;
320
+ case "A":
321
+ relArgs[5] -= cx;
322
+ relArgs[6] -= cy;
323
+ break;
251
324
  }
252
325
 
253
326
  return { command: relCmd, args: relArgs };
@@ -258,17 +331,21 @@ export function toRelative(cmd, cx, cy) {
258
331
  */
259
332
  export function lineToHV(cmd, cx, cy, tolerance = 1e-6) {
260
333
  const { command, args } = cmd;
261
- if (command !== 'L' && command !== 'l') return null;
334
+ if (command !== "L" && command !== "l") return null;
262
335
 
263
- const isAbs = command === 'L';
336
+ const isAbs = command === "L";
264
337
  const endX = isAbs ? args[0] : cx + args[0];
265
338
  const endY = isAbs ? args[1] : cy + args[1];
266
339
 
267
340
  if (Math.abs(endY - cy) < tolerance) {
268
- return isAbs ? { command: 'H', args: [endX] } : { command: 'h', args: [endX - cx] };
341
+ return isAbs
342
+ ? { command: "H", args: [endX] }
343
+ : { command: "h", args: [endX - cx] };
269
344
  }
270
345
  if (Math.abs(endX - cx) < tolerance) {
271
- return isAbs ? { command: 'V', args: [endY] } : { command: 'v', args: [endY - cy] };
346
+ return isAbs
347
+ ? { command: "V", args: [endY] }
348
+ : { command: "v", args: [endY - cy] };
272
349
  }
273
350
  return null;
274
351
  }
@@ -278,19 +355,31 @@ export function lineToHV(cmd, cx, cy, tolerance = 1e-6) {
278
355
  */
279
356
  export function lineToZ(cmd, cx, cy, startX, startY, tolerance = 1e-6) {
280
357
  const { command, args } = cmd;
281
- if (command !== 'L' && command !== 'l') return false;
358
+ if (command !== "L" && command !== "l") return false;
282
359
 
283
- const isAbs = command === 'L';
360
+ const isAbs = command === "L";
284
361
  const endX = isAbs ? args[0] : cx + args[0];
285
362
  const endY = isAbs ? args[1] : cy + args[1];
286
363
 
287
- return Math.abs(endX - startX) < tolerance && Math.abs(endY - startY) < tolerance;
364
+ return (
365
+ Math.abs(endX - startX) < tolerance && Math.abs(endY - startY) < tolerance
366
+ );
288
367
  }
289
368
 
290
369
  /**
291
370
  * Check if a cubic bezier is effectively a straight line.
292
371
  */
293
- export function isCurveStraight(x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3, tolerance = 0.5) {
372
+ export function isCurveStraight(
373
+ x0,
374
+ y0,
375
+ cp1x,
376
+ cp1y,
377
+ cp2x,
378
+ cp2y,
379
+ x3,
380
+ y3,
381
+ tolerance = 0.5,
382
+ ) {
294
383
  const chordLengthSq = (x3 - x0) ** 2 + (y3 - y0) ** 2;
295
384
 
296
385
  if (chordLengthSq < 1e-10) {
@@ -300,8 +389,12 @@ export function isCurveStraight(x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3, toleranc
300
389
  }
301
390
 
302
391
  const chordLength = Math.sqrt(chordLengthSq);
303
- const d1 = Math.abs((y3 - y0) * cp1x - (x3 - x0) * cp1y + x3 * y0 - y3 * x0) / chordLength;
304
- const d2 = Math.abs((y3 - y0) * cp2x - (x3 - x0) * cp2y + x3 * y0 - y3 * x0) / chordLength;
392
+ const d1 =
393
+ Math.abs((y3 - y0) * cp1x - (x3 - x0) * cp1y + x3 * y0 - y3 * x0) /
394
+ chordLength;
395
+ const d2 =
396
+ Math.abs((y3 - y0) * cp2x - (x3 - x0) * cp2y + x3 * y0 - y3 * x0) /
397
+ chordLength;
305
398
 
306
399
  return Math.max(d1, d2) < tolerance;
307
400
  }
@@ -311,9 +404,9 @@ export function isCurveStraight(x0, y0, cp1x, cp1y, cp2x, cp2y, x3, y3, toleranc
311
404
  */
312
405
  export function straightCurveToLine(cmd, cx, cy, tolerance = 0.5) {
313
406
  const { command, args } = cmd;
314
- if (command !== 'C' && command !== 'c') return null;
407
+ if (command !== "C" && command !== "c") return null;
315
408
 
316
- const isAbs = command === 'C';
409
+ const isAbs = command === "C";
317
410
  const cp1x = isAbs ? args[0] : cx + args[0];
318
411
  const cp1y = isAbs ? args[1] : cy + args[1];
319
412
  const cp2x = isAbs ? args[2] : cx + args[2];
@@ -323,8 +416,8 @@ export function straightCurveToLine(cmd, cx, cy, tolerance = 0.5) {
323
416
 
324
417
  if (isCurveStraight(cx, cy, cp1x, cp1y, cp2x, cp2y, endX, endY, tolerance)) {
325
418
  return isAbs
326
- ? { command: 'L', args: [endX, endY] }
327
- : { command: 'l', args: [endX - cx, endY - cy] };
419
+ ? { command: "L", args: [endX, endY] }
420
+ : { command: "l", args: [endX - cx, endY - cy] };
328
421
  }
329
422
  return null;
330
423
  }
@@ -337,87 +430,93 @@ export function straightCurveToLine(cmd, cx, cy, tolerance = 0.5) {
337
430
  * @param {boolean} prevLastArgHadDecimal - Whether previous command's last arg had a decimal point
338
431
  * @returns {{str: string, lastArgHadDecimal: boolean}} Serialized string and decimal info for next command
339
432
  */
340
- export function serializeCommand(cmd, prevCommand, precision = 3, prevLastArgHadDecimal = false) {
433
+ export function serializeCommand(
434
+ cmd,
435
+ prevCommand,
436
+ precision = 3,
437
+ prevLastArgHadDecimal = false,
438
+ ) {
341
439
  const { command, args } = cmd;
342
440
 
343
- if (command === 'Z' || command === 'z') {
344
- return { str: 'z', lastArgHadDecimal: false };
441
+ if (command === "Z" || command === "z") {
442
+ return { str: "z", lastArgHadDecimal: false };
345
443
  }
346
444
 
347
445
  // SPECIAL HANDLING FOR ARC COMMANDS
348
446
  // Arc format: rx ry rotation large-arc-flag sweep-flag x y
349
447
  // Per SVG spec: flags MUST be exactly 0 or 1, arc commands CANNOT be implicitly repeated
350
- if (command === 'A' || command === 'a') {
448
+ if (command === "A" || command === "a") {
351
449
  const arcArgs = [
352
- formatNumber(args[0], precision), // rx
353
- formatNumber(args[1], precision), // ry
354
- formatNumber(args[2], precision), // rotation
355
- args[3] ? '1' : '0', // large-arc-flag (FORCE 0/1)
356
- args[4] ? '1' : '0', // sweep-flag (FORCE 0/1)
357
- formatNumber(args[5], precision), // x
358
- formatNumber(args[6], precision) // y
359
- ].join(' '); // ALWAYS use space delimiters for arcs to avoid invalid double-decimals
450
+ formatNumber(args[0], precision), // rx
451
+ formatNumber(args[1], precision), // ry
452
+ formatNumber(args[2], precision), // rotation
453
+ args[3] ? "1" : "0", // large-arc-flag (FORCE 0/1)
454
+ args[4] ? "1" : "0", // sweep-flag (FORCE 0/1)
455
+ formatNumber(args[5], precision), // x
456
+ formatNumber(args[6], precision), // y
457
+ ].join(" "); // ALWAYS use space delimiters for arcs to avoid invalid double-decimals
360
458
 
361
459
  // Arc commands CANNOT be implicitly repeated - always include command letter
362
460
  return {
363
461
  str: command + arcArgs,
364
- lastArgHadDecimal: arcArgs.includes('.')
462
+ lastArgHadDecimal: arcArgs.includes("."),
365
463
  };
366
464
  }
367
465
 
368
- const formattedArgs = args.map(n => formatNumber(n, precision));
466
+ const formattedArgs = args.map((n) => formatNumber(n, precision));
369
467
 
370
- let argsStr = '';
468
+ let argsStr = "";
371
469
  for (let i = 0; i < formattedArgs.length; i++) {
372
470
  const arg = formattedArgs[i];
373
471
  if (i === 0) {
374
472
  argsStr = arg;
375
473
  } else {
376
474
  const prevArg = formattedArgs[i - 1];
377
- const prevHasDecimal = prevArg.includes('.');
475
+ const prevHasDecimal = prevArg.includes(".");
378
476
 
379
- if (arg.startsWith('-')) {
477
+ if (arg.startsWith("-")) {
380
478
  // Negative sign is always a valid delimiter
381
479
  argsStr += arg;
382
- } else if (arg.startsWith('.') && prevHasDecimal) {
480
+ } else if (arg.startsWith(".") && prevHasDecimal) {
383
481
  // Decimal point works as delimiter only if prev already has a decimal
384
482
  // e.g., "-2.5" + ".3" = "-2.5.3" parses as -2.5 and .3 (number can't have two decimals)
385
483
  // but "0" + ".3" = "0.3" would merge into single number 0.3!
386
484
  argsStr += arg;
387
485
  } else {
388
486
  // Need space delimiter
389
- argsStr += ' ' + arg;
487
+ argsStr += " " + arg;
390
488
  }
391
489
  }
392
490
  }
393
491
 
394
492
  // Track whether our last arg has a decimal (for next command's delimiter decision)
395
- const lastArg = formattedArgs[formattedArgs.length - 1] || '';
396
- const thisLastArgHadDecimal = lastArg.includes('.');
493
+ const lastArg = formattedArgs[formattedArgs.length - 1] || "";
494
+ const thisLastArgHadDecimal = lastArg.includes(".");
397
495
 
398
496
  // Decide if we need command letter or can use implicit continuation
399
497
  // Arc commands CANNOT be implicitly repeated per SVG spec
400
498
  let cmdStr = command;
401
- if (prevCommand === 'M' && command === 'L') cmdStr = '';
402
- else if (prevCommand === 'm' && command === 'l') cmdStr = '';
403
- else if (prevCommand === command && command !== 'A' && command !== 'a') cmdStr = '';
499
+ if (prevCommand === "M" && command === "L") cmdStr = "";
500
+ else if (prevCommand === "m" && command === "l") cmdStr = "";
501
+ else if (prevCommand === command && command !== "A" && command !== "a")
502
+ cmdStr = "";
404
503
 
405
504
  if (cmdStr) {
406
505
  // Explicit command letter - always safe, no delimiter needed
407
506
  return { str: cmdStr + argsStr, lastArgHadDecimal: thisLastArgHadDecimal };
408
507
  } else {
409
508
  // Implicit command - need to check delimiter between commands
410
- const firstArg = formattedArgs[0] || '';
509
+ const firstArg = formattedArgs[0] || "";
411
510
 
412
- if (firstArg.startsWith('-')) {
511
+ if (firstArg.startsWith("-")) {
413
512
  // Negative sign always works as delimiter
414
513
  return { str: argsStr, lastArgHadDecimal: thisLastArgHadDecimal };
415
- } else if (firstArg.startsWith('.') && prevLastArgHadDecimal) {
514
+ } else if (firstArg.startsWith(".") && prevLastArgHadDecimal) {
416
515
  // Decimal point works only if prev command's last arg already had a decimal
417
516
  return { str: argsStr, lastArgHadDecimal: thisLastArgHadDecimal };
418
517
  } else {
419
518
  // Need space delimiter
420
- return { str: ' ' + argsStr, lastArgHadDecimal: thisLastArgHadDecimal };
519
+ return { str: " " + argsStr, lastArgHadDecimal: thisLastArgHadDecimal };
421
520
  }
422
521
  }
423
522
  }
@@ -426,14 +525,19 @@ export function serializeCommand(cmd, prevCommand, precision = 3, prevLastArgHad
426
525
  * Serialize a complete path to minimal string.
427
526
  */
428
527
  export function serializePath(commands, precision = 3) {
429
- if (commands.length === 0) return '';
528
+ if (commands.length === 0) return "";
430
529
 
431
- let result = '';
530
+ let result = "";
432
531
  let prevCommand = null;
433
532
  let prevLastArgHadDecimal = false;
434
533
 
435
534
  for (const cmd of commands) {
436
- const { str, lastArgHadDecimal } = serializeCommand(cmd, prevCommand, precision, prevLastArgHadDecimal);
535
+ const { str, lastArgHadDecimal } = serializeCommand(
536
+ cmd,
537
+ prevCommand,
538
+ precision,
539
+ prevLastArgHadDecimal,
540
+ );
437
541
  result += str;
438
542
  prevCommand = cmd.command;
439
543
  prevLastArgHadDecimal = lastArgHadDecimal;
@@ -455,17 +559,20 @@ export function convertPathData(d, options = {}) {
455
559
  lineShorthands = true,
456
560
  convertToZ = true,
457
561
  utilizeAbsolute = true,
458
- straightTolerance = 0.5
562
+ straightTolerance = 0.5,
459
563
  } = options;
460
564
 
461
565
  const originalLength = d.length;
462
- let commands = parsePath(d);
566
+ const commands = parsePath(d);
463
567
 
464
568
  if (commands.length === 0) {
465
569
  return { d, originalLength, optimizedLength: originalLength, savings: 0 };
466
570
  }
467
571
 
468
- let cx = 0, cy = 0, startX = 0, startY = 0;
572
+ let cx = 0,
573
+ cy = 0,
574
+ startX = 0,
575
+ startY = 0;
469
576
  const optimized = [];
470
577
 
471
578
  for (let i = 0; i < commands.length; i++) {
@@ -473,36 +580,39 @@ export function convertPathData(d, options = {}) {
473
580
 
474
581
  // BUG FIX #3: Convert zero arc radii to line per SVG spec Section 8.3.4
475
582
  // When rx or ry is 0, the arc degenerates to a straight line
476
- if ((cmd.command === 'A' || cmd.command === 'a') && (cmd.args[0] === 0 || cmd.args[1] === 0)) {
477
- const isAbs = cmd.command === 'A';
583
+ if (
584
+ (cmd.command === "A" || cmd.command === "a") &&
585
+ (cmd.args[0] === 0 || cmd.args[1] === 0)
586
+ ) {
587
+ const isAbs = cmd.command === "A";
478
588
  const endX = cmd.args[5];
479
589
  const endY = cmd.args[6];
480
590
  cmd = isAbs
481
- ? { command: 'L', args: [endX, endY] }
482
- : { command: 'l', args: [endX, endY] };
591
+ ? { command: "L", args: [endX, endY] }
592
+ : { command: "l", args: [endX, endY] };
483
593
  }
484
594
 
485
595
  // 1. Straight curve to line
486
- if (straightCurves && (cmd.command === 'C' || cmd.command === 'c')) {
596
+ if (straightCurves && (cmd.command === "C" || cmd.command === "c")) {
487
597
  const lineCmd = straightCurveToLine(cmd, cx, cy, straightTolerance);
488
598
  if (lineCmd) cmd = lineCmd;
489
599
  }
490
600
 
491
601
  // 2. Line shorthands (L -> H/V)
492
- if (lineShorthands && (cmd.command === 'L' || cmd.command === 'l')) {
602
+ if (lineShorthands && (cmd.command === "L" || cmd.command === "l")) {
493
603
  const hvCmd = lineToHV(cmd, cx, cy);
494
604
  if (hvCmd) cmd = hvCmd;
495
605
  }
496
606
 
497
607
  // 3. Line to Z
498
- if (convertToZ && (cmd.command === 'L' || cmd.command === 'l')) {
608
+ if (convertToZ && (cmd.command === "L" || cmd.command === "l")) {
499
609
  if (lineToZ(cmd, cx, cy, startX, startY)) {
500
- cmd = { command: 'z', args: [] };
610
+ cmd = { command: "z", args: [] };
501
611
  }
502
612
  }
503
613
 
504
614
  // 4. Choose shorter form (absolute vs relative)
505
- if (utilizeAbsolute && cmd.command !== 'Z' && cmd.command !== 'z') {
615
+ if (utilizeAbsolute && cmd.command !== "Z" && cmd.command !== "z") {
506
616
  const abs = toAbsolute(cmd, cx, cy);
507
617
  const rel = toRelative(cmd, cx, cy);
508
618
  // Bug fix: serializeCommand returns {str, lastArgHadDecimal} object,
@@ -517,14 +627,40 @@ export function convertPathData(d, options = {}) {
517
627
  // Update position
518
628
  const finalCmd = toAbsolute(cmd, cx, cy);
519
629
  switch (finalCmd.command) {
520
- case 'M': cx = finalCmd.args[0]; cy = finalCmd.args[1]; startX = cx; startY = cy; break;
521
- case 'L': case 'T': cx = finalCmd.args[0]; cy = finalCmd.args[1]; break;
522
- case 'H': cx = finalCmd.args[0]; break;
523
- case 'V': cy = finalCmd.args[0]; break;
524
- case 'C': cx = finalCmd.args[4]; cy = finalCmd.args[5]; break;
525
- case 'S': case 'Q': cx = finalCmd.args[2]; cy = finalCmd.args[3]; break;
526
- case 'A': cx = finalCmd.args[5]; cy = finalCmd.args[6]; break;
527
- case 'Z': cx = startX; cy = startY; break;
630
+ case "M":
631
+ cx = finalCmd.args[0];
632
+ cy = finalCmd.args[1];
633
+ startX = cx;
634
+ startY = cy;
635
+ break;
636
+ case "L":
637
+ case "T":
638
+ cx = finalCmd.args[0];
639
+ cy = finalCmd.args[1];
640
+ break;
641
+ case "H":
642
+ cx = finalCmd.args[0];
643
+ break;
644
+ case "V":
645
+ cy = finalCmd.args[0];
646
+ break;
647
+ case "C":
648
+ cx = finalCmd.args[4];
649
+ cy = finalCmd.args[5];
650
+ break;
651
+ case "S":
652
+ case "Q":
653
+ cx = finalCmd.args[2];
654
+ cy = finalCmd.args[3];
655
+ break;
656
+ case "A":
657
+ cx = finalCmd.args[5];
658
+ cy = finalCmd.args[6];
659
+ break;
660
+ case "Z":
661
+ cx = startX;
662
+ cy = startY;
663
+ break;
528
664
  }
529
665
  }
530
666
 
@@ -534,7 +670,7 @@ export function convertPathData(d, options = {}) {
534
670
  d: optimizedD,
535
671
  originalLength,
536
672
  optimizedLength: optimizedD.length,
537
- savings: originalLength - optimizedD.length
673
+ savings: originalLength - optimizedD.length,
538
674
  };
539
675
  }
540
676
 
@@ -549,19 +685,19 @@ export function optimizeDocumentPaths(root, options = {}) {
549
685
  const processElement = (el) => {
550
686
  const tagName = el.tagName?.toLowerCase();
551
687
 
552
- if (tagName === 'path') {
553
- const d = el.getAttribute('d');
688
+ if (tagName === "path") {
689
+ const d = el.getAttribute("d");
554
690
  if (d) {
555
691
  const result = convertPathData(d, options);
556
692
  if (result.savings > 0) {
557
- el.setAttribute('d', result.d);
693
+ el.setAttribute("d", result.d);
558
694
  pathsOptimized++;
559
695
  totalSavings += result.savings;
560
696
  details.push({
561
- id: el.getAttribute('id') || null,
697
+ id: el.getAttribute("id") || null,
562
698
  originalLength: result.originalLength,
563
699
  optimizedLength: result.optimizedLength,
564
- savings: result.savings
700
+ savings: result.savings,
565
701
  });
566
702
  }
567
703
  }
@@ -577,7 +713,16 @@ export function optimizeDocumentPaths(root, options = {}) {
577
713
  }
578
714
 
579
715
  export default {
580
- parsePath, formatNumber, toAbsolute, toRelative,
581
- lineToHV, lineToZ, isCurveStraight, straightCurveToLine,
582
- serializeCommand, serializePath, convertPathData, optimizeDocumentPaths
716
+ parsePath,
717
+ formatNumber,
718
+ toAbsolute,
719
+ toRelative,
720
+ lineToHV,
721
+ lineToZ,
722
+ isCurveStraight,
723
+ straightCurveToLine,
724
+ serializeCommand,
725
+ serializePath,
726
+ convertPathData,
727
+ optimizeDocumentPaths,
583
728
  };