react-inlinesvg 4.2.0 → 4.3.0

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.
package/dist/index.mjs CHANGED
@@ -4,16 +4,7 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
4
4
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
5
 
6
6
  // src/index.tsx
7
- import React, {
8
- cloneElement,
9
- isValidElement,
10
- useCallback,
11
- useEffect as useEffect2,
12
- useReducer,
13
- useRef as useRef2,
14
- useState
15
- } from "react";
16
- import convert2 from "react-from-dom";
7
+ import { cloneElement } from "react";
17
8
 
18
9
  // src/config.ts
19
10
  var CACHE_NAME = "react-inlinesvg";
@@ -70,11 +61,6 @@ async function request(url, options) {
70
61
  }
71
62
  return response.text();
72
63
  }
73
- function sleep(seconds = 1) {
74
- return new Promise((resolve) => {
75
- setTimeout(resolve, seconds * 1e3);
76
- });
77
- }
78
64
  function supportsInlineSVG() {
79
65
  if (!document) {
80
66
  return false;
@@ -87,20 +73,16 @@ function supportsInlineSVG() {
87
73
 
88
74
  // src/modules/cache.ts
89
75
  var CacheStore = class {
90
- constructor() {
76
+ constructor(options = {}) {
91
77
  __publicField(this, "cacheApi");
92
78
  __publicField(this, "cacheStore");
93
79
  __publicField(this, "subscribers", []);
94
80
  __publicField(this, "isReady", false);
81
+ const { name = CACHE_NAME, persistent = false } = options;
95
82
  this.cacheStore = /* @__PURE__ */ new Map();
96
- let cacheName = CACHE_NAME;
97
- let usePersistentCache = false;
98
- if (canUseDOM()) {
99
- cacheName = window.REACT_INLINESVG_CACHE_NAME ?? CACHE_NAME;
100
- usePersistentCache = !!window.REACT_INLINESVG_PERSISTENT_CACHE && "caches" in window;
101
- }
83
+ const usePersistentCache = persistent && canUseDOM() && "caches" in window;
102
84
  if (usePersistentCache) {
103
- caches.open(cacheName).then((cache) => {
85
+ caches.open(name).then((cache) => {
104
86
  this.cacheApi = cache;
105
87
  }).catch((error) => {
106
88
  console.error(`Failed to open cache: ${error.message}`);
@@ -124,12 +106,30 @@ var CacheStore = class {
124
106
  onReady(callback) {
125
107
  if (this.isReady) {
126
108
  callback();
127
- } else {
128
- this.subscribers.push(callback);
109
+ return () => {
110
+ };
111
+ }
112
+ this.subscribers.push(callback);
113
+ return () => {
114
+ const index = this.subscribers.indexOf(callback);
115
+ if (index >= 0) {
116
+ this.subscribers.splice(index, 1);
117
+ }
118
+ };
119
+ }
120
+ waitForReady() {
121
+ if (this.isReady) {
122
+ return Promise.resolve();
129
123
  }
124
+ return new Promise((resolve) => {
125
+ this.onReady(resolve);
126
+ });
130
127
  }
131
128
  async get(url, fetchOptions) {
132
- await (this.cacheApi ? this.fetchAndAddToPersistentCache(url, fetchOptions) : this.fetchAndAddToInternalCache(url, fetchOptions));
129
+ await this.fetchAndCache(url, fetchOptions);
130
+ return this.cacheStore.get(url)?.content ?? "";
131
+ }
132
+ getContent(url) {
133
133
  return this.cacheStore.get(url)?.content ?? "";
134
134
  }
135
135
  set(url, data) {
@@ -138,57 +138,44 @@ var CacheStore = class {
138
138
  isCached(url) {
139
139
  return this.cacheStore.get(url)?.status === STATUS.LOADED;
140
140
  }
141
- async fetchAndAddToInternalCache(url, fetchOptions) {
142
- const cache = this.cacheStore.get(url);
143
- if (cache?.status === STATUS.LOADING) {
144
- await this.handleLoading(url, async () => {
145
- this.cacheStore.set(url, { content: "", status: STATUS.IDLE });
146
- await this.fetchAndAddToInternalCache(url, fetchOptions);
147
- });
148
- return;
141
+ async fetchAndCache(url, fetchOptions) {
142
+ if (!this.isReady) {
143
+ await this.waitForReady();
149
144
  }
150
- if (!cache?.content) {
151
- this.cacheStore.set(url, { content: "", status: STATUS.LOADING });
152
- try {
153
- const content = await request(url, fetchOptions);
154
- this.cacheStore.set(url, { content, status: STATUS.LOADED });
155
- } catch (error) {
156
- this.cacheStore.set(url, { content: "", status: STATUS.FAILED });
157
- throw error;
158
- }
159
- }
160
- }
161
- async fetchAndAddToPersistentCache(url, fetchOptions) {
162
145
  const cache = this.cacheStore.get(url);
163
146
  if (cache?.status === STATUS.LOADED) {
164
147
  return;
165
148
  }
166
149
  if (cache?.status === STATUS.LOADING) {
167
- await this.handleLoading(url, async () => {
150
+ await this.handleLoading(url, fetchOptions?.signal || void 0, async () => {
168
151
  this.cacheStore.set(url, { content: "", status: STATUS.IDLE });
169
- await this.fetchAndAddToPersistentCache(url, fetchOptions);
152
+ await this.fetchAndCache(url, fetchOptions);
170
153
  });
171
154
  return;
172
155
  }
173
156
  this.cacheStore.set(url, { content: "", status: STATUS.LOADING });
174
- const data = await this.cacheApi?.match(url);
175
- if (data) {
176
- const content = await data.text();
177
- this.cacheStore.set(url, { content, status: STATUS.LOADED });
178
- return;
179
- }
180
157
  try {
181
- await this.cacheApi?.add(new Request(url, fetchOptions));
182
- const response = await this.cacheApi?.match(url);
183
- const content = await response?.text() ?? "";
158
+ const content = this.cacheApi ? await this.fetchFromPersistentCache(url, fetchOptions) : await request(url, fetchOptions);
184
159
  this.cacheStore.set(url, { content, status: STATUS.LOADED });
185
160
  } catch (error) {
186
161
  this.cacheStore.set(url, { content: "", status: STATUS.FAILED });
187
162
  throw error;
188
163
  }
189
164
  }
190
- async handleLoading(url, callback) {
165
+ async fetchFromPersistentCache(url, fetchOptions) {
166
+ const data = await this.cacheApi?.match(url);
167
+ if (data) {
168
+ return data.text();
169
+ }
170
+ await this.cacheApi?.add(new Request(url, fetchOptions));
171
+ const response = await this.cacheApi?.match(url);
172
+ return await response?.text() ?? "";
173
+ }
174
+ async handleLoading(url, signal, callback) {
191
175
  for (let retryCount = 0; retryCount < CACHE_MAX_RETRIES; retryCount++) {
176
+ if (signal?.aborted) {
177
+ throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted.", "AbortError");
178
+ }
192
179
  if (this.cacheStore.get(url)?.status !== STATUS.LOADING) {
193
180
  return;
194
181
  }
@@ -216,9 +203,21 @@ var CacheStore = class {
216
203
  this.cacheStore.clear();
217
204
  }
218
205
  };
206
+ function sleep(seconds = 1) {
207
+ return new Promise((resolve) => {
208
+ setTimeout(resolve, seconds * 1e3);
209
+ });
210
+ }
211
+
212
+ // src/modules/useInlineSVG.ts
213
+ import { isValidElement, useCallback, useEffect as useEffect2, useReducer, useRef as useRef2 } from "react";
214
+ import convert2 from "react-from-dom";
219
215
 
220
216
  // src/modules/hooks.tsx
221
217
  import { useEffect, useRef } from "react";
218
+ function useMount(effect) {
219
+ useEffect(effect, []);
220
+ }
222
221
  function usePrevious(state) {
223
222
  const ref = useRef(void 0);
224
223
  useEffect(() => {
@@ -229,6 +228,29 @@ function usePrevious(state) {
229
228
 
230
229
  // src/modules/utils.ts
231
230
  import convert from "react-from-dom";
231
+ function uniquifyStyleIds(svgText, hash, baseURL) {
232
+ const idMatches = svgText.matchAll(/\bid=(["'])([^"']+)\1/g);
233
+ const ids = [...new Set([...idMatches].map((m) => m[2]))];
234
+ if (!ids.length) {
235
+ return svgText;
236
+ }
237
+ ids.sort((a, b) => b.length - a.length);
238
+ return svgText.replace(/<style[^>]*>([\S\s]*?)<\/style>/gi, (fullMatch, cssContent) => {
239
+ let modified = cssContent;
240
+ for (const id of ids) {
241
+ const escaped = id.replace(/[$()*+.?[\\\]^{|}]/g, "\\$&");
242
+ modified = modified.replace(
243
+ new RegExp(`url\\((['"]?)#${escaped}\\1\\)`, "g"),
244
+ `url($1${baseURL}#${id}__${hash}$1)`
245
+ );
246
+ modified = modified.replace(
247
+ new RegExp(`#${escaped}(?![a-zA-Z0-9_-])`, "g"),
248
+ `#${id}__${hash}`
249
+ );
250
+ }
251
+ return fullMatch.replace(cssContent, modified);
252
+ });
253
+ }
232
254
  function getNode(options) {
233
255
  const {
234
256
  baseURL,
@@ -241,7 +263,10 @@ function getNode(options) {
241
263
  uniquifyIDs = false
242
264
  } = options;
243
265
  try {
244
- const svgText = processSVG(content, preProcessor);
266
+ let svgText = processSVG(content, preProcessor);
267
+ if (uniquifyIDs) {
268
+ svgText = uniquifyStyleIds(svgText, hash, baseURL ?? "");
269
+ }
245
270
  const node = convert(svgText, { nodeOnly: true });
246
271
  if (!node || !(node instanceof SVGSVGElement)) {
247
272
  throw new Error("Could not convert the src to a DOM Node");
@@ -311,22 +336,30 @@ function updateSVGAttributes(node, options) {
311
336
  return node;
312
337
  }
313
338
 
314
- // src/index.tsx
315
- var cacheStore;
316
- function ReactInlineSVG(props) {
339
+ // src/modules/useInlineSVG.ts
340
+ function useInlineSVG(props, cacheStore2) {
317
341
  const {
342
+ baseURL,
318
343
  cacheRequests = true,
319
- children = null,
320
344
  description,
321
345
  fetchOptions,
322
- innerRef,
323
- loader = null,
324
346
  onError,
325
347
  onLoad,
348
+ preProcessor,
326
349
  src,
327
350
  title,
328
- uniqueHash
351
+ uniqueHash,
352
+ uniquifyIDs
329
353
  } = props;
354
+ const hash = useRef2(uniqueHash ?? randomString(8));
355
+ const fetchOptionsRef = useRef2(fetchOptions);
356
+ const onErrorRef = useRef2(onError);
357
+ const onLoadRef = useRef2(onLoad);
358
+ const preProcessorRef = useRef2(preProcessor);
359
+ fetchOptionsRef.current = fetchOptions;
360
+ onErrorRef.current = onError;
361
+ onLoadRef.current = onLoad;
362
+ preProcessorRef.current = preProcessor;
330
363
  const [state, setState] = useReducer(
331
364
  (previousState2, nextState) => ({
332
365
  ...previousState2,
@@ -335,43 +368,71 @@ function ReactInlineSVG(props) {
335
368
  {
336
369
  content: "",
337
370
  element: null,
338
- isCached: cacheRequests && cacheStore.isCached(props.src),
371
+ isCached: false,
339
372
  status: STATUS.IDLE
373
+ },
374
+ (initial) => {
375
+ const cached = cacheRequests && cacheStore2.isCached(src);
376
+ if (!cached) {
377
+ return initial;
378
+ }
379
+ const cachedContent = cacheStore2.getContent(src);
380
+ try {
381
+ const node = getNode({
382
+ ...props,
383
+ handleError: () => {
384
+ },
385
+ hash: hash.current,
386
+ content: cachedContent
387
+ });
388
+ if (!node) {
389
+ return { ...initial, content: cachedContent, isCached: true, status: STATUS.LOADED };
390
+ }
391
+ const convertedElement = convert2(node);
392
+ if (convertedElement && isValidElement(convertedElement)) {
393
+ return {
394
+ content: cachedContent,
395
+ element: convertedElement,
396
+ isCached: true,
397
+ status: STATUS.READY
398
+ };
399
+ }
400
+ } catch {
401
+ }
402
+ return {
403
+ ...initial,
404
+ content: cachedContent,
405
+ isCached: true,
406
+ status: STATUS.LOADED
407
+ };
340
408
  }
341
409
  );
342
410
  const { content, element, isCached, status } = state;
343
411
  const previousProps = usePrevious(props);
344
412
  const previousState = usePrevious(state);
345
- const hash = useRef2(uniqueHash ?? randomString(8));
346
413
  const isActive = useRef2(false);
347
414
  const isInitialized = useRef2(false);
348
- const handleError = useCallback(
349
- (error) => {
350
- if (isActive.current) {
351
- setState({
352
- status: error.message === "Browser does not support SVG" ? STATUS.UNSUPPORTED : STATUS.FAILED
353
- });
354
- onError?.(error);
355
- }
356
- },
357
- [onError]
358
- );
359
- const handleLoad = useCallback((loadedContent, hasCache = false) => {
415
+ const handleError = useCallback((error) => {
360
416
  if (isActive.current) {
361
417
  setState({
362
- content: loadedContent,
363
- isCached: hasCache,
364
- status: STATUS.LOADED
418
+ status: error.message === "Browser does not support SVG" ? STATUS.UNSUPPORTED : STATUS.FAILED
365
419
  });
420
+ onErrorRef.current?.(error);
366
421
  }
367
422
  }, []);
368
- const fetchContent = useCallback(async () => {
369
- const responseContent = await request(src, fetchOptions);
370
- handleLoad(responseContent);
371
- }, [fetchOptions, handleLoad, src]);
372
423
  const getElement = useCallback(() => {
373
424
  try {
374
- const node = getNode({ ...props, handleError, hash: hash.current, content });
425
+ const node = getNode({
426
+ baseURL,
427
+ content,
428
+ description,
429
+ handleError,
430
+ hash: hash.current,
431
+ preProcessor: preProcessorRef.current,
432
+ src,
433
+ title,
434
+ uniquifyIDs
435
+ });
375
436
  const convertedElement = convert2(node);
376
437
  if (!convertedElement || !isValidElement(convertedElement)) {
377
438
  throw new Error("Could not convert the src to a React element");
@@ -383,67 +444,32 @@ function ReactInlineSVG(props) {
383
444
  } catch (error) {
384
445
  handleError(error);
385
446
  }
386
- }, [content, handleError, props]);
387
- const getContent = useCallback(async () => {
388
- const dataURI = /^data:image\/svg[^,]*?(;base64)?,(.*)/u.exec(src);
389
- let inlineSrc;
390
- if (dataURI) {
391
- inlineSrc = dataURI[1] ? window.atob(dataURI[2]) : decodeURIComponent(dataURI[2]);
392
- } else if (src.includes("<svg")) {
393
- inlineSrc = src;
394
- }
395
- if (inlineSrc) {
396
- handleLoad(inlineSrc);
397
- return;
447
+ }, [baseURL, content, description, handleError, src, title, uniquifyIDs]);
448
+ useMount(() => {
449
+ isActive.current = true;
450
+ if (!canUseDOM() || isInitialized.current) {
451
+ return void 0;
398
452
  }
399
453
  try {
400
- if (cacheRequests) {
401
- const cachedContent = await cacheStore.get(src, fetchOptions);
402
- handleLoad(cachedContent, true);
403
- } else {
404
- await fetchContent();
454
+ if (status === STATUS.READY) {
455
+ onLoadRef.current?.(src, isCached);
456
+ } else if (status === STATUS.IDLE) {
457
+ if (!isSupportedEnvironment()) {
458
+ throw new Error("Browser does not support SVG");
459
+ }
460
+ if (!src) {
461
+ throw new Error("Missing src");
462
+ }
463
+ setState({ content: "", element: null, isCached: false, status: STATUS.LOADING });
405
464
  }
406
465
  } catch (error) {
407
466
  handleError(error);
408
467
  }
409
- }, [cacheRequests, fetchContent, fetchOptions, handleError, handleLoad, src]);
410
- const load = useCallback(async () => {
411
- if (isActive.current) {
412
- setState({
413
- content: "",
414
- element: null,
415
- isCached: false,
416
- status: STATUS.LOADING
417
- });
418
- }
419
- }, []);
420
- useEffect2(
421
- () => {
422
- isActive.current = true;
423
- if (!canUseDOM() || isInitialized.current) {
424
- return void 0;
425
- }
426
- try {
427
- if (status === STATUS.IDLE) {
428
- if (!isSupportedEnvironment()) {
429
- throw new Error("Browser does not support SVG");
430
- }
431
- if (!src) {
432
- throw new Error("Missing src");
433
- }
434
- load();
435
- }
436
- } catch (error) {
437
- handleError(error);
438
- }
439
- isInitialized.current = true;
440
- return () => {
441
- isActive.current = false;
442
- };
443
- },
444
- // eslint-disable-next-line react-hooks/exhaustive-deps
445
- []
446
- );
468
+ isInitialized.current = true;
469
+ return () => {
470
+ isActive.current = false;
471
+ };
472
+ });
447
473
  useEffect2(() => {
448
474
  if (!canUseDOM() || !previousProps) {
449
475
  return;
@@ -453,14 +479,58 @@ function ReactInlineSVG(props) {
453
479
  handleError(new Error("Missing src"));
454
480
  return;
455
481
  }
456
- load();
482
+ setState({ content: "", element: null, isCached: false, status: STATUS.LOADING });
457
483
  }
458
- }, [handleError, load, previousProps, src]);
484
+ }, [handleError, previousProps, src]);
459
485
  useEffect2(() => {
460
- if (status === STATUS.LOADED) {
486
+ if (status !== STATUS.LOADING) {
487
+ return void 0;
488
+ }
489
+ const controller = new AbortController();
490
+ let active = true;
491
+ (async () => {
492
+ try {
493
+ const dataURI = /^data:image\/svg[^,]*?(;base64)?,(.*)/.exec(src);
494
+ let inlineSrc;
495
+ if (dataURI) {
496
+ inlineSrc = dataURI[1] ? window.atob(dataURI[2]) : decodeURIComponent(dataURI[2]);
497
+ } else if (src.includes("<svg")) {
498
+ inlineSrc = src;
499
+ }
500
+ if (inlineSrc) {
501
+ if (active) {
502
+ setState({ content: inlineSrc, isCached: false, status: STATUS.LOADED });
503
+ }
504
+ return;
505
+ }
506
+ const fetchParameters = { ...fetchOptionsRef.current, signal: controller.signal };
507
+ let loadedContent;
508
+ let hasCache = false;
509
+ if (cacheRequests) {
510
+ hasCache = cacheStore2.isCached(src);
511
+ loadedContent = await cacheStore2.get(src, fetchParameters);
512
+ } else {
513
+ loadedContent = await request(src, fetchParameters);
514
+ }
515
+ if (active) {
516
+ setState({ content: loadedContent, isCached: hasCache, status: STATUS.LOADED });
517
+ }
518
+ } catch (error) {
519
+ if (active && error.name !== "AbortError") {
520
+ handleError(error);
521
+ }
522
+ }
523
+ })();
524
+ return () => {
525
+ active = false;
526
+ controller.abort();
527
+ };
528
+ }, [cacheRequests, cacheStore2, handleError, src, status]);
529
+ useEffect2(() => {
530
+ if (status === STATUS.LOADED && content) {
461
531
  getElement();
462
532
  }
463
- }, [status, getElement]);
533
+ }, [content, getElement, status]);
464
534
  useEffect2(() => {
465
535
  if (!canUseDOM() || !previousProps || previousProps.src !== src) {
466
536
  return;
@@ -473,51 +543,50 @@ function ReactInlineSVG(props) {
473
543
  if (!previousState) {
474
544
  return;
475
545
  }
476
- switch (status) {
477
- case STATUS.LOADING: {
478
- if (previousState.status !== STATUS.LOADING) {
479
- getContent();
480
- }
481
- break;
482
- }
483
- case STATUS.LOADED: {
484
- if (previousState.status !== STATUS.LOADED) {
485
- getElement();
486
- }
487
- break;
488
- }
489
- case STATUS.READY: {
490
- if (previousState.status !== STATUS.READY) {
491
- onLoad?.(src, isCached);
492
- }
493
- break;
494
- }
546
+ if (status === STATUS.READY && previousState.status !== STATUS.READY) {
547
+ onLoadRef.current?.(src, isCached);
495
548
  }
496
- }, [getContent, getElement, isCached, onLoad, previousState, src, status]);
497
- const elementProps = omit(
498
- props,
499
- "baseURL",
500
- "cacheRequests",
501
- "children",
502
- "description",
503
- "fetchOptions",
504
- "innerRef",
505
- "loader",
506
- "onError",
507
- "onLoad",
508
- "preProcessor",
509
- "src",
510
- "title",
511
- "uniqueHash",
512
- "uniquifyIDs"
513
- );
549
+ }, [isCached, previousState, src, status]);
550
+ return { element, status };
551
+ }
552
+
553
+ // src/provider.tsx
554
+ import React, { createContext, useContext, useState } from "react";
555
+ var CacheContext = createContext(null);
556
+ function useCacheStore() {
557
+ return useContext(CacheContext);
558
+ }
559
+
560
+ // src/index.tsx
561
+ var cacheStore = new CacheStore();
562
+ function InlineSVG(props) {
563
+ const { children = null, innerRef, loader = null } = props;
564
+ const contextStore = useCacheStore();
565
+ const store = contextStore ?? cacheStore;
566
+ const { element, status } = useInlineSVG(props, store);
514
567
  if (!canUseDOM()) {
515
568
  return loader;
516
569
  }
517
570
  if (element) {
518
571
  return cloneElement(element, {
519
572
  ref: innerRef,
520
- ...elementProps
573
+ ...omit(
574
+ props,
575
+ "baseURL",
576
+ "cacheRequests",
577
+ "children",
578
+ "description",
579
+ "fetchOptions",
580
+ "innerRef",
581
+ "loader",
582
+ "onError",
583
+ "onLoad",
584
+ "preProcessor",
585
+ "src",
586
+ "title",
587
+ "uniqueHash",
588
+ "uniquifyIDs"
589
+ )
521
590
  });
522
591
  }
523
592
  if ([STATUS.UNSUPPORTED, STATUS.FAILED].includes(status)) {
@@ -525,25 +594,6 @@ function ReactInlineSVG(props) {
525
594
  }
526
595
  return loader;
527
596
  }
528
- function InlineSVG(props) {
529
- if (!cacheStore) {
530
- cacheStore = new CacheStore();
531
- }
532
- const { loader } = props;
533
- const [isReady, setReady] = useState(cacheStore.isReady);
534
- useEffect2(() => {
535
- if (isReady) {
536
- return;
537
- }
538
- cacheStore.onReady(() => {
539
- setReady(true);
540
- });
541
- }, [isReady]);
542
- if (!isReady) {
543
- return loader;
544
- }
545
- return /* @__PURE__ */ React.createElement(ReactInlineSVG, { ...props });
546
- }
547
597
  export {
548
598
  cacheStore,
549
599
  InlineSVG as default