react-inlinesvg
Version:
An SVG loader for React
327 lines (326 loc) • 9.77 kB
JavaScript
"use client";
import { a as isSupportedEnvironment, c as request, i as canUseDOM, l as STATUS, n as useCacheStore, o as omit, r as CacheStore, s as randomString } from "./provider-D8ZFleLH.mjs";
import { cloneElement, isValidElement, useCallback, useEffect, useReducer, useRef } from "react";
import convert from "react-from-dom";
//#region src/modules/hooks.tsx
function useMount(effect) {
useEffect(effect, []);
}
function usePrevious(state) {
const ref = useRef(void 0);
useEffect(() => {
ref.current = state;
});
return ref.current;
}
//#endregion
//#region src/modules/utils.ts
function uniquifyStyleIds(svgText, hash, baseURL) {
const idMatches = svgText.matchAll(/\bid=(["'])([^"']+)\1/g);
const ids = [...new Set([...idMatches].map((m) => m[2]))];
if (!ids.length) return svgText;
ids.sort((a, b) => b.length - a.length);
return svgText.replace(/<style[^>]*>([\S\s]*?)<\/style>/gi, (fullMatch, cssContent) => {
let modified = cssContent;
for (const id of ids) {
const escaped = id.replace(/[$()*+.?[\\\]^{|}]/g, "\\$&");
modified = modified.replace(new RegExp(`url\\((['"]?)#${escaped}\\1\\)`, "g"), `url($1${baseURL}#${id}__${hash}$1)`);
modified = modified.replace(new RegExp(`#${escaped}(?![a-zA-Z0-9_-])`, "g"), `#${id}__${hash}`);
}
return fullMatch.replace(cssContent, modified);
});
}
function getNode(options) {
const { baseURL, content, description, handleError, hash, preProcessor, title, uniquifyIDs = false } = options;
try {
let svgText = preProcessor ? preProcessor(content) : content;
if (uniquifyIDs) svgText = uniquifyStyleIds(svgText, hash, baseURL ?? "");
const node = convert(svgText, { nodeOnly: true });
if (!node || !(node instanceof SVGSVGElement)) throw new Error("Could not convert the src to a DOM Node");
const svg = updateSVGAttributes(node, {
baseURL,
hash,
uniquifyIDs
});
if (description) {
const originalDesc = svg.querySelector("desc");
if (originalDesc?.parentNode) originalDesc.parentNode.removeChild(originalDesc);
const descElement = document.createElementNS("http://www.w3.org/2000/svg", "desc");
descElement.innerHTML = description;
svg.prepend(descElement);
}
if (typeof title !== "undefined") {
const originalTitle = svg.querySelector("title");
if (originalTitle?.parentNode) originalTitle.parentNode.removeChild(originalTitle);
if (title) {
const titleElement = document.createElementNS("http://www.w3.org/2000/svg", "title");
titleElement.innerHTML = title;
svg.prepend(titleElement);
}
}
return svg;
} catch (error) {
return handleError(error);
}
}
function updateSVGAttributes(node, options) {
const { baseURL = "", hash, uniquifyIDs } = options;
const replaceableAttributes = [
"id",
"href",
"xlink:href",
"xlink:role",
"xlink:arcrole"
];
const linkAttributes = ["href", "xlink:href"];
const isDataValue = (name, value) => linkAttributes.includes(name) && (value ? !value.includes("#") : false);
if (!uniquifyIDs) return node;
[...node.children].forEach((d) => {
if (d.attributes?.length) {
const attributes = Object.values(d.attributes).map((a) => {
const attribute = a;
const match = /url\((.*?)\)/.exec(a.value);
if (match?.[1]) attribute.value = a.value.replace(match[0], `url(${baseURL}${match[1]}__${hash})`);
return attribute;
});
replaceableAttributes.forEach((r) => {
const attribute = attributes.find((a) => a.name === r);
if (attribute && !isDataValue(r, attribute.value)) attribute.value = `${attribute.value}__${hash}`;
});
}
if (d.children.length) return updateSVGAttributes(d, options);
return d;
});
return node;
}
//#endregion
//#region src/modules/useInlineSVG.ts
function useInlineSVG(props, cacheStore) {
const { baseURL, cacheRequests = true, description, fetchOptions, onError, onLoad, preProcessor, src, title, uniqueHash, uniquifyIDs } = props;
const hash = useRef(uniqueHash ?? randomString(8));
const fetchOptionsRef = useRef(fetchOptions);
const onErrorRef = useRef(onError);
const onLoadRef = useRef(onLoad);
const preProcessorRef = useRef(preProcessor);
fetchOptionsRef.current = fetchOptions;
onErrorRef.current = onError;
onLoadRef.current = onLoad;
preProcessorRef.current = preProcessor;
const [state, setState] = useReducer((previousState, nextState) => ({
...previousState,
...nextState
}), {
content: "",
element: null,
isCached: false,
status: STATUS.IDLE
}, (initial) => {
if (!(cacheRequests && cacheStore.isCached(src))) return initial;
const cachedContent = cacheStore.getContent(src);
try {
const node = getNode({
...props,
handleError: () => {},
hash: hash.current,
content: cachedContent
});
if (!node) return {
...initial,
content: cachedContent,
isCached: true,
status: STATUS.LOADED
};
const convertedElement = convert(node);
if (convertedElement && isValidElement(convertedElement)) return {
content: cachedContent,
element: convertedElement,
isCached: true,
status: STATUS.READY
};
} catch {}
return {
...initial,
content: cachedContent,
isCached: true,
status: STATUS.LOADED
};
});
const { content, element, isCached, status } = state;
const previousProps = usePrevious(props);
const previousState = usePrevious(state);
const isActive = useRef(false);
const isInitialized = useRef(false);
const handleError = useCallback((error) => {
if (isActive.current) {
setState({ status: error.message === "Browser does not support SVG" ? STATUS.UNSUPPORTED : STATUS.FAILED });
onErrorRef.current?.(error);
}
}, []);
const getElement = useCallback(() => {
try {
const convertedElement = convert(getNode({
baseURL,
content,
description,
handleError,
hash: hash.current,
preProcessor: preProcessorRef.current,
src,
title,
uniquifyIDs
}));
if (!convertedElement || !isValidElement(convertedElement)) throw new Error("Could not convert the src to a React element");
setState({
element: convertedElement,
status: STATUS.READY
});
} catch (error) {
handleError(error);
}
}, [
baseURL,
content,
description,
handleError,
src,
title,
uniquifyIDs
]);
useMount(() => {
isActive.current = true;
if (!canUseDOM() || isInitialized.current) return;
try {
if (status === STATUS.READY) onLoadRef.current?.(src, isCached);
else if (status === STATUS.IDLE) {
if (!isSupportedEnvironment()) throw new Error("Browser does not support SVG");
if (!src) throw new Error("Missing src");
setState({
content: "",
element: null,
isCached: false,
status: STATUS.LOADING
});
}
} catch (error) {
handleError(error);
}
isInitialized.current = true;
return () => {
isActive.current = false;
};
});
useEffect(() => {
if (!canUseDOM() || !previousProps) return;
if (previousProps.src !== src) {
if (!src) {
handleError(/* @__PURE__ */ new Error("Missing src"));
return;
}
setState({
content: "",
element: null,
isCached: false,
status: STATUS.LOADING
});
}
}, [
handleError,
previousProps,
src
]);
useEffect(() => {
if (status !== STATUS.LOADING) return;
const controller = new AbortController();
let active = true;
(async () => {
try {
const dataURI = /^data:image\/svg[^,]*?(;base64)?,(.*)/.exec(src);
let inlineSrc;
if (dataURI) inlineSrc = dataURI[1] ? window.atob(dataURI[2]) : decodeURIComponent(dataURI[2]);
else if (src.includes("<svg")) inlineSrc = src;
if (inlineSrc) {
if (active) setState({
content: inlineSrc,
isCached: false,
status: STATUS.LOADED
});
return;
}
const fetchParameters = {
...fetchOptionsRef.current,
signal: controller.signal
};
let loadedContent;
let hasCache = false;
if (cacheRequests) {
hasCache = cacheStore.isCached(src);
loadedContent = await cacheStore.get(src, fetchParameters);
} else loadedContent = await request(src, fetchParameters);
if (active) setState({
content: loadedContent,
isCached: hasCache,
status: STATUS.LOADED
});
} catch (error) {
if (active && error.name !== "AbortError") handleError(error);
}
})();
return () => {
active = false;
controller.abort();
};
}, [
cacheRequests,
cacheStore,
handleError,
src,
status
]);
useEffect(() => {
if (status === STATUS.LOADED && content) getElement();
}, [
content,
getElement,
status
]);
useEffect(() => {
if (!canUseDOM() || !previousProps || previousProps.src !== src) return;
if (previousProps.title !== title || previousProps.description !== description) getElement();
}, [
description,
getElement,
previousProps,
src,
title
]);
useEffect(() => {
if (!previousState) return;
if (status === STATUS.READY && previousState.status !== STATUS.READY) onLoadRef.current?.(src, isCached);
}, [
isCached,
previousState,
src,
status
]);
return {
element,
status
};
}
//#endregion
//#region src/index.tsx
const cacheStore = new CacheStore();
function InlineSVG(props) {
const { children = null, innerRef, loader = null } = props;
const { element, status } = useInlineSVG(props, useCacheStore() ?? cacheStore);
if (!canUseDOM()) return loader;
if (element) return cloneElement(element, {
ref: innerRef,
...omit(props, "baseURL", "cacheRequests", "children", "description", "fetchOptions", "innerRef", "loader", "onError", "onLoad", "preProcessor", "src", "title", "uniqueHash", "uniquifyIDs")
});
if ([STATUS.UNSUPPORTED, STATUS.FAILED].includes(status)) return children;
return loader;
}
//#endregion
export { cacheStore, InlineSVG as default };
//# sourceMappingURL=index.mjs.map