File size: 4,873 Bytes
e40b114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0c0f79
e40b114
 
 
 
 
 
 
 
 
e0c0f79
e40b114
e0c0f79
 
 
 
 
e40b114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fcdf166
e40b114
 
 
 
 
 
 
 
 
 
 
 
e0c0f79
e40b114
 
 
 
 
 
 
 
 
 
 
e0c0f79
e40b114
 
e0c0f79
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# mermaid_renderer.py
import os
import sys
import subprocess
import tempfile
import logging

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

class MermaidRenderer:
    """
    A Python class to render Mermaid diagrams using @mermaid-js/mermaid-cli.
    """
    def __init__(self):
        """Initialize the renderer and check if dependencies are installed"""
        self._check_dependencies()

    def _check_dependencies(self):
        """Check if Node.js and npx are installed"""
        try:
            subprocess.run(["node", "--version"], capture_output=True, check=True, text=True)
            logging.info("Node.js found.")
        except (subprocess.SubprocessError, FileNotFoundError) as e:
            logging.error(f"Node.js check failed: {e}")
            # In a web context, exiting might not be ideal. Log error.
            # Consider raising an exception or handling this state in the Flask app.
            raise RuntimeError("Error: Node.js is not installed or not found in PATH.")

        # Check if npx is available
        try:
            subprocess.run(["npx", "--version"], capture_output=True, check=True, text=True)
            logging.info("npx found.")
        except (subprocess.SubprocessError, FileNotFoundError) as e:
            logging.error(f"npx check failed: {e}")
            raise RuntimeError("Error: npx is not installed or not found in PATH.")

    def render(self, mermaid_code, output_format="png", theme="default"):
        """
        Render Mermaid code to the specified format into a temporary file.

        Args:
            mermaid_code (str): The Mermaid diagram code.
            output_format (str, optional): Output format (png, pdf, svg). Default: png.
            theme (str, optional): Mermaid theme. Default: default.

        Returns:
            tuple: (path_to_temp_output_file, temp_input_file_path) or raises Exception on error.
                   The caller is responsible for deleting these files.
        """
        valid_formats = ["png", "pdf", "svg"]
        if output_format not in valid_formats:
            raise ValueError(f"Invalid output format '{output_format}'. Choose from: {', '.join(valid_formats)}")

        valid_themes = ["default", "forest", "dark", "neutral"]
        if theme not in valid_themes:
             raise ValueError(f"Invalid theme '{theme}'. Choose from: {', '.join(valid_themes)}")

        # Create temporary files (ensure they are deleted by the caller)
        # Input file for mermaid code
        temp_input_file = tempfile.NamedTemporaryFile(mode='w', suffix='.mmd', delete=False)
        # Output file for the generated diagram
        temp_output_file = tempfile.NamedTemporaryFile(suffix=f'.{output_format}', delete=False)

        input_path = temp_input_file.name
        output_path = temp_output_file.name

        try:
            temp_input_file.write(mermaid_code)
            temp_input_file.close() # Close file before passing to subprocess

            cmd = [
                "npx", "@mermaid-js/mermaid-cli",
                "-i", input_path,
                "-o", output_path,
                "-t", theme,
                # No -f flag needed for mmdc, format is determined by -o extension
                # However, explicitly setting background color might be needed for transparency
                # "-b", "transparent" # Example: if you want transparent background for PNG/SVG
            ]
            logging.info(f"Running mmdc command: {' '.join(cmd)}")

            result = subprocess.run(cmd, check=True, capture_output=True, text=True)
            logging.info(f"mmdc execution successful. Output saved to: {output_path}")
            if result.stderr:
                logging.warning(f"mmdc stderr: {result.stderr}")

            # Return paths for Flask to handle; caller must delete files
            return output_path, input_path

        except subprocess.CalledProcessError as e:
            logging.error(f"Error rendering diagram with mmdc: {e}")
            logging.error(f"mmdc stderr: {e.stderr}")
            # Clean up files on error before raising
            temp_output_file.close()
            os.unlink(output_path)
            if os.path.exists(input_path): # Input file might already be closed/deleted
                os.unlink(input_path)
            raise RuntimeError(f"Error rendering diagram: {e.stderr or e}")
        except Exception as e:
            # Catch any other unexpected errors
            logging.error(f"Unexpected error during rendering: {e}")
            # Ensure cleanup
            temp_input_file.close()
            temp_output_file.close()
            if os.path.exists(input_path): os.unlink(input_path)
            if os.path.exists(output_path): os.unlink(output_path)
            raise # Re-raise the caught exception