react-inlinesvg
Version:
An SVG loader for React
333 lines (332 loc) • 10.6 kB
JavaScript
"use client";
Object.defineProperties(exports, {
__esModule: { value: true },
[Symbol.toStringTag]: { value: "Module" }
});
const require_provider = require("./provider-B5LIZTJG.cjs");
let react = require("react");
let react_from_dom = require("react-from-dom");
react_from_dom = require_provider.__toESM(react_from_dom);
//#region src/modules/hooks.tsx
function useMount(effect) {
(0, react.useEffect)(effect, []);
}
function usePrevious(state) {
const ref = (0, react.useRef)(void 0);
(0, react.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 = (0, react_from_dom.default)(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 = (0, react.useRef)(uniqueHash ?? require_provider.randomString(8));
const fetchOptionsRef = (0, react.useRef)(fetchOptions);
const onErrorRef = (0, react.useRef)(onError);
const onLoadRef = (0, react.useRef)(onLoad);
const preProcessorRef = (0, react.useRef)(preProcessor);
fetchOptionsRef.current = fetchOptions;
onErrorRef.current = onError;
onLoadRef.current = onLoad;
preProcessorRef.current = preProcessor;
const [state, setState] = (0, react.useReducer)((previousState, nextState) => ({
...previousState,
...nextState
}), {
content: "",
element: null,
isCached: false,
status: require_provider.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: require_provider.STATUS.LOADED
};
const convertedElement = (0, react_from_dom.default)(node);
if (convertedElement && (0, react.isValidElement)(convertedElement)) return {
content: cachedContent,
element: convertedElement,
isCached: true,
status: require_provider.STATUS.READY
};
} catch {}
return {
...initial,
content: cachedContent,
isCached: true,
status: require_provider.STATUS.LOADED
};
});
const { content, element, isCached, status } = state;
const previousProps = usePrevious(props);
const previousState = usePrevious(state);
const isActive = (0, react.useRef)(false);
const isInitialized = (0, react.useRef)(false);
const handleError = (0, react.useCallback)((error) => {
if (isActive.current) {
setState({ status: error.message === "Browser does not support SVG" ? require_provider.STATUS.UNSUPPORTED : require_provider.STATUS.FAILED });
onErrorRef.current?.(error);
}
}, []);
const getElement = (0, react.useCallback)(() => {
try {
const convertedElement = (0, react_from_dom.default)(getNode({
baseURL,
content,
description,
handleError,
hash: hash.current,
preProcessor: preProcessorRef.current,
src,
title,
uniquifyIDs
}));
if (!convertedElement || !(0, react.isValidElement)(convertedElement)) throw new Error("Could not convert the src to a React element");
setState({
element: convertedElement,
status: require_provider.STATUS.READY
});
} catch (error) {
handleError(error);
}
}, [
baseURL,
content,
description,
handleError,
src,
title,
uniquifyIDs
]);
useMount(() => {
isActive.current = true;
if (!require_provider.canUseDOM() || isInitialized.current) return;
try {
if (status === require_provider.STATUS.READY) onLoadRef.current?.(src, isCached);
else if (status === require_provider.STATUS.IDLE) {
if (!require_provider.isSupportedEnvironment()) throw new Error("Browser does not support SVG");
if (!src) throw new Error("Missing src");
setState({
content: "",
element: null,
isCached: false,
status: require_provider.STATUS.LOADING
});
}
} catch (error) {
handleError(error);
}
isInitialized.current = true;
return () => {
isActive.current = false;
};
});
(0, react.useEffect)(() => {
if (!require_provider.canUseDOM() || !previousProps) return;
if (previousProps.src !== src) {
if (!src) {
handleError(/* @__PURE__ */ new Error("Missing src"));
return;
}
setState({
content: "",
element: null,
isCached: false,
status: require_provider.STATUS.LOADING
});
}
}, [
handleError,
previousProps,
src
]);
(0, react.useEffect)(() => {
if (status !== require_provider.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: require_provider.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 require_provider.request(src, fetchParameters);
if (active) setState({
content: loadedContent,
isCached: hasCache,
status: require_provider.STATUS.LOADED
});
} catch (error) {
if (active && error.name !== "AbortError") handleError(error);
}
})();
return () => {
active = false;
controller.abort();
};
}, [
cacheRequests,
cacheStore,
handleError,
src,
status
]);
(0, react.useEffect)(() => {
if (status === require_provider.STATUS.LOADED && content) getElement();
}, [
content,
getElement,
status
]);
(0, react.useEffect)(() => {
if (!require_provider.canUseDOM() || !previousProps || previousProps.src !== src) return;
if (previousProps.title !== title || previousProps.description !== description) getElement();
}, [
description,
getElement,
previousProps,
src,
title
]);
(0, react.useEffect)(() => {
if (!previousState) return;
if (status === require_provider.STATUS.READY && previousState.status !== require_provider.STATUS.READY) onLoadRef.current?.(src, isCached);
}, [
isCached,
previousState,
src,
status
]);
return {
element,
status
};
}
//#endregion
//#region src/index.tsx
const cacheStore = new require_provider.CacheStore();
function InlineSVG(props) {
const { children = null, innerRef, loader = null } = props;
const { element, status } = useInlineSVG(props, require_provider.useCacheStore() ?? cacheStore);
if (!require_provider.canUseDOM()) return loader;
if (element) return (0, react.cloneElement)(element, {
ref: innerRef,
...require_provider.omit(props, "baseURL", "cacheRequests", "children", "description", "fetchOptions", "innerRef", "loader", "onError", "onLoad", "preProcessor", "src", "title", "uniqueHash", "uniquifyIDs")
});
if ([require_provider.STATUS.UNSUPPORTED, require_provider.STATUS.FAILED].includes(status)) return children;
return loader;
}
//#endregion
exports.cacheStore = cacheStore;
exports.default = InlineSVG;
//# sourceMappingURL=index.cjs.map