Files
freno-dev/src/components/blog/MermaidRenderer.tsx
Michael Freno 898c891bd5 chore: remediate pygienium audit findings
Dead code: 77 verified-unused exports, files (BackArrow, MenuBars,
cookies.ts, db/create.ts, schemas/comment.ts, security-headers.ts) and
12 unused dependencies removed
Comments: ~370 RESTATE comments stripped across 53 files; 2 verbose
blocks tightened; dead commented-out config removed
Complexity: bulkUpsert extracted into 11 per-entity helpers (CCN 156->~10);
login formHandler split into 3 submitters (CCN 63->~5); account page render
split into 8 section components (CCN 40); updatePost SQL builder rebuilt;
assert*Owned consolidated behind generic assertOwnedBy
Defensive guards: 4 redundant rethrow/nullish guards removed
2026-08-11 13:36:18 -04:00

87 lines
2.4 KiB
TypeScript

import { onMount } from "solid-js";
/**
* Sanitize Mermaid SVG output by removing dangerous elements and attributes.
* Prevents stored XSS via malicious Mermaid diagram code.
*/
function sanitizeMermaidSvg(svgString: string): string {
const parser = new DOMParser();
const doc = parser.parseFromString(svgString, "text/html");
doc.querySelectorAll("script, iframe, object, embed, form, link, meta, base").forEach((el) => {
el.remove();
});
doc.querySelectorAll("[on*], [href*='javascript:'], [style*='expression(']").forEach((el) => {
const attrs = Array.from(el.attributes);
attrs.forEach((attr) => {
if (
attr.name.startsWith("on") ||
attr.name === "href" ||
attr.name === "style"
) {
const value = attr.value;
if (
attr.name.startsWith("on") ||
value.includes("javascript:") ||
value.includes("expression(")
) {
el.removeAttribute(attr.name);
}
}
});
});
return doc.body.innerHTML;
}
export default function MermaidRenderer() {
onMount(async () => {
const mermaidPres = document.querySelectorAll('pre[data-type="mermaid"]');
// Only load mermaid if there are diagrams to render
if (mermaidPres.length === 0) return;
const mermaid = (await import("mermaid")).default;
mermaid.initialize({
startOnLoad: false,
theme: "dark",
securityLevel: "strict",
fontFamily: "monospace",
themeVariables: {
darkMode: true,
primaryColor: "#2c2f40",
primaryTextColor: "#b5c1f1",
primaryBorderColor: "#739df2",
lineColor: "#739df2",
secondaryColor: "#3e4255",
tertiaryColor: "#505469"
}
});
mermaidPres.forEach(async (pre, index) => {
const code = pre.querySelector("code");
if (!code) return;
const content = code.textContent || "";
if (!content.trim()) return;
try {
const id = `mermaid-${index}-${Math.random().toString(36).substr(2, 9)}`;
const { svg } = await mermaid.render(id, content);
const wrapper = document.createElement("div");
wrapper.className = "mermaid-rendered";
wrapper.innerHTML = sanitizeMermaidSvg(svg);
pre.replaceWith(wrapper);
} catch (err) {
console.error("Failed to render mermaid diagram:", err);
pre.classList.add("mermaid-error");
}
});
});
return null;
}