Render Mermaid Diagram
Convert Mermaid diagram syntax to SVG using the free mermaid.ink API
Source Code
import fs from "fs";
import path from "path";
const [mermaidCode, title, outputDir] = process.argv.slice(2);
if (!mermaidCode) {
console.error("Error: mermaidCode is required");
process.exit(1);
}
if (!outputDir) {
console.error("Error: outputDir is required");
process.exit(1);
}
// Exponential backoff retry logic
async function fetchWithRetry(url, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url);
if (response.ok) {
return response;
}
// Rate limit (429) or server error (500+) - retry with backoff
if (response.status === 429 || response.status >= 500) {
if (attempt < maxRetries - 1) {
const baseDelay = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * 1000;
const delay = baseDelay + jitter;
console.log(
`Rate limit hit (attempt ${attempt + 1}/${maxRetries}). Retrying in ${Math.round(delay / 1000)}s...`
);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
}
// Non-retryable error
const text = await response.text();
throw new Error(`API error (${response.status}): ${text}`);
} catch (err) {
if (err.message.startsWith("API error")) {
throw err;
}
// Network error - retry with backoff
if (attempt < maxRetries - 1) {
const baseDelay = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * 1000;
const delay = baseDelay + jitter;
console.log(
`Network error (attempt ${attempt + 1}/${maxRetries}). Retrying in ${Math.round(delay / 1000)}s...`
);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw err;
}
}
}
// Detect diagram type from mermaid code
function detectDiagramType(code) {
const firstLine = code.trim().split("\n")[0].toLowerCase();
if (firstLine.startsWith("flowchart") || firstLine.startsWith("graph"))
return "flowchart";
if (firstLine.startsWith("sequencediagram")) return "sequence";
if (firstLine.startsWith("classdiagram")) return "class";
if (firstLine.startsWith("statediagram")) return "state";
if (firstLine.startsWith("erdiagram")) return "er";
if (firstLine.startsWith("gantt")) return "gantt";
if (firstLine.startsWith("pie")) return "pie";
if (firstLine.startsWith("mindmap")) return "mindmap";
if (firstLine.startsWith("timeline")) return "timeline";
if (firstLine.startsWith("gitgraph")) return "gitgraph";
if (firstLine.startsWith("quadrantchart")) return "quadrant";
if (firstLine.startsWith("journey")) return "journey";
if (firstLine.startsWith("requirementdiagram")) return "requirement";
if (firstLine.startsWith("c4context") || firstLine.startsWith("c4container"))
return "c4";
return "diagram";
}
async function main() {
const diagramType = detectDiagramType(mermaidCode);
console.log(`Rendering ${diagramType} diagram...`);
// Encode the mermaid code for the URL
// mermaid.ink accepts base64-encoded diagram definitions
// Using /svg/ endpoint for vector output (scales better than raster)
const encoded = Buffer.from(mermaidCode, "utf-8").toString("base64");
const url = `https://mermaid.ink/svg/${encoded}`;
console.log("Fetching rendered SVG from mermaid.ink...");
const response = await fetchWithRetry(url);
const arrayBuffer = await response.arrayBuffer();
const imageBuffer = Buffer.from(arrayBuffer);
// Generate filename
let outputName;
if (title && title.trim()) {
const slug = title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "")
.slice(0, 40);
outputName = `${diagramType}-${slug}`;
} else {
const timestamp = Date.now();
outputName = `${diagramType}-${timestamp}`;
}
// Ensure output directory exists
fs.mkdirSync(outputDir, { recursive: true });
const outputPath = path.join(outputDir, `${outputName}.svg`);
fs.writeFileSync(outputPath, imageBuffer);
console.log(`✓ Saved: ${outputPath}`);
console.log(
JSON.stringify({
success: true,
path: outputPath,
diagramType,
title: title || null,
})
);
}
main().catch((err) => {
console.error("Failed:", err.message);
process.exit(1);
});