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.js CHANGED
@@ -31,14 +31,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
32
32
 
33
33
  // src/index.tsx
34
- var src_exports = {};
35
- __export(src_exports, {
34
+ var index_exports = {};
35
+ __export(index_exports, {
36
36
  cacheStore: () => cacheStore,
37
37
  default: () => InlineSVG
38
38
  });
39
- module.exports = __toCommonJS(src_exports);
40
- var import_react2 = __toESM(require("react"));
41
- var import_react_from_dom2 = __toESM(require("react-from-dom"));
39
+ module.exports = __toCommonJS(index_exports);
40
+ var import_react4 = require("react");
42
41
 
43
42
  // src/config.ts
44
43
  var CACHE_NAME = "react-inlinesvg";
@@ -95,11 +94,6 @@ async function request(url, options) {
95
94
  }
96
95
  return response.text();
97
96
  }
98
- function sleep(seconds = 1) {
99
- return new Promise((resolve) => {
100
- setTimeout(resolve, seconds * 1e3);
101
- });
102
- }
103
97
  function supportsInlineSVG() {
104
98
  if (!document) {
105
99
  return false;
@@ -112,20 +106,16 @@ function supportsInlineSVG() {
112
106
 
113
107
  // src/modules/cache.ts
114
108
  var CacheStore = class {
115
- constructor() {
109
+ constructor(options = {}) {
116
110
  __publicField(this, "cacheApi");
117
111
  __publicField(this, "cacheStore");
118
112
  __publicField(this, "subscribers", []);
119
113
  __publicField(this, "isReady", false);
114
+ const { name = CACHE_NAME, persistent = false } = options;
120
115
  this.cacheStore = /* @__PURE__ */ new Map();
121
- let cacheName = CACHE_NAME;
122
- let usePersistentCache = false;
123
- if (canUseDOM()) {
124
- cacheName = window.REACT_INLINESVG_CACHE_NAME ?? CACHE_NAME;
125
- usePersistentCache = !!window.REACT_INLINESVG_PERSISTENT_CACHE && "caches" in window;
126
- }
116
+ const usePersistentCache = persistent && canUseDOM() && "caches" in window;
127
117
  if (usePersistentCache) {
128
- caches.open(cacheName).then((cache) => {
118
+ caches.open(name).then((cache) => {
129
119
  this.cacheApi = cache;
130
120
  }).catch((error) => {
131
121
  console.error(`Failed to open cache: ${error.message}`);
@@ -149,12 +139,30 @@ var CacheStore = class {
149
139
  onReady(callback) {
150
140
  if (this.isReady) {
151
141
  callback();
152
- } else {
153
- this.subscribers.push(callback);
142
+ return () => {
143
+ };
144
+ }
145
+ this.subscribers.push(callback);
146
+ return () => {
147
+ const index = this.subscribers.indexOf(callback);
148
+ if (index >= 0) {
149
+ this.subscribers.splice(index, 1);
150
+ }
151
+ };
152
+ }
153
+ waitForReady() {
154
+ if (this.isReady) {
155
+ return Promise.resolve();
154
156
  }
157
+ return new Promise((resolve) => {
158
+ this.onReady(resolve);
159
+ });
155
160
  }
156
161
  async get(url, fetchOptions) {
157
- await (this.cacheApi ? this.fetchAndAddToPersistentCache(url, fetchOptions) : this.fetchAndAddToInternalCache(url, fetchOptions));
162
+ await this.fetchAndCache(url, fetchOptions);
163
+ return this.cacheStore.get(url)?.content ?? "";
164
+ }
165
+ getContent(url) {
158
166
  return this.cacheStore.get(url)?.content ?? "";
159
167
  }
160
168
  set(url, data) {
@@ -163,57 +171,44 @@ var CacheStore = class {
163
171
  isCached(url) {
164
172
  return this.cacheStore.get(url)?.status === STATUS.LOADED;
165
173
  }
166
- async fetchAndAddToInternalCache(url, fetchOptions) {
167
- const cache = this.cacheStore.get(url);
168
- if (cache?.status === STATUS.LOADING) {
169
- await this.handleLoading(url, async () => {
170
- this.cacheStore.set(url, { content: "", status: STATUS.IDLE });
171
- await this.fetchAndAddToInternalCache(url, fetchOptions);
172
- });
173
- return;
174
+ async fetchAndCache(url, fetchOptions) {
175
+ if (!this.isReady) {
176
+ await this.waitForReady();
174
177
  }
175
- if (!cache?.content) {
176
- this.cacheStore.set(url, { content: "", status: STATUS.LOADING });
177
- try {
178
- const content = await request(url, fetchOptions);
179
- this.cacheStore.set(url, { content, status: STATUS.LOADED });
180
- } catch (error) {
181
- this.cacheStore.set(url, { content: "", status: STATUS.FAILED });
182
- throw error;
183
- }
184
- }
185
- }
186
- async fetchAndAddToPersistentCache(url, fetchOptions) {
187
178
  const cache = this.cacheStore.get(url);
188
179
  if (cache?.status === STATUS.LOADED) {
189
180
  return;
190
181
  }
191
182
  if (cache?.status === STATUS.LOADING) {
192
- await this.handleLoading(url, async () => {
183
+ await this.handleLoading(url, fetchOptions?.signal || void 0, async () => {
193
184
  this.cacheStore.set(url, { content: "", status: STATUS.IDLE });
194
- await this.fetchAndAddToPersistentCache(url, fetchOptions);
185
+ await this.fetchAndCache(url, fetchOptions);
195
186
  });
196
187
  return;
197
188
  }
198
189
  this.cacheStore.set(url, { content: "", status: STATUS.LOADING });
199
- const data = await this.cacheApi?.match(url);
200
- if (data) {
201
- const content = await data.text();
202
- this.cacheStore.set(url, { content, status: STATUS.LOADED });
203
- return;
204
- }
205
190
  try {
206
- await this.cacheApi?.add(new Request(url, fetchOptions));
207
- const response = await this.cacheApi?.match(url);
208
- const content = await response?.text() ?? "";
191
+ const content = this.cacheApi ? await this.fetchFromPersistentCache(url, fetchOptions) : await request(url, fetchOptions);
209
192
  this.cacheStore.set(url, { content, status: STATUS.LOADED });
210
193
  } catch (error) {
211
194
  this.cacheStore.set(url, { content: "", status: STATUS.FAILED });
212
195
  throw error;
213
196
  }
214
197
  }
215
- async handleLoading(url, callback) {
198
+ async fetchFromPersistentCache(url, fetchOptions) {
199
+ const data = await this.cacheApi?.match(url);
200
+ if (data) {
201
+ return data.text();
202
+ }
203
+ await this.cacheApi?.add(new Request(url, fetchOptions));
204
+ const response = await this.cacheApi?.match(url);
205
+ return await response?.text() ?? "";
206
+ }
207
+ async handleLoading(url, signal, callback) {
216
208
  for (let retryCount = 0; retryCount < CACHE_MAX_RETRIES; retryCount++) {
209
+ if (signal?.aborted) {
210
+ throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted.", "AbortError");
211
+ }
217
212
  if (this.cacheStore.get(url)?.status !== STATUS.LOADING) {
218
213
  return;
219
214
  }
@@ -241,9 +236,21 @@ var CacheStore = class {
241
236
  this.cacheStore.clear();
242
237
  }
243
238
  };
239
+ function sleep(seconds = 1) {
240
+ return new Promise((resolve) => {
241
+ setTimeout(resolve, seconds * 1e3);
242
+ });
243
+ }
244
+
245
+ // src/modules/useInlineSVG.ts
246
+ var import_react2 = require("react");
247
+ var import_react_from_dom2 = __toESM(require("react-from-dom"));
244
248
 
245
249
  // src/modules/hooks.tsx
246
250
  var import_react = require("react");
251
+ function useMount(effect) {
252
+ (0, import_react.useEffect)(effect, []);
253
+ }
247
254
  function usePrevious(state) {
248
255
  const ref = (0, import_react.useRef)(void 0);
249
256
  (0, import_react.useEffect)(() => {
@@ -254,6 +261,29 @@ function usePrevious(state) {
254
261
 
255
262
  // src/modules/utils.ts
256
263
  var import_react_from_dom = __toESM(require("react-from-dom"));
264
+ function uniquifyStyleIds(svgText, hash, baseURL) {
265
+ const idMatches = svgText.matchAll(/\bid=(["'])([^"']+)\1/g);
266
+ const ids = [...new Set([...idMatches].map((m) => m[2]))];
267
+ if (!ids.length) {
268
+ return svgText;
269
+ }
270
+ ids.sort((a, b) => b.length - a.length);
271
+ return svgText.replace(/<style[^>]*>([\S\s]*?)<\/style>/gi, (fullMatch, cssContent) => {
272
+ let modified = cssContent;
273
+ for (const id of ids) {
274
+ const escaped = id.replace(/[$()*+.?[\\\]^{|}]/g, "\\$&");
275
+ modified = modified.replace(
276
+ new RegExp(`url\\((['"]?)#${escaped}\\1\\)`, "g"),
277
+ `url($1${baseURL}#${id}__${hash}$1)`
278
+ );
279
+ modified = modified.replace(
280
+ new RegExp(`#${escaped}(?![a-zA-Z0-9_-])`, "g"),
281
+ `#${id}__${hash}`
282
+ );
283
+ }
284
+ return fullMatch.replace(cssContent, modified);
285
+ });
286
+ }
257
287
  function getNode(options) {
258
288
  const {
259
289
  baseURL,
@@ -266,7 +296,10 @@ function getNode(options) {
266
296
  uniquifyIDs = false
267
297
  } = options;
268
298
  try {
269
- const svgText = processSVG(content, preProcessor);
299
+ let svgText = processSVG(content, preProcessor);
300
+ if (uniquifyIDs) {
301
+ svgText = uniquifyStyleIds(svgText, hash, baseURL ?? "");
302
+ }
270
303
  const node = (0, import_react_from_dom.default)(svgText, { nodeOnly: true });
271
304
  if (!node || !(node instanceof SVGSVGElement)) {
272
305
  throw new Error("Could not convert the src to a DOM Node");
@@ -336,22 +369,30 @@ function updateSVGAttributes(node, options) {
336
369
  return node;
337
370
  }
338
371
 
339
- // src/index.tsx
340
- var cacheStore;
341
- function ReactInlineSVG(props) {
372
+ // src/modules/useInlineSVG.ts
373
+ function useInlineSVG(props, cacheStore2) {
342
374
  const {
375
+ baseURL,
343
376
  cacheRequests = true,
344
- children = null,
345
377
  description,
346
378
  fetchOptions,
347
- innerRef,
348
- loader = null,
349
379
  onError,
350
380
  onLoad,
381
+ preProcessor,
351
382
  src,
352
383
  title,
353
- uniqueHash
384
+ uniqueHash,
385
+ uniquifyIDs
354
386
  } = props;
387
+ const hash = (0, import_react2.useRef)(uniqueHash ?? randomString(8));
388
+ const fetchOptionsRef = (0, import_react2.useRef)(fetchOptions);
389
+ const onErrorRef = (0, import_react2.useRef)(onError);
390
+ const onLoadRef = (0, import_react2.useRef)(onLoad);
391
+ const preProcessorRef = (0, import_react2.useRef)(preProcessor);
392
+ fetchOptionsRef.current = fetchOptions;
393
+ onErrorRef.current = onError;
394
+ onLoadRef.current = onLoad;
395
+ preProcessorRef.current = preProcessor;
355
396
  const [state, setState] = (0, import_react2.useReducer)(
356
397
  (previousState2, nextState) => ({
357
398
  ...previousState2,
@@ -360,43 +401,71 @@ function ReactInlineSVG(props) {
360
401
  {
361
402
  content: "",
362
403
  element: null,
363
- isCached: cacheRequests && cacheStore.isCached(props.src),
404
+ isCached: false,
364
405
  status: STATUS.IDLE
406
+ },
407
+ (initial) => {
408
+ const cached = cacheRequests && cacheStore2.isCached(src);
409
+ if (!cached) {
410
+ return initial;
411
+ }
412
+ const cachedContent = cacheStore2.getContent(src);
413
+ try {
414
+ const node = getNode({
415
+ ...props,
416
+ handleError: () => {
417
+ },
418
+ hash: hash.current,
419
+ content: cachedContent
420
+ });
421
+ if (!node) {
422
+ return { ...initial, content: cachedContent, isCached: true, status: STATUS.LOADED };
423
+ }
424
+ const convertedElement = (0, import_react_from_dom2.default)(node);
425
+ if (convertedElement && (0, import_react2.isValidElement)(convertedElement)) {
426
+ return {
427
+ content: cachedContent,
428
+ element: convertedElement,
429
+ isCached: true,
430
+ status: STATUS.READY
431
+ };
432
+ }
433
+ } catch {
434
+ }
435
+ return {
436
+ ...initial,
437
+ content: cachedContent,
438
+ isCached: true,
439
+ status: STATUS.LOADED
440
+ };
365
441
  }
366
442
  );
367
443
  const { content, element, isCached, status } = state;
368
444
  const previousProps = usePrevious(props);
369
445
  const previousState = usePrevious(state);
370
- const hash = (0, import_react2.useRef)(uniqueHash ?? randomString(8));
371
446
  const isActive = (0, import_react2.useRef)(false);
372
447
  const isInitialized = (0, import_react2.useRef)(false);
373
- const handleError = (0, import_react2.useCallback)(
374
- (error) => {
375
- if (isActive.current) {
376
- setState({
377
- status: error.message === "Browser does not support SVG" ? STATUS.UNSUPPORTED : STATUS.FAILED
378
- });
379
- onError?.(error);
380
- }
381
- },
382
- [onError]
383
- );
384
- const handleLoad = (0, import_react2.useCallback)((loadedContent, hasCache = false) => {
448
+ const handleError = (0, import_react2.useCallback)((error) => {
385
449
  if (isActive.current) {
386
450
  setState({
387
- content: loadedContent,
388
- isCached: hasCache,
389
- status: STATUS.LOADED
451
+ status: error.message === "Browser does not support SVG" ? STATUS.UNSUPPORTED : STATUS.FAILED
390
452
  });
453
+ onErrorRef.current?.(error);
391
454
  }
392
455
  }, []);
393
- const fetchContent = (0, import_react2.useCallback)(async () => {
394
- const responseContent = await request(src, fetchOptions);
395
- handleLoad(responseContent);
396
- }, [fetchOptions, handleLoad, src]);
397
456
  const getElement = (0, import_react2.useCallback)(() => {
398
457
  try {
399
- const node = getNode({ ...props, handleError, hash: hash.current, content });
458
+ const node = getNode({
459
+ baseURL,
460
+ content,
461
+ description,
462
+ handleError,
463
+ hash: hash.current,
464
+ preProcessor: preProcessorRef.current,
465
+ src,
466
+ title,
467
+ uniquifyIDs
468
+ });
400
469
  const convertedElement = (0, import_react_from_dom2.default)(node);
401
470
  if (!convertedElement || !(0, import_react2.isValidElement)(convertedElement)) {
402
471
  throw new Error("Could not convert the src to a React element");
@@ -408,67 +477,32 @@ function ReactInlineSVG(props) {
408
477
  } catch (error) {
409
478
  handleError(error);
410
479
  }
411
- }, [content, handleError, props]);
412
- const getContent = (0, import_react2.useCallback)(async () => {
413
- const dataURI = /^data:image\/svg[^,]*?(;base64)?,(.*)/u.exec(src);
414
- let inlineSrc;
415
- if (dataURI) {
416
- inlineSrc = dataURI[1] ? window.atob(dataURI[2]) : decodeURIComponent(dataURI[2]);
417
- } else if (src.includes("<svg")) {
418
- inlineSrc = src;
419
- }
420
- if (inlineSrc) {
421
- handleLoad(inlineSrc);
422
- return;
480
+ }, [baseURL, content, description, handleError, src, title, uniquifyIDs]);
481
+ useMount(() => {
482
+ isActive.current = true;
483
+ if (!canUseDOM() || isInitialized.current) {
484
+ return void 0;
423
485
  }
424
486
  try {
425
- if (cacheRequests) {
426
- const cachedContent = await cacheStore.get(src, fetchOptions);
427
- handleLoad(cachedContent, true);
428
- } else {
429
- await fetchContent();
487
+ if (status === STATUS.READY) {
488
+ onLoadRef.current?.(src, isCached);
489
+ } else if (status === STATUS.IDLE) {
490
+ if (!isSupportedEnvironment()) {
491
+ throw new Error("Browser does not support SVG");
492
+ }
493
+ if (!src) {
494
+ throw new Error("Missing src");
495
+ }
496
+ setState({ content: "", element: null, isCached: false, status: STATUS.LOADING });
430
497
  }
431
498
  } catch (error) {
432
499
  handleError(error);
433
500
  }
434
- }, [cacheRequests, fetchContent, fetchOptions, handleError, handleLoad, src]);
435
- const load = (0, import_react2.useCallback)(async () => {
436
- if (isActive.current) {
437
- setState({
438
- content: "",
439
- element: null,
440
- isCached: false,
441
- status: STATUS.LOADING
442
- });
443
- }
444
- }, []);
445
- (0, import_react2.useEffect)(
446
- () => {
447
- isActive.current = true;
448
- if (!canUseDOM() || isInitialized.current) {
449
- return void 0;
450
- }
451
- try {
452
- if (status === STATUS.IDLE) {
453
- if (!isSupportedEnvironment()) {
454
- throw new Error("Browser does not support SVG");
455
- }
456
- if (!src) {
457
- throw new Error("Missing src");
458
- }
459
- load();
460
- }
461
- } catch (error) {
462
- handleError(error);
463
- }
464
- isInitialized.current = true;
465
- return () => {
466
- isActive.current = false;
467
- };
468
- },
469
- // eslint-disable-next-line react-hooks/exhaustive-deps
470
- []
471
- );
501
+ isInitialized.current = true;
502
+ return () => {
503
+ isActive.current = false;
504
+ };
505
+ });
472
506
  (0, import_react2.useEffect)(() => {
473
507
  if (!canUseDOM() || !previousProps) {
474
508
  return;
@@ -478,14 +512,58 @@ function ReactInlineSVG(props) {
478
512
  handleError(new Error("Missing src"));
479
513
  return;
480
514
  }
481
- load();
515
+ setState({ content: "", element: null, isCached: false, status: STATUS.LOADING });
482
516
  }
483
- }, [handleError, load, previousProps, src]);
517
+ }, [handleError, previousProps, src]);
484
518
  (0, import_react2.useEffect)(() => {
485
- if (status === STATUS.LOADED) {
519
+ if (status !== STATUS.LOADING) {
520
+ return void 0;
521
+ }
522
+ const controller = new AbortController();
523
+ let active = true;
524
+ (async () => {
525
+ try {
526
+ const dataURI = /^data:image\/svg[^,]*?(;base64)?,(.*)/.exec(src);
527
+ let inlineSrc;
528
+ if (dataURI) {
529
+ inlineSrc = dataURI[1] ? window.atob(dataURI[2]) : decodeURIComponent(dataURI[2]);
530
+ } else if (src.includes("<svg")) {
531
+ inlineSrc = src;
532
+ }
533
+ if (inlineSrc) {
534
+ if (active) {
535
+ setState({ content: inlineSrc, isCached: false, status: STATUS.LOADED });
536
+ }
537
+ return;
538
+ }
539
+ const fetchParameters = { ...fetchOptionsRef.current, signal: controller.signal };
540
+ let loadedContent;
541
+ let hasCache = false;
542
+ if (cacheRequests) {
543
+ hasCache = cacheStore2.isCached(src);
544
+ loadedContent = await cacheStore2.get(src, fetchParameters);
545
+ } else {
546
+ loadedContent = await request(src, fetchParameters);
547
+ }
548
+ if (active) {
549
+ setState({ content: loadedContent, isCached: hasCache, status: STATUS.LOADED });
550
+ }
551
+ } catch (error) {
552
+ if (active && error.name !== "AbortError") {
553
+ handleError(error);
554
+ }
555
+ }
556
+ })();
557
+ return () => {
558
+ active = false;
559
+ controller.abort();
560
+ };
561
+ }, [cacheRequests, cacheStore2, handleError, src, status]);
562
+ (0, import_react2.useEffect)(() => {
563
+ if (status === STATUS.LOADED && content) {
486
564
  getElement();
487
565
  }
488
- }, [status, getElement]);
566
+ }, [content, getElement, status]);
489
567
  (0, import_react2.useEffect)(() => {
490
568
  if (!canUseDOM() || !previousProps || previousProps.src !== src) {
491
569
  return;
@@ -498,51 +576,50 @@ function ReactInlineSVG(props) {
498
576
  if (!previousState) {
499
577
  return;
500
578
  }
501
- switch (status) {
502
- case STATUS.LOADING: {
503
- if (previousState.status !== STATUS.LOADING) {
504
- getContent();
505
- }
506
- break;
507
- }
508
- case STATUS.LOADED: {
509
- if (previousState.status !== STATUS.LOADED) {
510
- getElement();
511
- }
512
- break;
513
- }
514
- case STATUS.READY: {
515
- if (previousState.status !== STATUS.READY) {
516
- onLoad?.(src, isCached);
517
- }
518
- break;
519
- }
579
+ if (status === STATUS.READY && previousState.status !== STATUS.READY) {
580
+ onLoadRef.current?.(src, isCached);
520
581
  }
521
- }, [getContent, getElement, isCached, onLoad, previousState, src, status]);
522
- const elementProps = omit(
523
- props,
524
- "baseURL",
525
- "cacheRequests",
526
- "children",
527
- "description",
528
- "fetchOptions",
529
- "innerRef",
530
- "loader",
531
- "onError",
532
- "onLoad",
533
- "preProcessor",
534
- "src",
535
- "title",
536
- "uniqueHash",
537
- "uniquifyIDs"
538
- );
582
+ }, [isCached, previousState, src, status]);
583
+ return { element, status };
584
+ }
585
+
586
+ // src/provider.tsx
587
+ var import_react3 = __toESM(require("react"));
588
+ var CacheContext = (0, import_react3.createContext)(null);
589
+ function useCacheStore() {
590
+ return (0, import_react3.useContext)(CacheContext);
591
+ }
592
+
593
+ // src/index.tsx
594
+ var cacheStore = new CacheStore();
595
+ function InlineSVG(props) {
596
+ const { children = null, innerRef, loader = null } = props;
597
+ const contextStore = useCacheStore();
598
+ const store = contextStore ?? cacheStore;
599
+ const { element, status } = useInlineSVG(props, store);
539
600
  if (!canUseDOM()) {
540
601
  return loader;
541
602
  }
542
603
  if (element) {
543
- return (0, import_react2.cloneElement)(element, {
604
+ return (0, import_react4.cloneElement)(element, {
544
605
  ref: innerRef,
545
- ...elementProps
606
+ ...omit(
607
+ props,
608
+ "baseURL",
609
+ "cacheRequests",
610
+ "children",
611
+ "description",
612
+ "fetchOptions",
613
+ "innerRef",
614
+ "loader",
615
+ "onError",
616
+ "onLoad",
617
+ "preProcessor",
618
+ "src",
619
+ "title",
620
+ "uniqueHash",
621
+ "uniquifyIDs"
622
+ )
546
623
  });
547
624
  }
548
625
  if ([STATUS.UNSUPPORTED, STATUS.FAILED].includes(status)) {
@@ -550,25 +627,6 @@ function ReactInlineSVG(props) {
550
627
  }
551
628
  return loader;
552
629
  }
553
- function InlineSVG(props) {
554
- if (!cacheStore) {
555
- cacheStore = new CacheStore();
556
- }
557
- const { loader } = props;
558
- const [isReady, setReady] = (0, import_react2.useState)(cacheStore.isReady);
559
- (0, import_react2.useEffect)(() => {
560
- if (isReady) {
561
- return;
562
- }
563
- cacheStore.onReady(() => {
564
- setReady(true);
565
- });
566
- }, [isReady]);
567
- if (!isReady) {
568
- return loader;
569
- }
570
- return /* @__PURE__ */ import_react2.default.createElement(ReactInlineSVG, { ...props });
571
- }
572
630
  // Annotate the CommonJS export names for ESM import in node:
573
631
  0 && (module.exports = {
574
632
  cacheStore