UNPKG

react-inlinesvg

Version:
327 lines (326 loc) 9.77 kB
"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