/** A deliberately narrow Mermaid source profile; syntax errors remain renderer errors. */ export function validateDiagram(source: string): string { if (typeof source !== "string" || !source.trim() || source.length > 12_000) { throw new Error("diagram must contain 1 to 12000 characters"); } // No configuration, HTML/entities, CSS shorthand, or URI-bearing features. if ( /[\u0000-\u0008\u000b-\u001f]/u.test(source) || /%%\s*\{|^\s*---|:::/mu.test(source) || /<\/?[A-Za-z!][^>]*>|&(?:#\w+|[A-Za-z]+);/u.test(source) || /\b(?:https?|mailto|javascript|data):|\b(?:href|xlink:href)\s*=/iu.test( source, ) ) throw new Error( "diagram directives, HTML, links, or styling are not allowed", ); let type = ""; for (const rawLine of source.split("\n")) { const line = rawLine.trim(); if (!line || line.startsWith("%%")) continue; if (!type) { const header = /^(flowchart\s+(?:TB|TD|BT|RL|LR)|sequenceDiagram|stateDiagram-v2|classDiagram|erDiagram)\s*;?$/u.exec( line, ); if (!header) throw new Error("unsupported diagram type or header"); type = line.split(/\s/u)[0] ?? ""; continue; } for (const statement of line.split(";")) { const trimmed = statement.trim(); if ( /^(?:click|links?|style|classDef|linkStyle|cssClass|callback)\b/iu.test( trimmed, ) || (/^class\b/iu.test(trimmed) && type !== "classDiagram") || /@\s*\{/u.test(trimmed) ) throw new Error("diagram links or styling are not allowed"); } } if (!type) throw new Error("diagram type is required"); return source; }