react-inlinesvg 4.1.8 → 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/README.md +28 -34
- package/dist/cache-NLB60kAd.d.mts +142 -0
- package/dist/cache-NLB60kAd.d.ts +142 -0
- package/dist/index.d.mts +7 -73
- package/dist/index.d.ts +7 -73
- package/dist/index.js +284 -202
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +280 -206
- package/dist/index.mjs.map +1 -1
- package/dist/provider.d.mts +5 -3
- package/dist/provider.d.ts +5 -3
- package/dist/provider.js +187 -6
- package/dist/provider.js.map +1 -1
- package/dist/provider.mjs +176 -6
- package/dist/provider.mjs.map +1 -1
- package/package.json +25 -41
- package/src/index.tsx +29 -261
- package/src/modules/cache.ts +100 -70
- package/src/modules/helpers.ts +1 -9
- package/src/modules/hooks.tsx +6 -1
- package/src/modules/useInlineSVG.ts +272 -0
- package/src/modules/utils.ts +36 -1
- package/src/provider.tsx +10 -7
- package/src/types.ts +69 -3
- package/src/global.d.ts +0 -6
package/src/modules/cache.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { CACHE_MAX_RETRIES, CACHE_NAME, STATUS } from '../config';
|
|
2
2
|
import { StorageItem } from '../types';
|
|
3
3
|
|
|
4
|
-
import { canUseDOM, request
|
|
4
|
+
import { canUseDOM, request } from './helpers';
|
|
5
|
+
|
|
6
|
+
export interface CacheStoreOptions {
|
|
7
|
+
name?: string;
|
|
8
|
+
persistent?: boolean;
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
export default class CacheStore {
|
|
7
12
|
private cacheApi: Cache | undefined;
|
|
@@ -9,49 +14,82 @@ export default class CacheStore {
|
|
|
9
14
|
private readonly subscribers: Array<() => void> = [];
|
|
10
15
|
public isReady = false;
|
|
11
16
|
|
|
12
|
-
constructor() {
|
|
13
|
-
|
|
17
|
+
constructor(options: CacheStoreOptions = {}) {
|
|
18
|
+
const { name = CACHE_NAME, persistent = false } = options;
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
let usePersistentCache = false;
|
|
20
|
+
this.cacheStore = new Map<string, StorageItem>();
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
cacheName = window.REACT_INLINESVG_CACHE_NAME ?? CACHE_NAME;
|
|
20
|
-
usePersistentCache = !!window.REACT_INLINESVG_PERSISTENT_CACHE && 'caches' in window;
|
|
21
|
-
}
|
|
22
|
+
const usePersistentCache = persistent && canUseDOM() && 'caches' in window;
|
|
22
23
|
|
|
23
24
|
if (usePersistentCache) {
|
|
25
|
+
// eslint-disable-next-line promise/catch-or-return
|
|
24
26
|
caches
|
|
25
|
-
.open(
|
|
27
|
+
.open(name)
|
|
26
28
|
.then(cache => {
|
|
27
29
|
this.cacheApi = cache;
|
|
28
30
|
})
|
|
29
31
|
.catch(error => {
|
|
30
32
|
// eslint-disable-next-line no-console
|
|
31
33
|
console.error(`Failed to open cache: ${error.message}`);
|
|
34
|
+
this.cacheApi = undefined;
|
|
32
35
|
})
|
|
33
36
|
.finally(() => {
|
|
34
37
|
this.isReady = true;
|
|
35
|
-
|
|
38
|
+
// Copy to avoid mutation issues
|
|
39
|
+
const callbacks = [...this.subscribers];
|
|
40
|
+
|
|
41
|
+
// Clear array efficiently
|
|
42
|
+
this.subscribers.length = 0;
|
|
43
|
+
|
|
44
|
+
callbacks.forEach(callback => {
|
|
45
|
+
try {
|
|
46
|
+
callback();
|
|
47
|
+
} catch (error: any) {
|
|
48
|
+
// eslint-disable-next-line no-console
|
|
49
|
+
console.error(`Error in CacheStore subscriber callback: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
36
52
|
});
|
|
37
53
|
} else {
|
|
38
54
|
this.isReady = true;
|
|
39
55
|
}
|
|
40
56
|
}
|
|
41
57
|
|
|
42
|
-
public onReady(callback: () => void) {
|
|
58
|
+
public onReady(callback: () => void): () => void {
|
|
43
59
|
if (this.isReady) {
|
|
44
60
|
callback();
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
|
|
62
|
+
return () => {};
|
|
47
63
|
}
|
|
64
|
+
|
|
65
|
+
this.subscribers.push(callback);
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
const index = this.subscribers.indexOf(callback);
|
|
69
|
+
|
|
70
|
+
if (index >= 0) {
|
|
71
|
+
this.subscribers.splice(index, 1);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private waitForReady(): Promise<void> {
|
|
77
|
+
if (this.isReady) {
|
|
78
|
+
return Promise.resolve();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return new Promise(resolve => {
|
|
82
|
+
this.onReady(resolve);
|
|
83
|
+
});
|
|
48
84
|
}
|
|
49
85
|
|
|
50
86
|
public async get(url: string, fetchOptions?: RequestInit) {
|
|
51
|
-
await
|
|
52
|
-
|
|
53
|
-
|
|
87
|
+
await this.fetchAndCache(url, fetchOptions);
|
|
88
|
+
|
|
89
|
+
return this.cacheStore.get(url)?.content ?? '';
|
|
90
|
+
}
|
|
54
91
|
|
|
92
|
+
public getContent(url: string): string {
|
|
55
93
|
return this.cacheStore.get(url)?.content ?? '';
|
|
56
94
|
}
|
|
57
95
|
|
|
@@ -63,33 +101,11 @@ export default class CacheStore {
|
|
|
63
101
|
return this.cacheStore.get(url)?.status === STATUS.LOADED;
|
|
64
102
|
}
|
|
65
103
|
|
|
66
|
-
private async
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (cache?.status === STATUS.LOADING) {
|
|
70
|
-
await this.handleLoading(url, async () => {
|
|
71
|
-
this.cacheStore.set(url, { content: '', status: STATUS.IDLE });
|
|
72
|
-
await this.fetchAndAddToInternalCache(url, fetchOptions);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!cache?.content) {
|
|
79
|
-
this.cacheStore.set(url, { content: '', status: STATUS.LOADING });
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const content = await request(url, fetchOptions);
|
|
83
|
-
|
|
84
|
-
this.cacheStore.set(url, { content, status: STATUS.LOADED });
|
|
85
|
-
} catch (error: any) {
|
|
86
|
-
this.cacheStore.set(url, { content: '', status: STATUS.FAILED });
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
104
|
+
private async fetchAndCache(url: string, fetchOptions?: RequestInit) {
|
|
105
|
+
if (!this.isReady) {
|
|
106
|
+
await this.waitForReady();
|
|
89
107
|
}
|
|
90
|
-
}
|
|
91
108
|
|
|
92
|
-
private async fetchAndAddToPersistentCache(url: string, fetchOptions?: RequestInit) {
|
|
93
109
|
const cache = this.cacheStore.get(url);
|
|
94
110
|
|
|
95
111
|
if (cache?.status === STATUS.LOADED) {
|
|
@@ -97,9 +113,9 @@ export default class CacheStore {
|
|
|
97
113
|
}
|
|
98
114
|
|
|
99
115
|
if (cache?.status === STATUS.LOADING) {
|
|
100
|
-
await this.handleLoading(url, async () => {
|
|
116
|
+
await this.handleLoading(url, fetchOptions?.signal || undefined, async () => {
|
|
101
117
|
this.cacheStore.set(url, { content: '', status: STATUS.IDLE });
|
|
102
|
-
await this.
|
|
118
|
+
await this.fetchAndCache(url, fetchOptions);
|
|
103
119
|
});
|
|
104
120
|
|
|
105
121
|
return;
|
|
@@ -107,21 +123,10 @@ export default class CacheStore {
|
|
|
107
123
|
|
|
108
124
|
this.cacheStore.set(url, { content: '', status: STATUS.LOADING });
|
|
109
125
|
|
|
110
|
-
const data = await this.cacheApi?.match(url);
|
|
111
|
-
|
|
112
|
-
if (data) {
|
|
113
|
-
const content = await data.text();
|
|
114
|
-
|
|
115
|
-
this.cacheStore.set(url, { content, status: STATUS.LOADED });
|
|
116
|
-
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
126
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const content = (await response?.text()) ?? '';
|
|
127
|
+
const content = this.cacheApi
|
|
128
|
+
? await this.fetchFromPersistentCache(url, fetchOptions)
|
|
129
|
+
: await request(url, fetchOptions);
|
|
125
130
|
|
|
126
131
|
this.cacheStore.set(url, { content, status: STATUS.LOADED });
|
|
127
132
|
} catch (error: any) {
|
|
@@ -130,18 +135,40 @@ export default class CacheStore {
|
|
|
130
135
|
}
|
|
131
136
|
}
|
|
132
137
|
|
|
133
|
-
private async
|
|
134
|
-
|
|
138
|
+
private async fetchFromPersistentCache(url: string, fetchOptions?: RequestInit): Promise<string> {
|
|
139
|
+
const data = await this.cacheApi?.match(url);
|
|
135
140
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
await sleep(0.1);
|
|
139
|
-
retryCount += 1;
|
|
141
|
+
if (data) {
|
|
142
|
+
return data.text();
|
|
140
143
|
}
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
await this.cacheApi?.add(new Request(url, fetchOptions));
|
|
146
|
+
|
|
147
|
+
const response = await this.cacheApi?.match(url);
|
|
148
|
+
|
|
149
|
+
return (await response?.text()) ?? '';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async handleLoading(
|
|
153
|
+
url: string,
|
|
154
|
+
signal: AbortSignal | undefined,
|
|
155
|
+
callback: () => Promise<void>,
|
|
156
|
+
) {
|
|
157
|
+
for (let retryCount = 0; retryCount < CACHE_MAX_RETRIES; retryCount++) {
|
|
158
|
+
if (signal?.aborted) {
|
|
159
|
+
throw signal.reason instanceof Error
|
|
160
|
+
? signal.reason
|
|
161
|
+
: new DOMException('The operation was aborted.', 'AbortError');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (this.cacheStore.get(url)?.status !== STATUS.LOADING) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await sleep(0.1);
|
|
144
169
|
}
|
|
170
|
+
|
|
171
|
+
await callback();
|
|
145
172
|
}
|
|
146
173
|
|
|
147
174
|
public keys(): Array<string> {
|
|
@@ -164,12 +191,15 @@ export default class CacheStore {
|
|
|
164
191
|
if (this.cacheApi) {
|
|
165
192
|
const keys = await this.cacheApi.keys();
|
|
166
193
|
|
|
167
|
-
|
|
168
|
-
// eslint-disable-next-line no-await-in-loop
|
|
169
|
-
await this.cacheApi.delete(key);
|
|
170
|
-
}
|
|
194
|
+
await Promise.allSettled(keys.map(key => this.cacheApi!.delete(key)));
|
|
171
195
|
}
|
|
172
196
|
|
|
173
197
|
this.cacheStore.clear();
|
|
174
198
|
}
|
|
175
199
|
}
|
|
200
|
+
|
|
201
|
+
function sleep(seconds = 1) {
|
|
202
|
+
return new Promise(resolve => {
|
|
203
|
+
setTimeout(resolve, seconds * 1000);
|
|
204
|
+
});
|
|
205
|
+
}
|
package/src/modules/helpers.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { PlainObject } from '../types';
|
|
2
|
-
|
|
3
1
|
function randomCharacter(character: string) {
|
|
4
2
|
return character[Math.floor(Math.random() * character.length)];
|
|
5
3
|
}
|
|
@@ -15,7 +13,7 @@ export function isSupportedEnvironment(): boolean {
|
|
|
15
13
|
/**
|
|
16
14
|
* Remove properties from an object
|
|
17
15
|
*/
|
|
18
|
-
export function omit<T extends
|
|
16
|
+
export function omit<T extends Record<string, unknown>, K extends keyof T>(
|
|
19
17
|
input: T,
|
|
20
18
|
...filter: K[]
|
|
21
19
|
): Omit<T, K> {
|
|
@@ -62,12 +60,6 @@ export async function request(url: string, options?: RequestInit) {
|
|
|
62
60
|
return response.text();
|
|
63
61
|
}
|
|
64
62
|
|
|
65
|
-
export function sleep(seconds = 1) {
|
|
66
|
-
return new Promise(resolve => {
|
|
67
|
-
setTimeout(resolve, seconds * 1000);
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
63
|
export function supportsInlineSVG(): boolean {
|
|
72
64
|
/* c8 ignore next 3 */
|
|
73
65
|
if (!document) {
|
package/src/modules/hooks.tsx
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react';
|
|
1
|
+
import { EffectCallback, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useMount(effect: EffectCallback) {
|
|
4
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
5
|
+
useEffect(effect, []);
|
|
6
|
+
}
|
|
2
7
|
|
|
3
8
|
export function usePrevious<T>(state: T): T | undefined {
|
|
4
9
|
const ref = useRef<T>(undefined);
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { isValidElement, useCallback, useEffect, useReducer, useRef } from 'react';
|
|
2
|
+
import convert from 'react-from-dom';
|
|
3
|
+
|
|
4
|
+
import { STATUS } from '../config';
|
|
5
|
+
import type { FetchError, Props, State } from '../types';
|
|
6
|
+
|
|
7
|
+
import type CacheStore from './cache';
|
|
8
|
+
import { canUseDOM, isSupportedEnvironment, randomString, request } from './helpers';
|
|
9
|
+
import { useMount, usePrevious } from './hooks';
|
|
10
|
+
import { getNode } from './utils';
|
|
11
|
+
|
|
12
|
+
export default function useInlineSVG(props: Props, cacheStore: CacheStore) {
|
|
13
|
+
const {
|
|
14
|
+
baseURL,
|
|
15
|
+
cacheRequests = true,
|
|
16
|
+
description,
|
|
17
|
+
fetchOptions,
|
|
18
|
+
onError,
|
|
19
|
+
onLoad,
|
|
20
|
+
preProcessor,
|
|
21
|
+
src,
|
|
22
|
+
title,
|
|
23
|
+
uniqueHash,
|
|
24
|
+
uniquifyIDs,
|
|
25
|
+
} = props;
|
|
26
|
+
|
|
27
|
+
const hash = useRef(uniqueHash ?? randomString(8));
|
|
28
|
+
const fetchOptionsRef = useRef(fetchOptions);
|
|
29
|
+
const onErrorRef = useRef(onError);
|
|
30
|
+
const onLoadRef = useRef(onLoad);
|
|
31
|
+
const preProcessorRef = useRef(preProcessor);
|
|
32
|
+
|
|
33
|
+
fetchOptionsRef.current = fetchOptions;
|
|
34
|
+
onErrorRef.current = onError;
|
|
35
|
+
onLoadRef.current = onLoad;
|
|
36
|
+
preProcessorRef.current = preProcessor;
|
|
37
|
+
|
|
38
|
+
const [state, setState] = useReducer(
|
|
39
|
+
(previousState: State, nextState: Partial<State>) => ({
|
|
40
|
+
...previousState,
|
|
41
|
+
...nextState,
|
|
42
|
+
}),
|
|
43
|
+
{
|
|
44
|
+
content: '',
|
|
45
|
+
element: null,
|
|
46
|
+
isCached: false,
|
|
47
|
+
status: STATUS.IDLE,
|
|
48
|
+
},
|
|
49
|
+
(initial): State => {
|
|
50
|
+
const cached = cacheRequests && cacheStore.isCached(src);
|
|
51
|
+
|
|
52
|
+
if (!cached) {
|
|
53
|
+
return initial;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const cachedContent = cacheStore.getContent(src);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const node = getNode({
|
|
60
|
+
...props,
|
|
61
|
+
handleError: () => {},
|
|
62
|
+
hash: hash.current,
|
|
63
|
+
content: cachedContent,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!node) {
|
|
67
|
+
return { ...initial, content: cachedContent, isCached: true, status: STATUS.LOADED };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const convertedElement = convert(node as Node);
|
|
71
|
+
|
|
72
|
+
if (convertedElement && isValidElement(convertedElement)) {
|
|
73
|
+
return {
|
|
74
|
+
content: cachedContent,
|
|
75
|
+
element: convertedElement,
|
|
76
|
+
isCached: true,
|
|
77
|
+
status: STATUS.READY,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Fall through to effect-driven flow
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
...initial,
|
|
86
|
+
content: cachedContent,
|
|
87
|
+
isCached: true,
|
|
88
|
+
status: STATUS.LOADED,
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
const { content, element, isCached, status } = state;
|
|
93
|
+
const previousProps = usePrevious(props);
|
|
94
|
+
const previousState = usePrevious(state);
|
|
95
|
+
const isActive = useRef(false);
|
|
96
|
+
const isInitialized = useRef(false);
|
|
97
|
+
|
|
98
|
+
const handleError = useCallback((error: Error | FetchError) => {
|
|
99
|
+
if (isActive.current) {
|
|
100
|
+
setState({
|
|
101
|
+
status:
|
|
102
|
+
error.message === 'Browser does not support SVG' ? STATUS.UNSUPPORTED : STATUS.FAILED,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
onErrorRef.current?.(error);
|
|
106
|
+
}
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
const getElement = useCallback(() => {
|
|
110
|
+
try {
|
|
111
|
+
const node = getNode({
|
|
112
|
+
baseURL,
|
|
113
|
+
content,
|
|
114
|
+
description,
|
|
115
|
+
handleError,
|
|
116
|
+
hash: hash.current,
|
|
117
|
+
preProcessor: preProcessorRef.current,
|
|
118
|
+
src,
|
|
119
|
+
title,
|
|
120
|
+
uniquifyIDs,
|
|
121
|
+
}) as Node;
|
|
122
|
+
const convertedElement = convert(node);
|
|
123
|
+
|
|
124
|
+
if (!convertedElement || !isValidElement(convertedElement)) {
|
|
125
|
+
throw new Error('Could not convert the src to a React element');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
setState({
|
|
129
|
+
element: convertedElement,
|
|
130
|
+
status: STATUS.READY,
|
|
131
|
+
});
|
|
132
|
+
} catch (error: any) {
|
|
133
|
+
handleError(error);
|
|
134
|
+
}
|
|
135
|
+
}, [baseURL, content, description, handleError, src, title, uniquifyIDs]);
|
|
136
|
+
|
|
137
|
+
// Mount
|
|
138
|
+
useMount(() => {
|
|
139
|
+
isActive.current = true;
|
|
140
|
+
|
|
141
|
+
if (!canUseDOM() || isInitialized.current) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
if (status === STATUS.READY) {
|
|
147
|
+
onLoadRef.current?.(src, isCached);
|
|
148
|
+
} else if (status === STATUS.IDLE) {
|
|
149
|
+
if (!isSupportedEnvironment()) {
|
|
150
|
+
throw new Error('Browser does not support SVG');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!src) {
|
|
154
|
+
throw new Error('Missing src');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setState({ content: '', element: null, isCached: false, status: STATUS.LOADING });
|
|
158
|
+
}
|
|
159
|
+
} catch (error: any) {
|
|
160
|
+
handleError(error);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
isInitialized.current = true;
|
|
164
|
+
|
|
165
|
+
return () => {
|
|
166
|
+
isActive.current = false;
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Src changes
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!canUseDOM() || !previousProps) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (previousProps.src !== src) {
|
|
177
|
+
if (!src) {
|
|
178
|
+
handleError(new Error('Missing src'));
|
|
179
|
+
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
setState({ content: '', element: null, isCached: false, status: STATUS.LOADING });
|
|
184
|
+
}
|
|
185
|
+
}, [handleError, previousProps, src]);
|
|
186
|
+
|
|
187
|
+
// Fetch content when status is LOADING
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (status !== STATUS.LOADING) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const controller = new AbortController();
|
|
194
|
+
let active = true;
|
|
195
|
+
|
|
196
|
+
(async () => {
|
|
197
|
+
try {
|
|
198
|
+
const dataURI = /^data:image\/svg[^,]*?(;base64)?,(.*)/.exec(src);
|
|
199
|
+
let inlineSrc;
|
|
200
|
+
|
|
201
|
+
if (dataURI) {
|
|
202
|
+
inlineSrc = dataURI[1] ? window.atob(dataURI[2]) : decodeURIComponent(dataURI[2]);
|
|
203
|
+
} else if (src.includes('<svg')) {
|
|
204
|
+
inlineSrc = src;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (inlineSrc) {
|
|
208
|
+
if (active) {
|
|
209
|
+
setState({ content: inlineSrc, isCached: false, status: STATUS.LOADED });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const fetchParameters = { ...fetchOptionsRef.current, signal: controller.signal };
|
|
216
|
+
let loadedContent: string;
|
|
217
|
+
let hasCache = false;
|
|
218
|
+
|
|
219
|
+
if (cacheRequests) {
|
|
220
|
+
hasCache = cacheStore.isCached(src);
|
|
221
|
+
loadedContent = await cacheStore.get(src, fetchParameters);
|
|
222
|
+
} else {
|
|
223
|
+
loadedContent = await request(src, fetchParameters);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (active) {
|
|
227
|
+
setState({ content: loadedContent, isCached: hasCache, status: STATUS.LOADED });
|
|
228
|
+
}
|
|
229
|
+
} catch (error: any) {
|
|
230
|
+
if (active && error.name !== 'AbortError') {
|
|
231
|
+
handleError(error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
})();
|
|
235
|
+
|
|
236
|
+
return () => {
|
|
237
|
+
active = false;
|
|
238
|
+
controller.abort();
|
|
239
|
+
};
|
|
240
|
+
}, [cacheRequests, cacheStore, handleError, src, status]);
|
|
241
|
+
|
|
242
|
+
// LOADED -> READY
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
if (status === STATUS.LOADED && content) {
|
|
245
|
+
getElement();
|
|
246
|
+
}
|
|
247
|
+
}, [content, getElement, status]);
|
|
248
|
+
|
|
249
|
+
// Title and description changes
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
if (!canUseDOM() || !previousProps || previousProps.src !== src) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (previousProps.title !== title || previousProps.description !== description) {
|
|
256
|
+
getElement();
|
|
257
|
+
}
|
|
258
|
+
}, [description, getElement, previousProps, src, title]);
|
|
259
|
+
|
|
260
|
+
// READY -> onLoad
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (!previousState) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (status === STATUS.READY && previousState.status !== STATUS.READY) {
|
|
267
|
+
onLoadRef.current?.(src, isCached);
|
|
268
|
+
}
|
|
269
|
+
}, [isCached, previousState, src, status]);
|
|
270
|
+
|
|
271
|
+
return { element, status };
|
|
272
|
+
}
|
package/src/modules/utils.ts
CHANGED
|
@@ -11,6 +11,36 @@ interface UpdateSVGAttributesOptions extends Pick<Props, 'baseURL' | 'uniquifyID
|
|
|
11
11
|
hash: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function uniquifyStyleIds(svgText: string, hash: string, baseURL: string): string {
|
|
15
|
+
const idMatches = svgText.matchAll(/\bid=(["'])([^"']+)\1/g);
|
|
16
|
+
const ids = [...new Set([...idMatches].map(m => m[2]))];
|
|
17
|
+
|
|
18
|
+
if (!ids.length) {
|
|
19
|
+
return svgText;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
ids.sort((a, b) => b.length - a.length);
|
|
23
|
+
|
|
24
|
+
return svgText.replace(/<style[^>]*>([\S\s]*?)<\/style>/gi, (fullMatch, cssContent) => {
|
|
25
|
+
let modified = cssContent as string;
|
|
26
|
+
|
|
27
|
+
for (const id of ids) {
|
|
28
|
+
const escaped = id.replace(/[$()*+.?[\\\]^{|}]/g, '\\$&');
|
|
29
|
+
|
|
30
|
+
modified = modified.replace(
|
|
31
|
+
new RegExp(`url\\((['"]?)#${escaped}\\1\\)`, 'g'),
|
|
32
|
+
`url($1${baseURL}#${id}__${hash}$1)`,
|
|
33
|
+
);
|
|
34
|
+
modified = modified.replace(
|
|
35
|
+
new RegExp(`#${escaped}(?![a-zA-Z0-9_-])`, 'g'),
|
|
36
|
+
`#${id}__${hash}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return fullMatch.replace(cssContent, modified);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
14
44
|
export function getNode(options: GetNodeOptions) {
|
|
15
45
|
const {
|
|
16
46
|
baseURL,
|
|
@@ -24,7 +54,12 @@ export function getNode(options: GetNodeOptions) {
|
|
|
24
54
|
} = options;
|
|
25
55
|
|
|
26
56
|
try {
|
|
27
|
-
|
|
57
|
+
let svgText = processSVG(content, preProcessor);
|
|
58
|
+
|
|
59
|
+
if (uniquifyIDs) {
|
|
60
|
+
svgText = uniquifyStyleIds(svgText, hash, baseURL ?? '');
|
|
61
|
+
}
|
|
62
|
+
|
|
28
63
|
const node = convert(svgText, { nodeOnly: true });
|
|
29
64
|
|
|
30
65
|
if (!node || !(node instanceof SVGSVGElement)) {
|
package/src/provider.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { ReactNode } from 'react';
|
|
1
|
+
import React, { createContext, ReactNode, useContext, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import CacheStore from './modules/cache';
|
|
4
|
+
|
|
5
|
+
const CacheContext = createContext<CacheStore | null>(null);
|
|
4
6
|
|
|
5
7
|
interface Props {
|
|
6
8
|
children: ReactNode;
|
|
@@ -8,10 +10,11 @@ interface Props {
|
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export default function CacheProvider({ children, name }: Props) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const [store] = useState(() => new CacheStore({ name, persistent: true }));
|
|
14
|
+
|
|
15
|
+
return <CacheContext.Provider value={store}>{children}</CacheContext.Provider>;
|
|
16
|
+
}
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
export function useCacheStore(): CacheStore | null {
|
|
19
|
+
return useContext(CacheContext);
|
|
17
20
|
}
|