svger-cli 4.0.1 → 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.
Files changed (41) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/README.md +27 -27
  3. package/dist/builder.d.ts +6 -3
  4. package/dist/builder.js +34 -24
  5. package/dist/cli.js +122 -10
  6. package/dist/config.d.ts +8 -2
  7. package/dist/config.js +17 -124
  8. package/dist/core/enhanced-plugin-manager.d.ts +1 -0
  9. package/dist/core/enhanced-plugin-manager.js +37 -11
  10. package/dist/core/framework-templates.js +4 -0
  11. package/dist/core/logger.js +8 -4
  12. package/dist/core/performance-engine.js +16 -3
  13. package/dist/core/style-compiler.js +6 -7
  14. package/dist/core/template-manager.js +18 -14
  15. package/dist/index.d.ts +1 -2
  16. package/dist/index.js +8 -2
  17. package/dist/integrations/jest-preset.js +30 -2
  18. package/dist/lock.js +1 -1
  19. package/dist/optimizers/basic-cleaner.js +4 -0
  20. package/dist/optimizers/path-parser.js +199 -115
  21. package/dist/optimizers/path-simplifier.js +27 -24
  22. package/dist/optimizers/remove-unused-defs.js +16 -0
  23. package/dist/optimizers/shape-conversion.js +22 -27
  24. package/dist/optimizers/style-optimizer.js +5 -0
  25. package/dist/optimizers/svg-tree-parser.js +4 -0
  26. package/dist/optimizers/transform-collapsing.js +11 -15
  27. package/dist/optimizers/transform-optimizer.js +20 -21
  28. package/dist/optimizers/types.js +64 -74
  29. package/dist/plugins/gradient-optimizer.js +4 -0
  30. package/dist/processors/svg-processor.js +28 -10
  31. package/dist/services/config.js +28 -11
  32. package/dist/services/file-watcher.js +8 -3
  33. package/dist/services/svg-service.d.ts +1 -1
  34. package/dist/services/svg-service.js +24 -11
  35. package/dist/utils/native.d.ts +0 -1
  36. package/dist/utils/native.js +6 -14
  37. package/dist/utils/visual-diff.js +7 -2
  38. package/dist/watch.js +4 -3
  39. package/docs/ERROR-HANDLING-STANDARD.md +111 -0
  40. package/docs/OPTIONAL-DEPENDENCIES.md +1 -1
  41. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,10 +4,16 @@
4
4
  * A high-performance, framework-agnostic SVG processing toolkit with enterprise-grade
5
5
  * architecture and comprehensive styling capabilities.
6
6
  *
7
- * @version 2.0.0
8
7
  * @author SVGER-CLI Development Team
9
8
  * @license MIT
10
9
  */
10
+ import { readFileSync } from 'fs';
11
+ import { fileURLToPath } from 'url';
12
+ import { dirname, join } from 'path';
13
+ // Get package version dynamically
14
+ const __index_filename = fileURLToPath(import.meta.url);
15
+ const __index_dirname = dirname(__index_filename);
16
+ const __packageJson = JSON.parse(readFileSync(join(__index_dirname, '../package.json'), 'utf-8'));
11
17
  // ============================================================================
12
18
  // CORE SERVICES
13
19
  // ============================================================================
@@ -149,7 +155,7 @@ export const SVGER = {
149
155
  /**
150
156
  * Package Version Information
151
157
  */
152
- export const VERSION = '2.0.0';
158
+ export const VERSION = __packageJson.version;
153
159
  export const PACKAGE_NAME = 'svger-cli';
154
160
  // ============================================================================
155
161
  // BUILD TOOL INTEGRATIONS
@@ -72,12 +72,40 @@ module.exports.default = ${componentName};
72
72
  }
73
73
  },
74
74
  };
75
+ /**
76
+ * Synchronous SVG content cleaning for Jest compatibility.
77
+ * svgProcessor.cleanSVGContent() is async (uses optimizer pipeline) and cannot
78
+ * be called from Jest's synchronous process() method. This provides equivalent
79
+ * basic cleaning without the async optimizer pipeline.
80
+ */
81
+ function cleanSVGContentSync(svgContent) {
82
+ return (svgContent
83
+ // Remove XML declaration
84
+ .replace(/<\?xml.*?\?>/g, '')
85
+ // Remove DOCTYPE declaration
86
+ .replace(/<!DOCTYPE.*?>/g, '')
87
+ // Remove comments
88
+ .replace(/<!--[\s\S]*?-->/g, '')
89
+ // Normalize whitespace
90
+ .replace(/\r?\n|\r/g, '')
91
+ .replace(/\s{2,}/g, ' ')
92
+ // Remove xmlns attributes
93
+ .replace(/\s+xmlns(:xlink)?="[^"]*"/g, '')
94
+ // Remove metadata
95
+ .replace(/<metadata[\s\S]*?<\/metadata>/gi, '')
96
+ .replace(/<title[\s\S]*?<\/title>/gi, '')
97
+ .replace(/<desc[\s\S]*?<\/desc>/gi, '')
98
+ .trim()
99
+ // Extract inner content from <svg> tag
100
+ .replace(/^<svg[^>]*>([\s\S]*)<\/svg>$/i, '$1')
101
+ .trim());
102
+ }
75
103
  /**
76
104
  * Generate component synchronously (for Jest compatibility)
77
105
  */
