svger-cli 4.0.2 → 4.0.3

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.
@@ -37,16 +37,55 @@ const COMMAND_PARAMS = {
37
37
  z: 0, // no params
38
38
  };
39
39
  /**
40
- * Check if character is a path command letter
40
+ * Check if character is a path command letter - O(1) Set lookup
41
41
  */
42
+ const COMMAND_LETTERS = new Set([
43
+ 'M',
44
+ 'm',
45
+ 'L',
46
+ 'l',
47
+ 'H',
48
+ 'h',
49
+ 'V',
50
+ 'v',
51
+ 'C',
52
+ 'c',
53
+ 'S',
54
+ 's',
55
+ 'Q',
56
+ 'q',
57
+ 'T',
58
+ 't',
59
+ 'A',
60
+ 'a',
61
+ 'Z',
62
+ 'z',
63
+ ]);
42
64
  function isCommandLetter(char) {
43
- return /[MmLlHhVvCcSsQqTtAaZz]/.test(char);
65
+ return COMMAND_LETTERS.has(char);
44
66
  }
45
67
  /**
46
- * Check if character is numeric (digit, decimal, sign, exponent)
68
+ * Check if character is numeric (digit, decimal, sign, exponent) - O(1) Set lookup
47
69
  */
70
+ const NUMERIC_CHARS = new Set([
71
+ '0',
72
+ '1',
73
+ '2',
74
+ '3',
75
+ '4',
76
+ '5',
77
+ '6',
78
+ '7',
79
+ '8',
80
+ '9',
81
+ '.',
82
+ 'e',
83
+ 'E',
84
+ '+',
85
+ '-',
86
+ ]);
48
87
  function isNumericChar(char) {
49
- return /[\d.eE+-]/.test(char);
88
+ return NUMERIC_CHARS.has(char);
50
89
  }
