@iconfu/svg-inject
Version:
A tiny, intuitive, robust, caching solution for injecting SVG files inline into the DOM.
549 lines (543 loc) • 18.4 kB
JavaScript
;
var SVGInjectModule = (() => {
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
SVGInject: () => SVGInject,
createSVGInject: () => createSVGInject,
default: () => SVGInject
});
// src/copy-attributes.ts
var SVG_CASE_MAP = {
viewbox: "viewBox",
preserveaspectratio: "preserveAspectRatio",
basefrequency: "baseFrequency",
baseprofile: "baseProfile",
calcmode: "calcMode",
clippathunits: "clipPathUnits",
diffuseconstant: "diffuseConstant",
edgemode: "edgeMode",
filterunits: "filterUnits",
glyphref: "glyphRef",
gradienttransform: "gradientTransform",
gradientunits: "gradientUnits",
kernelmatrix: "kernelMatrix",
kernelunitlength: "kernelUnitLength",
keypoints: "keyPoints",
keysplines: "keySplines",
keytimes: "keyTimes",
lengthadjust: "lengthAdjust",
limitingconeangle: "limitingConeAngle",
markerheight: "markerHeight",
markerunits: "markerUnits",
markerwidth: "markerWidth",
maskcontentunits: "maskContentUnits",
maskunits: "maskUnits",
numoctaves: "numOctaves",
pathlength: "pathLength",
patterncontentunits: "patternContentUnits",
patterntransform: "patternTransform",
patternunits: "patternUnits",
pointsatx: "pointsAtX",
pointsaty: "pointsAtY",
pointsatz: "pointsAtZ",
primitiveunits: "primitiveUnits",
refx: "refX",
refy: "refY",
repeatcount: "repeatCount",
repeatdur: "repeatDur",
requiredextensions: "requiredExtensions",
requiredfeatures: "requiredFeatures",
specularconstant: "specularConstant",
specularexponent: "specularExponent",
spreadmethod: "spreadMethod",
startoffset: "startOffset",
stddeviation: "stdDeviation",
stitchtiles: "stitchTiles",
surfacescale: "surfaceScale",
systemlanguage: "systemLanguage",
tablevalues: "tableValues",
targetx: "targetX",
targety: "targetY",
textlength: "textLength",
xchannelselector: "xChannelSelector",
ychannelselector: "yChannelSelector",
zoomandpan: "zoomAndPan"
};
var EXCLUDED_ATTRIBUTES = /* @__PURE__ */ new Set(["src", "alt", "onload", "onerror"]);
function mergeStyles(svgStyle, imgStyle) {
const declarations = /* @__PURE__ */ new Map();
for (const style of [svgStyle, imgStyle]) {
if (!style) continue;
for (const decl of style.split(";")) {
const colon = decl.indexOf(":");
if (colon === -1) continue;
const prop = decl.substring(0, colon).trim();
const val = decl.substring(colon + 1).trim();
if (prop) {
declarations.set(prop, val);
}
}
}
if (declarations.size === 0) return "";
return Array.from(declarations.entries()).map(([prop, val]) => `${prop}: ${val}`).join("; ");
}
function copyAttributes(imgElem, svgElem) {
const alt = imgElem.getAttribute("alt");
if (alt === "") {
svgElem.setAttribute("role", "none");
svgElem.setAttribute("aria-hidden", "true");
} else {
svgElem.setAttribute("role", "img");
if (alt !== null) {
svgElem.setAttribute("aria-label", alt);
}
}
const attributes = imgElem.attributes;
for (let i = 0; i < attributes.length; i++) {
const attribute = attributes[i];
let attributeName = attribute.name;
if (EXCLUDED_ATTRIBUTES.has(attributeName)) continue;
const attributeValue = attribute.value;
if (attributeName === "title") {
const firstChild = svgElem.firstElementChild;
let titleElem;
if (firstChild && firstChild.localName.toLowerCase() === "title") {
titleElem = firstChild;
} else {
titleElem = (svgElem.ownerDocument || document).createElementNS("http://www.w3.org/2000/svg", "title");
svgElem.insertBefore(titleElem, firstChild);
}
titleElem.textContent = attributeValue;
} else if (attributeName === "style") {
const svgStyle = svgElem.getAttribute("style") || "";
const merged = mergeStyles(svgStyle, attributeValue);
if (merged) {
svgElem.setAttribute("style", merged);
}
} else {
attributeName = SVG_CASE_MAP[attributeName] || attributeName;
svgElem.setAttribute(attributeName, attributeValue);
}
}
}
// src/make-ids-unique.ts
var ID_SUFFIX = "--inject-";
var IRI_TAG_PROPERTIES_MAP = {
clipPath: ["clip-path"],
"color-profile": null,
cursor: null,
filter: null,
linearGradient: ["fill", "stroke"],
marker: ["marker", "marker-end", "marker-mid", "marker-start"],
mask: null,
pattern: ["fill", "stroke"],
radialGradient: ["fill", "stroke"]
};
var ARIA_ID_REF_ATTRIBUTES = [
"aria-labelledby",
"aria-describedby",
"aria-controls",
"aria-owns",
"aria-flowto",
"aria-activedescendant",
"aria-errormessage",
"aria-details"
];
var FUNC_IRI_REGEX = /url\(["']?#([a-zA-Z][\w:.-]*)["']?\)/g;
var uniqueIdCounter = 1;
function makeIdsUnique(svgElem) {
const idSuffix = ID_SUFFIX + uniqueIdCounter++;
const idElements = svgElem.querySelectorAll("[id]");
if (idElements.length === 0) return false;
const existingIds = /* @__PURE__ */ new Set();
const iriTagNames = /* @__PURE__ */ new Set();
for (let i = 0; i < idElements.length; i++) {
existingIds.add(idElements[i].id);
const tagName = idElements[i].localName;
if (tagName in IRI_TAG_PROPERTIES_MAP) {
iriTagNames.add(tagName);
}
}
const iriProperties = [];
for (const tagName of iriTagNames) {
const mapped = IRI_TAG_PROPERTIES_MAP[tagName] || [tagName];
for (const prop of mapped) {
if (!iriProperties.includes(prop)) {
iriProperties.push(prop);
}
}
}
if (iriProperties.length > 0) {
iriProperties.push("style");
}
const replaceIriRef = (_match, id) => {
return "url(#" + id + idSuffix + ")";
};
const descendants = svgElem.getElementsByTagName("*");
for (let i = -1; i < descendants.length; i++) {
const element = i === -1 ? svgElem : descendants[i];
if (element.localName === "style") {
const text = element.textContent;
if (text) {
const newText = text.replace(FUNC_IRI_REGEX, replaceIriRef);
if (newText !== text) {
element.textContent = newText;
}
}
} else if (element.hasAttributes()) {
for (const propertyName of iriProperties) {
const value = element.getAttribute(propertyName);
if (value) {
const newValue = value.replace(FUNC_IRI_REGEX, replaceIriRef);
if (newValue !== value) {
element.setAttribute(propertyName, newValue);
}
}
}
for (const refAttr of ["xlink:href", "href"]) {
const iri = element.getAttribute(refAttr);
if (iri && /^\s*#/.test(iri)) {
element.setAttribute(refAttr, iri.trim() + idSuffix);
}
}
for (const ariaAttr of ARIA_ID_REF_ATTRIBUTES) {
const value = element.getAttribute(ariaAttr);
if (value) {
const ids = value.split(/\s+/);
const newIds = ids.map((id) => existingIds.has(id) ? id + idSuffix : id);
const newValue = newIds.join(" ");
if (newValue !== value) {
element.setAttribute(ariaAttr, newValue);
}
}
}
}
}
for (let i = 0; i < idElements.length; i++) {
idElements[i].id += idSuffix;
}
return true;
}
// src/sanitize.ts
var DANGEROUS_ELEMENTS = /* @__PURE__ */ new Set([
"script",
"foreignobject"
]);
var DANGEROUS_URI = /^\s*(javascript|data|vbscript)\s*:/i;
function sanitizeSvg(svgElem) {
var _a;
const allElements = svgElem.getElementsByTagName("*");
const toRemove = [];
for (let i = 0; i < allElements.length; i++) {
if (DANGEROUS_ELEMENTS.has(allElements[i].localName.toLowerCase())) {
toRemove.push(allElements[i]);
}
}
for (const el of toRemove) {
(_a = el.parentNode) == null ? void 0 : _a.removeChild(el);
}
const remaining = svgElem.getElementsByTagName("*");
for (let i = -1; i < remaining.length; i++) {
const el = i === -1 ? svgElem : remaining[i];
if (!el.hasAttributes()) continue;
const attrsToRemove = [];
for (let j = 0; j < el.attributes.length; j++) {
const attr = el.attributes[j];
const name = attr.name.toLowerCase();
if (name.startsWith("on")) {
attrsToRemove.push(attr.name);
continue;
}
if ((name === "href" || name === "xlink:href") && DANGEROUS_URI.test(attr.value)) {
attrsToRemove.push(attr.name);
}
}
for (const attrName of attrsToRemove) {
el.removeAttribute(attrName);
}
}
}
// src/svg-inject.ts
var LOAD_FAIL = "LOAD_FAIL";
var SVG_NOT_SUPPORTED = "SVG_NOT_SUPPORTED";
var SVG_INVALID = "SVG_INVALID";
var __SVGINJECT = "__svgInject";
var INJECTED = 1;
var FAIL = 2;
var DEFAULT_OPTIONS = {
useCache: true,
copyAttributes: true,
makeIdsUnique: true,
sanitize: false,
injectStyleTag: true
};
var domParser = null;
var xmlSerializer = null;
var aElement = null;
function getAbsoluteUrl(url) {
aElement = aElement || document.createElement("a");
aElement.href = url;
return aElement.href;
}
function svgStringToSvgDoc(svgStr) {
domParser = domParser || new DOMParser();
return domParser.parseFromString(svgStr, "text/xml");
}
function svgElemToSvgString(svgElement) {
xmlSerializer = xmlSerializer || new XMLSerializer();
return xmlSerializer.serializeToString(svgElement);
}
function buildSvgElement(svgStr, verify) {
let svgDoc;
try {
svgDoc = svgStringToSvgDoc(svgStr);
} catch (e) {
return null;
}
if (verify && svgDoc.getElementsByTagName("parsererror").length) {
return null;
}
return document.adoptNode(svgDoc.documentElement);
}
async function loadSvg(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load SVG: ${response.status}`);
}
return (await response.text()).trim();
}
function mergeOptions(base, override) {
if (!override) return base;
return { ...base, ...override };
}
function addStyleToHead(css) {
const head = document.getElementsByTagName("head")[0];
if (head) {
const style = document.createElement("style");
style.appendChild(document.createTextNode(css));
head.appendChild(style);
}
}
function isSvgElement(el) {
return el != null && typeof el === "object" && "localName" in el && el.localName === "svg";
}
function errorMessage(msg) {
console.error("SVGInject: " + msg);
}
function createSVGInject(globalName, options, _assignToWindow = true) {
let defaultOptions = mergeOptions({ ...DEFAULT_OPTIONS }, options);
const svgLoadCache = /* @__PURE__ */ new Map();
if (defaultOptions.injectStyleTag && typeof document !== "undefined") {
addStyleToHead('img[onload^="' + globalName + '("]{visibility:hidden;}');
}
function fail(imgElem, status, opts) {
imgElem[__SVGINJECT] = FAIL;
if (opts.onFail) {
opts.onFail(imgElem, status);
} else {
errorMessage(status);
}
}
function inject(imgElem, svgElem, absUrl, opts) {
if (!svgElem) {
imgElem.removeAttribute("onload");
fail(imgElem, SVG_INVALID, opts);
return;
}
svgElem.setAttribute("data-inject-url", absUrl);
const parentNode = imgElem.parentNode;
if (!parentNode) return;
if (opts.copyAttributes) {
copyAttributes(imgElem, svgElem);
}
const injectElem = opts.beforeInject && opts.beforeInject(imgElem, svgElem) || svgElem;
parentNode.replaceChild(injectElem, imgElem);
imgElem[__SVGINJECT] = INJECTED;
imgElem.removeAttribute("onload");
if (opts.afterInject) {
opts.afterInject(imgElem, injectElem);
}
}
async function injectElement(imgElem, opts) {
if (!imgElem) {
errorMessage("no img element");
return;
}
const svgInjectValue = imgElem[__SVGINJECT];
if (!svgInjectValue) {
imgElem.onload = null;
imgElem.onerror = null;
}
if (svgInjectValue) {
if (svgInjectValue instanceof Promise) {
await svgInjectValue;
}
return;
}
if (typeof SVGRect === "undefined") {
imgElem.removeAttribute("onload");
fail(imgElem, SVG_NOT_SUPPORTED, opts);
return;
}
const beforeLoad = opts.beforeLoad;
const src = beforeLoad && beforeLoad(imgElem) || imgElem.getAttribute("src");
if (src === null || src === void 0) {
return;
}
if (src === "") {
fail(imgElem, LOAD_FAIL, opts);
return;
}
const absUrl = getAbsoluteUrl(src);
const useCache = opts.useCache;
const doMakeIdsUnique = opts.makeIdsUnique;
const { promise, resolve } = createDeferred();
imgElem[__SVGINJECT] = promise;
try {
let svgString;
if (useCache) {
svgString = await getCachedSvg(absUrl, opts);
} else {
svgString = await loadAndProcess(absUrl, opts);
}
let svgElem = buildSvgElement(svgString, false);
if (svgElem) {
if (opts.sanitize) sanitizeSvg(svgElem);
if (doMakeIdsUnique) makeIdsUnique(svgElem);
}
inject(imgElem, svgElem, absUrl, opts);
} catch (err) {
const status = err instanceof SvgError ? err.status : LOAD_FAIL;
imgElem.removeAttribute("onload");
fail(imgElem, status, opts);
} finally {
resolve();
}
}
async function loadAndProcess(absUrl, opts) {
let svgString;
try {
svgString = await loadSvg(absUrl);
} catch (e) {
throw new SvgError(LOAD_FAIL);
}
const svgElem = buildSvgElement(svgString, true);
if (!svgElem || !isSvgElement(svgElem)) {
throw new SvgError(SVG_INVALID);
}
if (opts.afterLoad) {
const result = opts.afterLoad(svgElem, svgString);
if (result) {
if (typeof result === "string") {
svgString = result;
} else {
svgString = svgElemToSvgString(result);
}
} else {
svgString = svgElemToSvgString(svgElem);
}
}
return svgString;
}
async function getCachedSvg(absUrl, opts) {
const cached = svgLoadCache.get(absUrl);
if (cached) {
if (cached.type === "loaded") {
return cached.svgString;
}
if (cached.type === "failed") {
throw new SvgError(cached.status);
}
return new Promise((resolve, reject) => {
cached.callbacks.push((entry) => {
if (entry.type === "loaded") resolve(entry.svgString);
else if (entry.type === "failed") reject(new SvgError(entry.status));
});
});
}
const pending = { type: "pending", callbacks: [] };
svgLoadCache.set(absUrl, pending);
try {
const svgString = await loadAndProcess(absUrl, opts);
const loaded = { type: "loaded", svgString };
svgLoadCache.set(absUrl, loaded);
pending.callbacks.forEach((cb) => cb(loaded));
return svgString;
} catch (err) {
const status = err instanceof SvgError ? err.status : LOAD_FAIL;
const failed = { type: "failed", status };
svgLoadCache.set(absUrl, failed);
pending.callbacks.forEach((cb) => cb(failed));
throw err;
}
}
function SVGInject2(img, options2) {
const opts = mergeOptions(defaultOptions, options2);
const elements = img instanceof HTMLImageElement ? [img] : Array.from(img);
const promises = elements.map((el) => injectElement(el, opts));
return Promise.all(promises).then(() => {
if (opts.onAllFinish) opts.onAllFinish();
});
}
SVGInject2.setOptions = function(options2) {
defaultOptions = mergeOptions(defaultOptions, options2);
};
SVGInject2.create = createSVGInject;
SVGInject2.err = function(img, fallbackSrc) {
if (!img) {
errorMessage("no img element");
return;
}
if (img[__SVGINJECT] === FAIL) return;
img.onload = null;
img.onerror = null;
img.removeAttribute("onload");
if (typeof SVGRect === "undefined") {
fail(img, SVG_NOT_SUPPORTED, defaultOptions);
} else {
fail(img, LOAD_FAIL, defaultOptions);
}
if (fallbackSrc) {
img.src = fallbackSrc;
}
};
if (_assignToWindow && typeof window !== "undefined") {
window[globalName] = SVGInject2;
}
return SVGInject2;
}
var SvgError = class extends Error {
constructor(status) {
super(status);
this.status = status;
}
};
function createDeferred() {
let resolve;
const promise = new Promise((r) => {
resolve = r;
});
return { promise, resolve };
}
var SVGInject = createSVGInject("SVGInject", void 0, false);
return __toCommonJS(src_exports);
})();
if(typeof window!=="undefined"&&SVGInjectModule&&SVGInjectModule.SVGInject){window.SVGInject=SVGInjectModule.SVGInject;}