UNPKG

react-inlinesvg

Version:
641 lines (630 loc) 19.9 kB
"use client"; "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/index.tsx var index_exports = {}; __export(index_exports, { cacheStore: () => cacheStore, default: () => InlineSVG }); module.exports = __toCommonJS(index_exports); var import_react4 = require("react"); // src/config.ts var CACHE_NAME = "react-inlinesvg"; var CACHE_MAX_RETRIES = 10; var STATUS = { IDLE: "idle", LOADING: "loading", LOADED: "loaded", FAILED: "failed", READY: "ready", UNSUPPORTED: "unsupported" }; // src/modules/helpers.ts function randomCharacter(character) { return character[Math.floor(Math.random() * character.length)]; } function canUseDOM() { return !!(typeof window !== "undefined" && window.document?.createElement); } function isSupportedEnvironment() { return supportsInlineSVG() && typeof window !== "undefined" && window !== null; } function omit(input, ...filter) { const output = {}; for (const key in input) { if ({}.hasOwnProperty.call(input, key)) { if (!filter.includes(key)) { output[key] = input[key]; } } } return output; } function randomString(length) { const letters = "abcdefghijklmnopqrstuvwxyz"; const numbers = "1234567890"; const charset = `${letters}${letters.toUpperCase()}${numbers}`; let R = ""; for (let index = 0; index < length; index++) { R += randomCharacter(charset); } return R; } async function request(url, options) { const response = await fetch(url, options); const contentType = response.headers.get("content-type"); const [fileType] = (contentType ?? "").split(/ ?; ?/); if (response.status > 299) { throw new Error("Not found"); } if (!["image/svg+xml", "text/plain"].some((d) => fileType.includes(d))) { throw new Error(`Content type isn't valid: ${fileType}`); } return response.text(); } function supportsInlineSVG() { if (!document) { return false; } const div = document.createElement("div"); div.innerHTML = "<svg />"; const svg = div.firstChild; return !!svg && svg.namespaceURI === "http://www.w3.org/2000/svg"; } // src/modules/cache.ts var CacheStore = class { constructor(options = {}) { __publicField(this, "cacheApi"); __publicField(this, "cacheStore"); __publicField(this, "subscribers", []); __publicField(this, "isReady", false); const { name = CACHE_NAME, persistent = false } = options; this.cacheStore = /* @__PURE__ */ new Map(); const usePersistentCache = persistent && canUseDOM() && "caches" in window; if (usePersistentCache) { caches.open(name).then((cache) => { this.cacheApi = cache; }).catch((error) => { console.error(`Failed to open cache: ${error.message}`); this.cacheApi = void 0; }).finally(() => { this.isReady = true; const callbacks = [...this.subscribers]; this.subscribers.length = 0; callbacks.forEach((callback) => { try { callback(); } catch (error) { console.error(`Error in CacheStore subscriber callback: ${error.message}`); } }); }); } else { this.isReady = true; } } onReady(callback) { if (this.isReady) { callback(); return () => { }; } this.subscribers.push(callback); return () => { const index = this.subscribers.indexOf(callback); if (index >= 0) { this.subscribers.splice(index, 1); } }; } waitForReady() { if (this.isReady) { return Promise.resolve(); } return new Promise((resolve) => { this.onReady(resolve); }); } async get(url, fetchOptions) { await this.fetchAndCache(url, fetchOptions); return this.cacheStore.get(url)?.content ?? ""; } getContent(url) { return this.cacheStore.get(url)?.content ?? ""; } set(url, data) { this.cacheStore.set(url, data); } isCached(url) { return this.cacheStore.get(url)?.status === STATUS.LOADED; } async fetchAndCache(url, fetchOptions) { if (!this.isReady) { await this.waitForReady(); } const cache = this.cacheStore.get(url); if (cache?.status === STATUS.LOADED) { return; } if (cache?.status === STATUS.LOADING) { await this.handleLoading(url, fetchOptions?.signal || void 0, async () => { this.cacheStore.set(url, { content: "", status: STATUS.IDLE }); await this.fetchAndCache(url, fetchOptions); }); return; } this.cacheStore.set(url, { content: "", status: STATUS.LOADING }); try { const content = this.cacheApi ? await this.fetchFromPersistentCache(url, fetchOptions) : await request(url, fetchOptions); this.cacheStore.set(url, { content, status: STATUS.LOADED }); } catch (error) { this.cacheStore.set(url, { content: "", status: STATUS.FAILED }); throw error; } } async fetchFromPersistentCache(url, fetchOptions) { const data = await this.cacheApi?.match(url); if (data) { return data.text(); } await this.cacheApi?.add(new Request(url, fetchOptions)); const response = await this.cacheApi?.match(url); return await response?.text() ?? ""; } async handleLoading(url, signal, callback) { for (let retryCount = 0; retryCount < CACHE_MAX_RETRIES; retryCount++) { if (signal?.aborted) { throw signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted.", "AbortError"); } if (this.cacheStore.get(url)?.status !== STATUS.LOADING) { return; } await sleep(0.1); } await callback(); } keys() { return [...this.cacheStore.keys()]; } data() { return [...this.cacheStore.entries()].map(([key, value]) => ({ [key]: value })); } async delete(url) { if (this.cacheApi) { await this.cacheApi.delete(url); } this.cacheStore.delete(url); } async clear() { if (this.cacheApi) { const keys = await this.cacheApi.keys(); await Promise.allSettled(keys.map((key) => this.cacheApi.delete(key))); } this.cacheStore.clear(); } }; function sleep(seconds = 1) { return new Promise((resolve) => { setTimeout(resolve, seconds * 1e3); }); } // src/modules/useInlineSVG.ts var import_react2 = require("react"); var import_react_from_dom2 = __toESM(require("react-from-dom")); // src/modules/hooks.tsx var import_react = require("react"); function useMount(effect) { (0, import_react.useEffect)(effect, []); } function usePrevious(state) { const ref = (0, import_react.useRef)(void 0); (0, import_react.useEffect)(() => { ref.current = state; }); return ref.current; } // src/modules/utils.ts var import_react_from_dom = __toESM(require("react-from-dom")); 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 = processSVG(content, preProcessor); if (uniquifyIDs) { svgText = uniquifyStyleIds(svgText, hash, baseURL ?? ""); } const node = (0, import_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 processSVG(content, preProcessor) { if (preProcessor) { return preProcessor(content); } return content; } 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; } // src/modules/useInlineSVG.ts function useInlineSVG(props, cacheStore2) { const { baseURL, cacheRequests = true, description, fetchOptions, onError, onLoad, preProcessor, src, title, uniqueHash, uniquifyIDs } = props; const hash = (0, import_react2.useRef)(uniqueHash ?? randomString(8)); const fetchOptionsRef = (0, import_react2.useRef)(fetchOptions); const onErrorRef = (0, import_react2.useRef)(onError); const onLoadRef = (0, import_react2.useRef)(onLoad); const preProcessorRef = (0, import_react2.useRef)(preProcessor); fetchOptionsRef.current = fetchOptions; onErrorRef.current = onError; onLoadRef.current = onLoad; preProcessorRef.current = preProcessor; const [state, setState] = (0, import_react2.useReducer)( (previousState2, nextState) => ({ ...previousState2, ...nextState }), { content: "", element: null, isCached: false, status: STATUS.IDLE }, (initial) => { const cached = cacheRequests && cacheStore2.isCached(src); if (!cached) { return initial; } const cachedContent = cacheStore2.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 = (0, import_react_from_dom2.default)(node); if (convertedElement && (0, import_react2.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 = (0, import_react2.useRef)(false); const isInitialized = (0, import_react2.useRef)(false); const handleError = (0, import_react2.useCallback)((error) => { if (isActive.current) { setState({ status: error.message === "Browser does not support SVG" ? STATUS.UNSUPPORTED : STATUS.FAILED }); onErrorRef.current?.(error); } }, []); const getElement = (0, import_react2.useCallback)(() => { try { const node = getNode({ baseURL, content, description, handleError, hash: hash.current, preProcessor: preProcessorRef.current, src, title, uniquifyIDs }); const convertedElement = (0, import_react_from_dom2.default)(node); if (!convertedElement || !(0, import_react2.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 void 0; } 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; }; }); (0, import_react2.useEffect)(() => { if (!canUseDOM() || !previousProps) { return; } if (previousProps.src !== src) { if (!src) { handleError(new Error("Missing src")); return; } setState({ content: "", element: null, isCached: false, status: STATUS.LOADING }); } }, [handleError, previousProps, src]); (0, import_react2.useEffect)(() => { if (status !== STATUS.LOADING) { return void 0; } 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 = cacheStore2.isCached(src); loadedContent = await cacheStore2.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, cacheStore2, handleError, src, status]); (0, import_react2.useEffect)(() => { if (status === STATUS.LOADED && content) { getElement(); } }, [content, getElement, status]); (0, import_react2.useEffect)(() => { if (!canUseDOM() || !previousProps || previousProps.src !== src) { return; } if (previousProps.title !== title || previousProps.description !== description) { getElement(); } }, [description, getElement, previousProps, src, title]); (0, import_react2.useEffect)(() => { if (!previousState) { return; } if (status === STATUS.READY && previousState.status !== STATUS.READY) { onLoadRef.current?.(src, isCached); } }, [isCached, previousState, src, status]); return { element, status }; } // src/provider.tsx var import_react3 = __toESM(require("react")); var CacheContext = (0, import_react3.createContext)(null); function useCacheStore() { return (0, import_react3.useContext)(CacheContext); } // src/index.tsx var cacheStore = new CacheStore(); function InlineSVG(props) { const { children = null, innerRef, loader = null } = props; const contextStore = useCacheStore(); const store = contextStore ?? cacheStore; const { element, status } = useInlineSVG(props, store); if (!canUseDOM()) { return loader; } if (element) { return (0, import_react4.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; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { cacheStore }); //# sourceMappingURL=index.js.map // fix-cjs-exports if (module.exports.default) { Object.assign(module.exports.default, module.exports); module.exports = module.exports.default; delete module.exports.default; }