From b885777bd811e8546975d86666b1a64280bf8f47 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 18:25:39 +0000 Subject: [PATCH] Fix SVGs containing not rendering in markdown-svg-renderer DOMPurify's SAFE_FOR_XML protection (default on in 3.x) strips any attribute whose value contains raw-text element closers like , or . The sanitized SVG was stored in a data-svg attribute and then run through the second document-level DOMPurify pass, so any SVG containing one of those elements lost its data-svg attribute and was silently skipped by hydrateSvgBlocks, leaving a blank gap in the preview. Store sanitized SVG sources in a JS Map keyed by id instead, and emit only a data-svg-id reference through the markdown sanitization pass, so DOMPurify never inspects the SVG markup as an attribute value. https://claude.ai/code/session_01HTbzMyrEPHhHqGiFttbbqA --- markdown-svg-renderer.html | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/markdown-svg-renderer.html b/markdown-svg-renderer.html index 64687a3..f0fe4e3 100644 --- a/markdown-svg-renderer.html +++ b/markdown-svg-renderer.html @@ -433,7 +433,7 @@ // ---- Markdown rendering ---- const markdownSanitizeConfig = { USE_PROFILES: { html: true }, - ADD_ATTR: ["data-svg"], + ADD_ATTR: ["data-svg-id"], FORBID_ATTR: ["style"], FORBID_TAGS: ["style"] }; @@ -469,13 +469,11 @@ typographer: false }); -function escapeAttr(s) { - return s - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """); -} +// Sanitized SVG sources are stashed here and referenced by id, rather than +// embedded in a data- attribute: DOMPurify's SAFE_FOR_XML protection strips +// any attribute whose value contains strings like "" or "", +// which silently dropped legitimate SVGs during the second sanitize pass. +const svgStore = new Map(); const defaultFenceRenderer = md.renderer.rules.fence; md.renderer.rules.fence = (tokens, idx, options, env, self) => { @@ -485,7 +483,9 @@ if (lang === "svg") { const svg = window.DOMPurify.sanitize(token.content, svgSanitizeConfig).trim(); if (svg) { - return `
\n`; + const id = String(svgStore.size); + svgStore.set(id, svg); + return `
\n`; } } @@ -493,14 +493,20 @@ }; function hydrateSvgBlocks(root) { - root.querySelectorAll(".svg-block-placeholder[data-svg]").forEach((placeholder) => { + root.querySelectorAll(".svg-block-placeholder[data-svg-id]").forEach((placeholder) => { + const svg = svgStore.get(placeholder.getAttribute("data-svg-id")); + if (!svg) { + placeholder.remove(); + return; + } const block = document.createElement("svg-block"); - block.setAttribute("data-svg", placeholder.getAttribute("data-svg") || ""); + block.setAttribute("data-svg", svg); placeholder.replaceWith(block); }); } function renderMarkdown(src) { + svgStore.clear(); const html = md.render(src); return window.DOMPurify.sanitize(html, markdownSanitizeConfig); }