tldraw
Version:
A tiny little drawing editor.
600 lines (599 loc) • 13.8 kB
JavaScript
;
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);
var sanitizeSvg_exports = {};
__export(sanitizeSvg_exports, {
sanitizeSvg: () => sanitizeSvg
});
module.exports = __toCommonJS(sanitizeSvg_exports);
/*!
* SVG/attribute allowlists and URI sanitization approach derived from DOMPurify.
* DOMPurify — MIT License, Copyright (c) 2015 Mario Heiderich
* https://github.com/cure53/DOMPurify/blob/main/LICENSE
*/
const ALLOWED_SVG_TAGS = /* @__PURE__ */ new Set([
"svg",
"a",
"altglyph",
"altglyphdef",
"altglyphitem",
"animate",
"animatecolor",
"animatemotion",
"animatetransform",
"circle",
"clippath",
"defs",
"desc",
"ellipse",
"feblend",
"fecolormatrix",
"fecomponenttransfer",
"fecomposite",
"feconvolvematrix",
"fediffuselighting",
"fedisplacementmap",
"fedistantlight",
"fedropshadow",
"feflood",
"fefunca",
"fefuncb",
"fefuncg",
"fefuncr",
"fegaussianblur",
"feimage",
"femerge",
"femergenode",
"femorphology",
"feoffset",
"fepointlight",
"fespecularlighting",
"fespotlight",
"fetile",
"feturbulence",
"filter",
"font",
"foreignobject",
"g",
"glyph",
"glyphref",
"hkern",
"image",
"line",
"lineargradient",
"marker",
"mask",
"metadata",
"mpath",
"path",
"pattern",
"polygon",
"polyline",
"radialgradient",
"rect",
"set",
"stop",
"style",
"switch",
"symbol",
"text",
"textpath",
"title",
"tref",
"tspan",
"use",
"view",
"vkern"
]);
const ALLOWED_HTML_TAGS = /* @__PURE__ */ new Set([
"a",
"b",
"blockquote",
"body",
"br",
"code",
"del",
"div",
"em",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"i",
"li",
"mark",
"ol",
"p",
"pre",
"span",
"strong",
"s",
"sub",
"sup",
"table",
"tbody",
"td",
"th",
"thead",
"tr",
"u",
"ul"
]);
const BLOCKED_HTML_TAGS = /* @__PURE__ */ new Set([
"script",
"iframe",
"object",
"embed",
"form",
"input",
"textarea",
"select",
"button",
"link",
"meta",
"base",
"img",
// onerror vector
"video",
"audio",
"source",
"picture",
"svg"
// no nested SVG inside foreignObject
]);
const ALLOWED_SVG_ATTRS = /* @__PURE__ */ new Set([
"accent-height",
"accumulate",
"additive",
"alignment-baseline",
"amplitude",
"ascent",
"attributename",
"attributetype",
"azimuth",
"basefrequency",
"baseline-shift",
"begin",
"bias",
"by",
"class",
"clip",
"clip-path",
"clip-rule",
"clippathunits",
"color",
"color-interpolation",
"color-interpolation-filters",
"color-profile",
"color-rendering",
"cx",
"cy",
"d",
"diffuseconstant",
"direction",
"display",
"divisor",
"dominant-baseline",
"dur",
"dx",
"dy",
"edgemode",
"elevation",
"end",
"exponent",
"fill",
"fill-opacity",
"fill-rule",
"filter",
"filterunits",
"flood-color",
"flood-opacity",
"font-family",
"font-size",
"font-size-adjust",
"font-stretch",
"font-style",
"font-variant",
"font-weight",
"from",
"fx",
"fy",
"g1",
"g2",
"glyph-name",
"glyphref",
"gradienttransform",
"gradientunits",
"height",
"href",
"id",
"image-rendering",
"in",
"in2",
"intercept",
"k",
"k1",
"k2",
"k3",
"k4",
"kerning",
"kernelmatrix",
"kernelunitlength",
"keypoints",
"keysplines",
"keytimes",
"lang",
"lengthadjust",
"letter-spacing",
"lighting-color",
"local",
"marker-end",
"marker-mid",
"marker-start",
"markerheight",
"markerunits",
"markerwidth",
"mask",
"mask-type",
"maskcontentunits",
"maskunits",
"max",
"media",
"method",
"min",
"mode",
"name",
"numoctaves",
"offset",
"opacity",
"operator",
"order",
"orient",
"orientation",
"origin",
"overflow",
"paint-order",
"path",
"pathlength",
"patterncontentunits",
"patterntransform",
"patternunits",
"pointer-events",
"points",
"preservealpha",
"preserveaspectratio",
"primitiveunits",
"r",
"radius",
"refx",
"refy",
"repeatcount",
"repeatdur",
"requiredfeatures",
"restart",
"result",
"role",
"rotate",
"rx",
"ry",
"scale",
"seed",
"shape-rendering",
"slope",
"specularconstant",
"specularexponent",
"spreadmethod",
"startoffset",
"stddeviation",
"stitchtiles",
"stop-color",
"stop-opacity",
"stroke",
"stroke-dasharray",
"stroke-dashoffset",
"stroke-linecap",
"stroke-linejoin",
"stroke-miterlimit",
"stroke-opacity",
"stroke-width",
"style",
"surfacescale",
"systemlanguage",
"tabindex",
"tablevalues",
"targetx",
"targety",
"text-anchor",
"text-decoration",
"text-rendering",
"textlength",
"to",
"transform",
"transform-origin",
"type",
"u1",
"u2",
"unicode",
"values",
"version",
"vert-adv-y",
"vert-origin-x",
"vert-origin-y",
"viewbox",
"visibility",
"width",
"word-spacing",
"wrap",
"writing-mode",
"x",
"x1",
"x2",
"xchannelselector",
"xlink:href",
"xml:id",
"xml:space",
"xlink:title",
"xmlns",
"xmlns:xlink",
"y",
"y1",
"y2",
"z",
"zoomandpan"
]);
const ALLOWED_HTML_ATTRS = /* @__PURE__ */ new Set([
"class",
"dir",
"href",
// only on <a>
"id",
"lang",
"role",
"style",
"tabindex",
"title"
]);
const DATA_ATTR_PATTERN = /^data-/;
const ARIA_ATTR_PATTERN = /^aria-/;
const INVISIBLE_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g;
const SAFE_LINK_PROTOCOLS = /^(?:https?:|mailto:)/i;
const DATA_URI = /^data:/i;
const RASTER_DATA_URI = /^data:image\/(?:png|jpeg|jpg|gif|webp|avif|bmp|tiff|x-icon|vnd\.microsoft\.icon)[;,]/i;
const SVG_DATA_URI = /^data:image\/svg\+xml[;,]/i;
const FRAGMENT_REF = /^#/;
function decodeCssEscapes(css) {
return css.replace(/\\([0-9a-fA-F]{1,6})\s?|\\([^\n])/g, (_, hex, literal) => {
if (hex) {
const codePoint = parseInt(hex, 16);
if (codePoint > 1114111 || codePoint === 0) return "\uFFFD";
return String.fromCodePoint(codePoint);
}
return literal;
});
}
const SAFE_CSS_DATA_MIME = /^data:(?:image\/(?:png|jpeg|jpg|gif|webp|avif)|font\/(?:woff2?|opentype|truetype|sfnt)|application\/(?:x-font-woff|font-woff2?|x-font-ttf|x-font-opentype|font-sfnt))[;,]/i;
function sanitizeCssValue(css) {
let decoded = decodeCssEscapes(css);
decoded = decoded.replace(
/@import\s+(?:url\s*\([^)]*\)|"[^"]*"|'[^']*')[^;]*;?|@import\b[^;]*;?/gi,
""
);
decoded = decoded.replace(/expression\s*\([^)]*\)/gi, "");
decoded = decoded.replace(/-moz-binding\s*:[^;]*/gi, "");
decoded = decoded.replace(/behavior\s*:[^;]*/gi, "");
decoded = decoded.replace(/url\s*\(\s*(['"]?)(.*?)\1\s*\)/gis, (match, _quote, uri) => {
const stripped = uri.replace(INVISIBLE_WHITESPACE, "");
if (SAFE_CSS_DATA_MIME.test(stripped) || FRAGMENT_REF.test(stripped)) {
return match;
}
return "";
});
return decoded;
}
function sanitizeStyleElement(textContent) {
return sanitizeCssValue(textContent);
}
const ANIMATION_TAGS = /* @__PURE__ */ new Set(["animate", "set", "animatecolor", "animatetransform"]);
const DANGEROUS_ANIMATION_TARGETS = /^(?:href|xlink:href|on)/i;
function isAnimationDangerous(el) {
const attrName = el.getAttribute("attributeName");
if (!attrName) return false;
return DANGEROUS_ANIMATION_TARGETS.test(attrName.replace(INVISIBLE_WHITESPACE, ""));
}
const EVENT_HANDLER_PATTERN = /^on/i;
const URL_BEARING_SVG_ATTRS = /* @__PURE__ */ new Set([
"clip-path",
"cursor",
"fill",
"filter",
"marker-end",
"marker-mid",
"marker-start",
"mask",
"stroke"
]);
const MAX_EMBED_DEPTH = 10;
function decodeDataUri(value) {
const base64Idx = value.search(/;base64,/i);
if (base64Idx >= 0) {
const base64 = value.slice(base64Idx + 8);
const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
const commaIdx = value.indexOf(",");
if (commaIdx < 0) return null;
return decodeURIComponent(value.slice(commaIdx + 1));
}
function encodeAsSvgDataUri(svgText) {
const bytes = new TextEncoder().encode(svgText);
const binaryStr = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
return "data:image/svg+xml;base64," + btoa(binaryStr);
}
function sanitizeEmbeddedSvgDataUri(value, depth) {
if (depth >= MAX_EMBED_DEPTH) {
console.warn(`Embedded SVG data URI recursion depth limit (${MAX_EMBED_DEPTH}) reached`);
return null;
}
let svgText;
try {
const decoded = decodeDataUri(value);
if (decoded === null) return null;
svgText = decoded;
} catch {
return null;
}
const sanitized = sanitizeSvgInner(svgText, depth + 1);
if (!sanitized) return null;
return encodeAsSvgDataUri(sanitized);
}
function sanitizeUri(el, attrName, value, depth) {
const stripped = value.replace(INVISIBLE_WHITESPACE, "");
const tagName = el.tagName.toLowerCase();
if (tagName === "image" || tagName === "feimage") {
if (RASTER_DATA_URI.test(stripped)) return value;
if (SVG_DATA_URI.test(stripped)) return sanitizeEmbeddedSvgDataUri(stripped, depth);
return null;
}
if (tagName === "use") {
if (FRAGMENT_REF.test(stripped)) return value;
return null;
}
if (tagName === "a") {
if (SAFE_LINK_PROTOCOLS.test(stripped)) return value;
return null;
}
if (DATA_URI.test(stripped) || FRAGMENT_REF.test(stripped)) return value;
return null;
}
function sanitizeSvgAttributes(el, depth) {
for (let i = el.attributes.length - 1; i >= 0; i--) {
const attr = el.attributes[i];
const name = attr.name.toLowerCase();
const normalized = name.replace(INVISIBLE_WHITESPACE, "");
if (EVENT_HANDLER_PATTERN.test(normalized)) {
el.removeAttribute(attr.name);
continue;
}
if (DATA_ATTR_PATTERN.test(name) || ARIA_ATTR_PATTERN.test(name)) {
continue;
}
if (!ALLOWED_SVG_ATTRS.has(name)) {
el.removeAttribute(attr.name);
continue;
}
if (name === "href" || name === "xlink:href") {
const sanitized = sanitizeUri(el, name, attr.value, depth);
if (sanitized === null) {
el.removeAttribute(attr.name);
} else if (sanitized !== attr.value) {
attr.value = sanitized;
}
continue;
}
if (name === "style") {
attr.value = sanitizeCssValue(attr.value);
continue;
}
if (URL_BEARING_SVG_ATTRS.has(name) && /url\s*\(/i.test(attr.value)) {
attr.value = sanitizeCssValue(attr.value);
}
}
}
function sanitizeHtmlAttributes(el) {
const tagName = el.tagName.toLowerCase();
for (let i = el.attributes.length - 1; i >= 0; i--) {
const attr = el.attributes[i];
const name = attr.name.toLowerCase();
const normalized = name.replace(INVISIBLE_WHITESPACE, "");
if (EVENT_HANDLER_PATTERN.test(normalized)) {
el.removeAttribute(attr.name);
continue;
}
if (DATA_ATTR_PATTERN.test(name) || ARIA_ATTR_PATTERN.test(name)) {
continue;
}
if (!ALLOWED_HTML_ATTRS.has(name)) {
el.removeAttribute(attr.name);
continue;
}
if (name === "href") {
if (tagName !== "a") {
el.removeAttribute(attr.name);
continue;
}
const stripped = attr.value.replace(INVISIBLE_WHITESPACE, "");
if (!SAFE_LINK_PROTOCOLS.test(stripped)) {
el.removeAttribute(attr.name);
}
continue;
}
if (name === "style") {
attr.value = sanitizeCssValue(attr.value);
}
}
}
function sanitizeNode(node, mode, depth) {
for (let i = node.children.length - 1; i >= 0; i--) {
const child = node.children[i];
const tag = child.tagName.toLowerCase();
if (mode === "svg") {
if (tag === "foreignobject") {
sanitizeSvgAttributes(child, depth);
sanitizeNode(child, "html", depth);
} else if (tag === "style") {
sanitizeSvgAttributes(child, depth);
if (child.textContent) {
child.textContent = sanitizeStyleElement(child.textContent);
}
} else if (ANIMATION_TAGS.has(tag) && isAnimationDangerous(child)) {
child.remove();
} else if (ALLOWED_SVG_TAGS.has(tag)) {
sanitizeSvgAttributes(child, depth);
sanitizeNode(child, "svg", depth);
} else {
child.remove();
}
} else {
if (BLOCKED_HTML_TAGS.has(tag)) {
child.remove();
} else if (ALLOWED_HTML_TAGS.has(tag)) {
sanitizeHtmlAttributes(child);
sanitizeNode(child, "html", depth);
} else {
child.remove();
}
}
}
}
function sanitizeSvgInner(svgText, depth) {
const doc = new DOMParser().parseFromString(svgText, "image/svg+xml");
const parseError = doc.querySelector("parsererror");
if (parseError) return "";
const svg = doc.documentElement;
if (svg.tagName.toLowerCase() !== "svg") return "";
sanitizeSvgAttributes(svg, depth);
sanitizeNode(svg, "svg", depth);
if (svg.children.length === 0) return "";
return new XMLSerializer().serializeToString(svg);
}
function sanitizeSvg(svgText) {
return sanitizeSvgInner(svgText, 0);
}
//# sourceMappingURL=sanitizeSvg.js.map