code icon Code

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);
});