51
90
  /**
52
91
  * Tokenize path string into array of tokens (commands and numbers)
@@ -158,67 +197,87 @@ export function parsePath(pathData) {
158
197
  // Skip incomplete command
159
198
  continue;
160
199
  }
161
- // Update current position
200
+ // Update current position using O(1) object lookup instead of switch
162
201
  const isUpperCase = currentCommand === currentCommand.toUpperCase();
163
202
  if (isUpperCase) {
164
- // Absolute commands
165
- switch (currentCommand) {
166
- case 'M':
167
- case 'L':
168
- case 'T':
169
- currentX = values[values.length - 2];
170
- currentY = values[values.length - 1];
171
- break;
172
- case 'H':
173
- currentX = values[values.length - 1];
174
- break;
175
- case 'V':
176
- currentY = values[values.length - 1];
177
- break;
178
- case 'C':
179
- currentX = values[values.length - 2];
180
- currentY = values[values.length - 1];
181
- break;
182
- case 'S':
183
- case 'Q':
184
- currentX = values[values.length - 2];
185
- currentY = values[values.length - 1];
186
- break;
187
- case 'A':
188
- currentX = values[values.length - 2];
189
- currentY = values[values.length - 1];
190
- break;
191
- }
203
+ // Absolute position update handlers
204
+ const absoluteHandlers = {
205
+ M: v => {
206
+ currentX = v[v.length - 2];
207
+ currentY = v[v.length - 1];
208
+ },
209
+ L: v => {
210
+ currentX = v[v.length - 2];
211
+ currentY = v[v.length - 1];
212
+ },
213
+ T: v => {
214
+ currentX = v[v.length - 2];
215
+ currentY = v[v.length - 1];
216
+ },
217
+ H: v => {
218
+ currentX = v[v.length - 1];
219
+ },
220
+ V: v => {
221
+ currentY = v[v.length - 1];
222
+ },
223
+ C: v => {
224
+ currentX = v[v.length - 2];
225
+ currentY = v[v.length - 1];
226
+ },
227
+ S: v => {
228
+ currentX = v[v.length - 2];
229
+ currentY = v[v.length - 1];
230
+ },
231
+ Q: v => {
232
+ currentX = v[v.length - 2];
233
+ currentY = v[v.length - 1];
234
+ },
235
+ A: v => {
236
+ currentX = v[v.length - 2];
237
+ currentY = v[v.length - 1];
238
+ },
239
+ };
240
+ absoluteHandlers[currentCommand]?.(values);
192
241
  }
193
242
  else {
194
- // Relative commands
195
- switch (currentCommand) {
196
- case 'm':
197
- case 'l':
198
- case 't':
199
- currentX += values[values.length - 2];
200
- currentY += values[values.length - 1];
201
- break;
202
- case 'h':
203
- currentX += values[values.length - 1];
204
- break;
205
- case 'v':
206
- currentY += values[values.length - 1];
207
- break;
208
- case 'c':
209
- currentX += values[values.length - 2];
210
- currentY += values[values.length - 1];
211
- break;
212
- case 's':
213
- case 'q':
214
- currentX += values[values.length - 2];
215
- currentY += values[values.length - 1];
216
- break;
217
- case 'a':
218
- currentX += values[values.length - 2];
219
- currentY += values[values.length - 1];
220
- break;
221
- }
243
+ // Relative position update handlers
244
+ const relativeHandlers = {
245
+ m: v => {
246
+ currentX += v[v.length - 2];
247
+ currentY += v[v.length - 1];
248
+ },
249
+ l: v => {
250
+ currentX += v[v.length - 2];
251
+ currentY += v[v.length - 1];
252
+ },
253
+ t: v => {
254
+ currentX += v[v.length - 2];
255
+ currentY += v[v.length - 1];
256
+ },
257
+ h: v => {
258
+ currentX += v[v.length - 1];
259
+ },
260
+ v: v => {
261
+ currentY += v[v.length - 1];
262
+ },
263
+ c: v => {
264
+ currentX += v[v.length - 2];
265
+ currentY += v[v.length - 1];
266
+ },
267
+ s: v => {
268
+ currentX += v[v.length - 2];
269
+ currentY += v[v.length - 1];
270
+ },
271
+ q: v => {
272
+ currentX += v[v.length - 2];
273
+ currentY += v[v.length - 1];
274
+ },
275
+ a: v => {
276
+ currentX += v[v.length - 2];
277
+ currentY += v[v.length - 1];
278
+ },
279
+ };
280
+ relativeHandlers[currentCommand]?.(values);
222
281
  }
223
282
  // Store command with absolute position
224
283
  commands.push({
@@ -248,33 +307,40 @@ export function toAbsolute(cmd, prevX = 0, prevY = 0) {
248
307
  if (type === type.toUpperCase()) {
249
308
  return cmd;
250
309
  }
251
- // Convert relative to absolute
310
+ // Convert relative to absolute using O(1) lookup
252
311
  const absoluteType = type.toUpperCase();
253
312
  const values = [...cmd.values];
254
- switch (type) {
255
- case 'm':
256
- case 'l':
257
- case 't':
258
- // x y → x+prevX y+prevY
313
+ // Transformation strategies keyed by command letter
314
+ const transformers = {
315
+ m: () => {
259
316
  for (let i = 0; i < values.length; i += 2) {
260
317
  values[i] += prevX;
261
318
  values[i + 1] += prevY;
262
319
  }
263
- break;
264
- case 'h':
265
- // x x+prevX
320
+ },
321
+ l: () => {
322
+ for (let i = 0; i < values.length; i += 2) {
323
+ values[i] += prevX;
324
+ values[i + 1] += prevY;
325
+ }
326
+ },
327
+ t: () => {
328
+ for (let i = 0; i < values.length; i += 2) {
329
+ values[i] += prevX;
330
+ values[i + 1] += prevY;
331
+ }
332
+ },
333
+ h: () => {
266
334
  for (let i = 0; i < values.length; i++) {
267
335
  values[i] += prevX;
268
336
  }
269
- break;
270
- case 'v':
271
- // y → y+prevY
337
+ },
338
+ v: () => {
272
339
  for (let i = 0; i < values.length; i++) {
273
340
  values[i] += prevY;
274
341
  }
275
- break;
276
- case 'c':
277
- // x1 y1 x2 y2 x y → all +prev
342
+ },
343
+ c: () => {
278
344
  for (let i = 0; i < values.length; i += 6) {
279
345
  values[i] += prevX;
280
346
  values[i + 1] += prevY;
@@ -283,28 +349,34 @@ export function toAbsolute(cmd, prevX = 0, prevY = 0) {
283
349
  values[i + 4] += prevX;
284
350
  values[i + 5] += prevY;
285
351
  }
286
- break;
287
- case 's':
288
- case 'q':
289
- // x1 y1 x y → all +prev
352
+ },
353
+ s: () => {
354
+ for (let i = 0; i < values.length; i += 4) {
355
+ values[i] += prevX;
356
+ values[i + 1] += prevY;
357
+ values[i + 2] += prevX;
358
+ values[i + 3] += prevY;
359
+ }
360
+ },
361
+ q: () => {
290
362
  for (let i = 0; i < values.length; i += 4) {
291
363
  values[i] += prevX;
292
364
  values[i + 1] += prevY;
293
365
  values[i + 2] += prevX;
294
366
  values[i + 3] += prevY;
295
367
  }
296
- break;
297
- case 'a':
298
- // rx ry rotation large-arc sweep x y → only x,y +prev
368
+ },
369
+ a: () => {
299
370
  for (let i = 0; i < values.length; i += 7) {
300
371
  values[i + 5] += prevX;
301
372
  values[i + 6] += prevY;
302
373
  }
303
- break;
304
- case 'z':
305
- // No conversion needed
306
- break;
307
- }
374
+ },
375
+ z: () => {
376
+ /* No conversion needed */
377
+ },
378
+ };
379
+ transformers[type]?.();
308
380
  return {
309
381
  type: absoluteType,
310
382
  values,
@@ -320,33 +392,39 @@ export function toRelative(cmd, prevX = 0, prevY = 0) {
320
392
  if (type === type.toLowerCase()) {
321
393
  return cmd;
322
394
  }
323
- // Convert absolute to relative
395
+ // Convert absolute to relative using O(1) lookup
324
396
  const relativeType = type.toLowerCase();
325
397
  const values = [...cmd.values];
326
- switch (type) {
327
- case 'M':
328
- case 'L':
329
- case 'T':
330
- // x y x-prevX y-prevY
398
+ const transformers = {
399
+ M: () => {
400
+ for (let i = 0; i < values.length; i += 2) {
401
+ values[i] -= prevX;
402
+ values[i + 1] -= prevY;
403
+ }
404
+ },
405
+ L: () => {
406
+ for (let i = 0; i < values.length; i += 2) {
407
+ values[i] -= prevX;
408
+ values[i + 1] -= prevY;
409
+ }
410
+ },
411
+ T: () => {
331
412
  for (let i = 0; i < values.length; i += 2) {
332
413
  values[i] -= prevX;
333
414
  values[i + 1] -= prevY;
334
415
  }
335
- break;
336
- case 'H':
337
- // x → x-prevX
416
+ },
417
+ H: () => {
338
418
  for (let i = 0; i < values.length; i++) {
339
419
  values[i] -= prevX;
340
420
  }
341
- break;
342
- case 'V':
343
- // y → y-prevY
421
+ },
422
+ V: () => {
344
423
  for (let i = 0; i < values.length; i++) {
345
424
  values[i] -= prevY;
346
425
  }
347
- break;
348
- case 'C':
349
- // x1 y1 x2 y2 x y → all -prev
426
+ },
427
+ C: () => {
350
428
  for (let i = 0; i < values.length; i += 6) {
351
429
  values[i] -= prevX;
352
430
  values[i + 1] -= prevY;
@@ -355,28 +433,34 @@ export function toRelative(cmd, prevX = 0, prevY = 0) {
355
433
  values[i + 4] -= prevX;
356
434
  values[i + 5] -= prevY;
357
435
  }
358
- break;
359
- case 'S':
360
- case 'Q':
361
- // x1 y1 x y → all -prev
436
+ },
437
+ S: () => {
362
438
  for (let i = 0; i < values.length; i += 4) {
363
439
  values[i] -= prevX;
364
440
  values[i + 1] -= prevY;
365
441
  values[i + 2] -= prevX;
366
442
  values[i + 3] -= prevY;
367
443
  }
368
- break;
369
- case 'A':
370
- // rx ry rotation large-arc sweep x y only x,y -prev
444
+ },
445
+ Q: () => {
446
+ for (let i = 0; i < values.length; i += 4) {
447
+ values[i] -= prevX;
448
+ values[i + 1] -= prevY;
449
+ values[i + 2] -= prevX;
450
+ values[i + 3] -= prevY;
451
+ }
452
+ },
453
+ A: () => {
371
454
  for (let i = 0; i < values.length; i += 7) {
372
455
  values[i + 5] -= prevX;
373
456
  values[i + 6] -= prevY;
374
457
  }
375
- break;
376
- case 'Z':
377
- // No conversion needed
378
- break;
379
- }
458
+ },
459
+ Z: () => {
460
+ /* No conversion needed */
461
+ },
462
+ };
463
+ transformers[type]?.();
380
464
  return {
381
465
  type: relativeType,
382
466
  values,
@@ -179,45 +179,48 @@ function extractLinearPoints(commands, startX, startY) {
179
179
  for (let i = 0; i < commands.length; i++) {
180
180
  const cmd = commands[i];
181
181
  const isRelative = cmd.type === cmd.type.toLowerCase();
182
- switch (cmd.type.toUpperCase()) {
183
- case 'L': {
182
+ const upperType = cmd.type.toUpperCase();
183
+ // O(1) object lookup for command type → point extraction
184
+ const pointExtractors = {
185
+ L: () => {
184
186
  const x = isRelative ? currentX + cmd.values[0] : cmd.values[0];
185
187
  const y = isRelative ? currentY + cmd.values[1] : cmd.values[1];
186
188
  points.push({ x, y, cmdIndex: i + 1 });
187
189
  indices.push(i + 1);
188
190
  currentX = x;
189
191
  currentY = y;
190
- break;
191
- }
192
- case 'H': {
192
+ },
193
+ H: () => {
193
194
  const x = isRelative ? currentX + cmd.values[0] : cmd.values[0];
194
195
  points.push({ x, y: currentY, cmdIndex: i + 1 });
195
196
  indices.push(i + 1);
196
197
  currentX = x;
197
- break;
198
- }
199
- case 'V': {
198
+ },
199
+ V: () => {
200
200
  const y = isRelative ? currentY + cmd.values[0] : cmd.values[0];
201
201
  points.push({ x: currentX, y, cmdIndex: i + 1 });
202
202
  indices.push(i + 1);
203
203
  currentY = y;
204
- break;
204
+ },
205
+ };
206
+ const extractor = pointExtractors[upperType];
207
+ if (extractor) {
208
+ extractor();
209
+ }
210
+ else {
211
+ // Update position for other commands but don't add to points
212
+ if (upperType === 'M') {
213
+ currentX = isRelative ? currentX + cmd.values[0] : cmd.values[0];
214
+ currentY = isRelative ? currentY + cmd.values[1] : cmd.values[1];
215
+ }
216
+ else if (upperType === 'C') {
217
+ currentX = isRelative ? currentX + cmd.values[4] : cmd.values[4];
218
+ currentY = isRelative ? currentY + cmd.values[5] : cmd.values[5];
219
+ }
220
+ else if (upperType === 'Q') {
221
+ currentX = isRelative ? currentX + cmd.values[2] : cmd.values[2];
222
+ currentY = isRelative ? currentY + cmd.values[3] : cmd.values[3];
205
223
  }
206
- default:
207
- // Update position for other commands but don't add to points
208
- if (cmd.type.toUpperCase() === 'M') {
209
- currentX = isRelative ? currentX + cmd.values[0] : cmd.values[0];
210
- currentY = isRelative ? currentY + cmd.values[1] : cmd.values[1];
211
- }
212
- else if (cmd.type.toUpperCase() === 'C') {
213
- currentX = isRelative ? currentX + cmd.values[4] : cmd.values[4];
214
- currentY = isRelative ? currentY + cmd.values[5] : cmd.values[5];
215
- }
216
- else if (cmd.type.toUpperCase() === 'Q') {
217
- currentX = isRelative ? currentX + cmd.values[2] : cmd.values[2];
218
- currentY = isRelative ? currentY + cmd.values[3] : cmd.values[3];
219
- }
220
- break;
221
224
  }
222
225
  }
223
226
  return { points, indices };
@@ -144,8 +144,9 @@ function shouldConvert(element, pathData, threshold) {
144
144
  function convertShapeToPath(node, threshold) {
145
145
  const tag = node.tag;
146
146
  let pathData = null;
147
- switch (tag) {
148
- case 'rect': {
147
+ // O(1) object lookup for shape tag → path converter
148
+ const shapeConverters = {
149
+ rect: () => {
149
150
  const x = parseFloat(node.attrs.get('x') || '0');
150
151
  const y = parseFloat(node.attrs.get('y') || '0');
151
152
  const width = parseFloat(node.attrs.get('width') || '0');
@@ -156,41 +157,35 @@ function convertShapeToPath(node, threshold) {
156
157
  const ry = node.attrs.has('ry')
157
158
  ? parseFloat(node.attrs.get('ry'))
158
159
  : undefined;
159
- pathData = rectToPath(x, y, width, height, rx, ry);
160
- break;
161
- }
162
- case 'circle': {
160
+ return rectToPath(x, y, width, height, rx, ry);
161
+ },
162
+ circle: () => {
163
163
  const cx = parseFloat(node.attrs.get('cx') || '0');
164
164
  const cy = parseFloat(node.attrs.get('cy') || '0');
165
165
  const r = parseFloat(node.attrs.get('r') || '0');
166
- pathData = circleToPath(cx, cy, r);
167
- break;
168
- }
169
- case 'ellipse': {
166
+ return circleToPath(cx, cy, r);
167
+ },
168
+ ellipse: () => {
170
169
  const cx = parseFloat(node.attrs.get('cx') || '0');
171
170
  const cy = parseFloat(node.attrs.get('cy') || '0');
172
171
  const rx = parseFloat(node.attrs.get('rx') || '0');
173
172
  const ry = parseFloat(node.attrs.get('ry') || '0');
174
- pathData = ellipseToPath(cx, cy, rx, ry);
175
- break;
176
- }
177
- case 'polygon': {
173
+ return ellipseToPath(cx, cy, rx, ry);
174
+ },
175
+ polygon: () => {
178
176
  const points = node.attrs.get('points');
179
- if (points) {
180
- pathData = polygonToPath(points);
181
- }
182
- break;
183
- }
184
- case 'polyline': {
177
+ return points ? polygonToPath(points) : null;
178
+ },
179
+ polyline: () => {
185
180
  const points = node.attrs.get('points');
186
- if (points) {
187
- pathData = polylineToPath(points);
188
- }
189
- break;
190
- }
191
- default:
192
- return { converted: false, savings: 0 };
181
+ return points ? polylineToPath(points) : null;
182
+ },
183
+ };
184
+ const converter = shapeConverters[tag];
185
+ if (!converter) {
186
+ return { converted: false, savings: 0 };
193
187
  }
188
+ pathData = converter();
194
189
  // Check if conversion was possible
195
190
  if (!pathData) {
196
191
  return { converted: false, savings: 0 };
@@ -220,21 +220,17 @@ function applyTransformToPath(node, matrix) {
220
220
  * Apply transform to shape coordinates
221
221
  */
222
222
  function applyTransformToShape(node, matrix) {
223
- switch (node.tag) {
224
- case 'rect':
225
- return applyTransformToRect(node, matrix);
226
- case 'circle':
227
- return applyTransformToCircle(node, matrix);
228
- case 'line':
229
- return applyTransformToLine(node, matrix);
230
- case 'polygon':
231
- case 'polyline':
232
- return applyTransformToPoints(node, matrix);
233
- case 'path':
234
- return applyTransformToPath(node, matrix);
235
- default:
236
- return false;
237
- }
223
+ // O(1) object lookup for shape tag → transform handler
224
+ const shapeHandlers = {
225
+ rect: applyTransformToRect,
226
+ circle: applyTransformToCircle,
227
+ line: applyTransformToLine,
228
+ polygon: applyTransformToPoints,
229
+ polyline: applyTransformToPoints,
230
+ path: applyTransformToPath,
231
+ };
232
+ const handler = shapeHandlers[node.tag];
233
+ return handler ? handler(node, matrix) : false;
238
234
  }
239
235
  /**
240
236
  * Collapse transforms by propagating down the tree
@@ -67,32 +67,27 @@ function multiplyMatrices(m1, m2) {
67
67
  */
68
68
  function transformToMatrix(transform) {
69
69
  const { type, values } = transform;
70
- switch (type) {
71
- case 'matrix':
72
- if (values.length === 6) {
73
- return values;
74
- }
75
- return IDENTITY_MATRIX;
76
- case 'translate': {
70
+ // O(1) object lookup for transform type → matrix factory
71
+ const matrixFactories = {
72
+ matrix: () => (values.length === 6 ? values : IDENTITY_MATRIX),
73
+ translate: () => {
77
74
  const tx = values[0] || 0;
78
75
  const ty = values[1] || 0;
79
76
  return [1, 0, 0, 1, tx, ty];
80
- }
81
- case 'scale': {
77
+ },
78
+ scale: () => {
82
79
  const sx = values[0] || 1;
83
80
  const sy = values[1] !== undefined ? values[1] : sx;
84
81
  return [sx, 0, 0, sy, 0, 0];
85
- }
86
- case 'rotate': {
82
+ },
83
+ rotate: () => {
87
84
  const angle = values[0] || 0;
88
85
  const rad = (angle * Math.PI) / 180;
89
86
  const cos = Math.cos(rad);
90
87
  const sin = Math.sin(rad);
91
- // Rotate around point (cx, cy) if provided
92
88
  if (values.length === 3) {
93
89
  const cx = values[1];
94
90
  const cy = values[2];
95
- // translate(-cx, -cy) × rotate(angle) × translate(cx, cy)
96
91
  return [
97
92
  cos,
98
93
  sin,
@@ -103,22 +98,22 @@ function transformToMatrix(transform) {
103
98
  ];
104
99
  }
105
100
  return [cos, sin, -sin, cos, 0, 0];
106
- }
107
- case 'skewX': {
101
+ },
102
+ skewX: () => {
108
103
  const angle = values[0] || 0;
109
104
  const rad = (angle * Math.PI) / 180;
110
105
  const tan = Math.tan(rad);
111
106
  return [1, 0, tan, 1, 0, 0];
112
- }
113
- case 'skewY': {
107
+ },
108
+ skewY: () => {
114
109
  const angle = values[0] || 0;
115
110
  const rad = (angle * Math.PI) / 180;
116
111
  const tan = Math.tan(rad);
117
112
  return [1, tan, 0, 1, 0, 0];
118
- }
119
- default:
120
- return IDENTITY_MATRIX;
121
- }
113
+ },
114
+ };
115
+ const factory = matrixFactories[type];
116
+ return factory ? factory() : IDENTITY_MATRIX;
122
117
  }
123
118
  /**
124
119
  * Consolidate transform list into single matrix