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/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 +258 -200
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +254 -204
- 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 -284
- package/src/modules/cache.ts +79 -59
- 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 +67 -1
- package/src/global.d.ts +0 -6
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
|
}
|
package/src/types.ts
CHANGED
|
@@ -2,26 +2,92 @@ import { ReactNode, Ref, SVGProps } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { STATUS } from './config';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Called when loading the SVG fails.
|
|
7
|
+
*/
|
|
5
8
|
export type ErrorCallback = (error: Error | FetchError) => void;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Called when the SVG loads successfully.
|
|
12
|
+
*/
|
|
6
13
|
export type LoadCallback = (src: string, isCached: boolean) => void;
|
|
7
|
-
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Pre-processes the SVG string before parsing.
|
|
17
|
+
* Must return a string.
|
|
18
|
+
*/
|
|
8
19
|
export type PreProcessorCallback = (code: string) => string;
|
|
9
20
|
|
|
10
21
|
export type Props = Simplify<
|
|
11
22
|
Omit<SVGProps<SVGElement>, 'onLoad' | 'onError' | 'ref'> & {
|
|
23
|
+
/**
|
|
24
|
+
* A URL to prepend to url() references inside the SVG when using `uniquifyIDs`.
|
|
25
|
+
* Only required if your page uses an HTML `<base>` tag.
|
|
26
|
+
*/
|
|
12
27
|
baseURL?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Cache remote SVGs in memory.
|
|
30
|
+
*
|
|
31
|
+
* When used with the CacheProvider, requests are also persisted in the browser cache.
|
|
32
|
+
* @default true
|
|
33
|
+
*/
|
|
13
34
|
cacheRequests?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Fallback content rendered on fetch error or unsupported browser.
|
|
37
|
+
*/
|
|
14
38
|
children?: ReactNode;
|
|
39
|
+
/**
|
|
40
|
+
* A description for the SVG.
|
|
41
|
+
* Overrides an existing `<desc>` tag.
|
|
42
|
+
*/
|
|
15
43
|
description?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Custom options for the fetch request.
|
|
46
|
+
*/
|
|
16
47
|
fetchOptions?: RequestInit;
|
|
48
|
+
/**
|
|
49
|
+
* A ref to the rendered SVG element.
|
|
50
|
+
* Not available on initial render — use `onLoad` instead.
|
|
51
|
+
*/
|
|
17
52
|
innerRef?: Ref<SVGElement | null>;
|
|
53
|
+
/**
|
|
54
|
+
* A component shown while the SVG is loading.
|
|
55
|
+
*/
|
|
18
56
|
loader?: ReactNode;
|
|
57
|
+
/**
|
|
58
|
+
* Called when loading the SVG fails.
|
|
59
|
+
* Receives an `Error` or `FetchError`.
|
|
60
|
+
*/
|
|
19
61
|
onError?: ErrorCallback;
|
|
62
|
+
/**
|
|
63
|
+
* Called when the SVG loads successfully.
|
|
64
|
+
* Receives the `src` and an `isCached` flag.
|
|
65
|
+
*/
|
|
20
66
|
onLoad?: LoadCallback;
|
|
67
|
+
/**
|
|
68
|
+
* A function to pre-process the SVG string before parsing.
|
|
69
|
+
* Must return a string.
|
|
70
|
+
*/
|
|
21
71
|
preProcessor?: PreProcessorCallback;
|
|
72
|
+
/**
|
|
73
|
+
* The SVG to load.
|
|
74
|
+
* Accepts a URL or path, a data URI (base64 or URL-encoded), or a raw SVG string.
|
|
75
|
+
*/
|
|
22
76
|
src: string;
|
|
77
|
+
/**
|
|
78
|
+
* A title for the SVG. Overrides an existing `<title>` tag.
|
|
79
|
+
* Pass `null` to remove it.
|
|
80
|
+
*/
|
|
23
81
|
title?: string | null;
|
|
82
|
+
/**
|
|
83
|
+
* A string to use with `uniquifyIDs`.
|
|
84
|
+
* @default random 8-character alphanumeric string
|
|
85
|
+
*/
|
|
24
86
|
uniqueHash?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Create unique IDs for each icon.
|
|
89
|
+
* @default false
|
|
90
|
+
*/
|
|
25
91
|
uniquifyIDs?: boolean;
|
|
26
92
|
}
|
|
27
93
|
>;
|