78
106
  function generateComponentSync(componentName, svgContent, options) {
79
- // Clean SVG content
80
- const cleanedContent = svgProcessor.cleanSVGContent(svgContent);
107
+ // Clean SVG content synchronously (cleanSVGContent is async and cannot be used here)
108
+ const cleanedContent = cleanSVGContentSync(svgContent);
81
109
  // For Jest, we'll generate a simple React component
82
110
  const viewBox = svgProcessor.extractViewBox(svgContent);
83
111
  const template = `
package/dist/lock.js CHANGED
@@ -7,7 +7,7 @@ const LOCK_FILE = '.svg-lock';
7
7
  * @returns {string} Absolute path to .svg-lock
8
8
  */
9
9
  function getLockFilePath() {
10
- return path.resolve(LOCK_FILE);
10
+ return path.resolve(process.cwd(), LOCK_FILE);
11
11
  }
12
12
  /**
13
13
  * Read the current locked SVG files from the lock file.
@@ -204,6 +204,10 @@ export function sortAttributes(svg, config) {
204
204
  const attrRegex = /([a-zA-Z][a-zA-Z0-9-]*)="([^"]*)"/g;
205
205
  let attrMatch;
206
206
  while ((attrMatch = attrRegex.exec(attrs)) !== null) {
207
+ // Prevent infinite loop if regex doesn't advance
208
+ if (attrMatch.index === attrRegex.lastIndex) {
209
+ attrRegex.lastIndex++;
210
+ }
207
211
  attrPairs.push([attrMatch[1], attrMatch[2]]);
208
212
  }
209
213
  // Sort alphabetically
@@ -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 };
@@ -39,12 +39,20 @@ function findReferencedIds(root) {
39
39
  let match;
40
40
  URL_REFERENCE_REGEX.lastIndex = 0;
41
41
  while ((match = URL_REFERENCE_REGEX.exec(attrValue)) !== null) {
42
+ // Prevent infinite loop if regex doesn't advance
43
+ if (match.index === URL_REFERENCE_REGEX.lastIndex) {
44
+ URL_REFERENCE_REGEX.lastIndex++;
45
+ }
42
46
  referencedIds.add(match[1]);
43
47
  }
44
48
  // Check for #id pattern (href, xlink:href)
45
49
  if (attrName === 'href' || attrName === 'xlink:href') {
46
50
  HREF_REFERENCE_REGEX.lastIndex = 0;
47
51
  while ((match = HREF_REFERENCE_REGEX.exec(attrValue)) !== null) {
52
+ // Prevent infinite loop if regex doesn't advance
53
+ if (match.index === HREF_REFERENCE_REGEX.lastIndex) {
54
+ HREF_REFERENCE_REGEX.lastIndex++;
55
+ }
48
56
  referencedIds.add(match[1]);
49
57
  }
50
58
  }
@@ -56,6 +64,10 @@ function findReferencedIds(root) {
56
64
  let match;
57
65
  URL_REFERENCE_REGEX.lastIndex = 0;
58
66
  while ((match = URL_REFERENCE_REGEX.exec(style)) !== null) {
67
+ // Prevent infinite loop if regex doesn't advance
68
+ if (match.index === URL_REFERENCE_REGEX.lastIndex) {
69
+ URL_REFERENCE_REGEX.lastIndex++;
70
+ }
59
71
  referencedIds.add(match[1]);
60
72
  }
61
73
  }
@@ -64,6 +76,10 @@ function findReferencedIds(root) {
64
76
  let match;
65
77
  URL_REFERENCE_REGEX.lastIndex = 0;
66
78
  while ((match = URL_REFERENCE_REGEX.exec(node.content)) !== null) {
79
+ // Prevent infinite loop if regex doesn't advance
80
+ if (match.index === URL_REFERENCE_REGEX.lastIndex) {
81
+ URL_REFERENCE_REGEX.lastIndex++;
82
+ }
67
83
  referencedIds.add(match[1]);
68
84
  }
69
85
  }