22
33import base64
44import os
5- import re
65import shutil
76import subprocess
87import tempfile
9- import xml . etree . ElementTree as etree
8+ from typing import List
109
1110import requests
1211from markdown import Extension
13- from markdown .blockprocessors import BlockProcessor
12+ from markdown .preprocessors import Preprocessor
1413
1514
16- class MermaidDataURIProcessor ( BlockProcessor ):
15+ class MermaidDataURIPreprocessor ( Preprocessor ):
1716 """Preprocessor to convert mermaid code blocks to SVG/PNG images."""
1817
19- MERMAID_CODE_BLOCK_RE = re .compile (r'```mermaid\s*(.*)' )
20- MIME_TYPES = {
21- 'svg' : 'image/svg+xml' ,
22- 'png' : 'image/png' ,
23- }
24-
25- def test (self , parent , block ):
26- return self .MERMAID_CODE_BLOCK_RE .match (block )
27-
28- def __init__ (self , parser , kroki_url , mermaid_cli ):
29- super ().__init__ (parser )
30- self .kroki_url = kroki_url
31- self .mermaid_cli = mermaid_cli
32-
33- def run (self , parent , blocks ):
34- # Mermaid code block
35- block = blocks .pop (0 )
36- match = self .MERMAID_CODE_BLOCK_RE .match (block )
37- mermaid_code = block [match .end () :].strip ().replace ('```' , '' ).strip ()
38-
39- # Options
40- options_str = match .group (1 ).strip ()
41- options = {}
42- if options_str :
43- for item in options_str .split ():
44- if '=' in item :
45- key , value = item .split ('=' , 1 )
46- options [key ] = value .strip ('"\' ' )
47-
48- # Data URI
49- data_uri = self ._get_data_uri (mermaid_code , options )
50-
51- # Create image element
52- el = etree .SubElement (parent , 'p' )
53- img = etree .SubElement (el , 'img' , {'src' : data_uri })
54- img .text = mermaid_code
55- del options ['image' ]
56- for key , value in options .items ():
57- img .set (key , value )
58-
59- def _get_data_uri (self , content : str , options : dict ) -> str :
60- """Convert mermaid code to data URI."""
61- image_type = options .get ('image' , 'svg' )
62- base64image = self ._get_base64image (content , image_type )
63- data_uri = f'data:{ self .MIME_TYPES [image_type ]} ;base64,{ base64image } '
64- return data_uri
65-
66- def _get_base64image (self , mermaid_code : str , image_type : str ) -> str :
18+ KROKI_URL = 'https://kroki.io'
19+
20+ def __init__ (self , md , config ):
21+ super ().__init__ (md )
22+ self .kroki_url = config .get ('kroki_url' , self .KROKI_URL )
23+ self .mermaid_cli = config .get ('mermaid_cli' , False )
24+
25+ def run (self , lines : List [str ]) -> List [str ]:
26+ new_lines : List [str ] = []
27+ is_in_mermaid = False
28+
29+ for line in lines :
30+ if line .strip ().startswith ('```mermaid' ):
31+ is_in_mermaid = True
32+ mermaid_block : List [str ] = []
33+ # Extract options after '```mermaid'
34+ options = line .strip ()[10 :].strip ()
35+ option_dict = {}
36+ if options :
37+ for option in options .split ():
38+ key , _ , value = option .partition ('=' )
39+ option_dict [key ] = value
40+ continue
41+ elif line .strip () == '```' and is_in_mermaid :
42+ is_in_mermaid = False
43+ if mermaid_block :
44+ mermaid_code = '\n ' .join (mermaid_block )
45+
46+ # Image type handling
47+ if 'image' in option_dict :
48+ image_type = option_dict ['image' ]
49+ del option_dict ['image' ]
50+ if image_type not in ['svg' , 'png' ]:
51+ image_type = 'svg'
52+ else :
53+ image_type = 'svg'
54+
55+ base64image = self ._mermaid2base64image (mermaid_code , image_type )
56+ if base64image :
57+ # Build the <img> tag with extracted options
58+ if image_type == 'svg' :
59+ img_tag = f'<img src="data:image/svg+xml;base64,{ base64image } "'
60+ else :
61+ img_tag = f'<img src="data:image/png;base64,{ base64image } "'
62+ for key , value in option_dict .items ():
63+ img_tag += f' { key } ={ value } '
64+ img_tag += ' />'
65+ new_lines .append (img_tag )
66+ else :
67+ new_lines .append ('```mermaid' )
68+ new_lines .extend (mermaid_block )
69+ new_lines .append ('```' )
70+ continue
71+
72+ if is_in_mermaid :
73+ mermaid_block .append (line )
74+ else :
75+ new_lines .append (line )
76+
77+ return new_lines
78+
79+ def _mermaid2base64image (self , mermaid_code : str , image_type : str ) -> str :
6780 """Convert mermaid code to SVG/PNG."""
81+ # Use Kroki or mmdc (Mermaid CLI) to convert mermaid code to image
6882 if not self .mermaid_cli :
69- return self ._get_base64image_from_kroki (mermaid_code , image_type )
83+ return self ._mermaid2base64image_kroki (mermaid_code , image_type )
7084 else :
71- return self ._get_base64image_from_mmdc (mermaid_code , image_type )
85+ return self ._mermaid2base64image_mmdc (mermaid_code , image_type )
7286
73- def _get_base64image_from_kroki (self , mermaid_code : str , image_type : str ) -> str :
87+ def _mermaid2base64image_kroki (self , mermaid_code : str , image_type : str ) -> str :
7488 """Convert mermaid code to SVG/PNG using Kroki."""
7589 kroki_url = f'{ self .kroki_url } /mermaid/{ image_type } '
7690 headers = {'Content-Type' : 'text/plain' }
@@ -86,7 +100,7 @@ def _get_base64image_from_kroki(self, mermaid_code: str, image_type: str) -> str
86100 return base64image
87101 return ''
88102
89- def _get_base64image_from_mmdc (self , mermaid_code : str , image_type : str ) -> str :
103+ def _mermaid2base64image_mmdc (self , mermaid_code : str , image_type : str ) -> str :
90104 """Convert mermaid code to SVG/PNG using mmdc (Mermaid CLI)."""
91105 with tempfile .NamedTemporaryFile (mode = 'w' , suffix = '.mmd' , delete = False ) as tmp_mmd :
92106 tmp_mmd .write (mermaid_code )
@@ -138,14 +152,17 @@ class MermaidDataURIExtension(Extension):
138152
139153 def __init__ (self , ** kwargs ):
140154 self .config = {
141- 'kroki_url' : [kwargs . get ( 'kroki_url' , ' https://kroki.io') , 'Kroki server URL ' ],
142- 'mermaid_cli' : [kwargs . get ( 'mermaid_cli' , False ) , 'Use mermaid CLI (requires installation) ' ],
155+ 'kroki_url' : [' https://kroki.io' , 'Base URL for the Kroki server. ' ],
156+ 'mermaid_cli' : [False , 'Use mmdc (Mermaid CLI) instead of Kroki server. ' ],
143157 }
144158 super ().__init__ (** kwargs )
159+ self .extension_configs = kwargs
145160
146161 def extendMarkdown (self , md ):
147- self .processor = MermaidDataURIProcessor (md .parser , self .getConfig ('kroki_url' ), self .getConfig ('mermaid_cli' ))
148- md .parser .blockprocessors .register (self .processor , 'markdown_mermaid_data_uri' , 50 )
162+ config = self .getConfigs ()
163+ final_config = {** config , ** self .extension_configs }
164+ mermaid_preprocessor = MermaidDataURIPreprocessor (md , final_config )
165+ md .preprocessors .register (mermaid_preprocessor , 'markdown_mermaid_data_udi' , 50 )
149166
150167
151168# pylint: disable=C0103
0 commit comments