88from contextlib import redirect_stdout , redirect_stderr
99import io
1010from pathlib import Path
11+ import plotly .graph_objects as go
1112import sys
1213import traceback
1314
1415
15- def parse_markdown (content ):
16- """Parse markdown content and extract Python code blocks."""
17- lines = content .split ("\n " )
18- blocks = []
19- current_block = None
20- in_code_block = False
21- 22- for i , line in enumerate (lines ):
23- # Start of Python code block
24- if line .strip ().startswith ("```python" ):
25- in_code_block = True
26- current_block = {
27- "start_line" : i ,
28- "end_line" : None ,
29- "code" : [],
30- "type" : "python" ,
31- }
32- 33- # End of code block
34- elif line .strip () == "```" and in_code_block :
35- in_code_block = False
36- current_block ["end_line" ] = i
37- current_block ["code" ] = "\n " .join (current_block ["code" ])
38- blocks .append (current_block )
39- current_block = None
40- 41- # Line inside code block
42- elif in_code_block :
43- current_block ["code" ].append (line )
16+ def main ():
17+ args = _parse_args ()
18+ for filename in args .input :
19+ _do_file (args , Path (filename ))
4420
45- return blocks
4621
22+ def _do_file (args , input_file ):
23+ """Process a single file."""
4724
48- def execute_python_code (code , output_dir , output_figure_stem ):
49- """Execute Python code and capture output and generated files."""
50- # Capture stdout and stderr
51- stdout_buffer = io .StringIO ()
52- stderr_buffer = io .StringIO ()
25+ # Validate input file
26+ if not input_file .exists ():
27+ print (f"Error: '{ input_file } ' not found" , file = sys .stderr )
28+ sys .exit (1 )
5329
54- # Track files created during execution
55- output_path = Path (output_dir )
56- if not output_path .exists ():
57- output_path .mkdir (parents = True , exist_ok = True )
30+ # Determine output file path etc.
31+ stem = input_file .stem
32+ output_file = args .outdir / f"{ input_file .stem } { input_file .suffix } "
33+ if input_file .resolve () == output_file .resolve ():
34+ print (f"Error: output would overwrite input '{ input_file } '" , file = sys .stderr )
35+ sys .exit (1 )
5836
59- files_before = set (f .name for f in output_path .iterdir ())
60- result = {"stdout" : "" , "stderr" : "" , "error" : None , "images" : [], "html_files" : []}
61- figures = []
37+ # Read input
6238 try :
63- # Create a custom show function to capture plotly figures
64- def capture_plotly_show (fig ):
65- """Custom show function that saves plotly figures instead of displaying them."""
66- nonlocal figures
67- figures .append (fig )
68- png_filename = (
69- f"{ output_figure_stem } _{ len (figures )} .png"
70- )
71- png_path = Path (output_dir ) / png_filename
72- fig .write_image (png_path , width = 800 , height = 600 )
73- result ["images" ].append (png_filename )
74- print (f"Plotly figure saved as PNG: { png_filename } " )
75- return
76- 77- # Create a namespace for code execution
78- exec_globals = {
79- "__name__" : "__main__" ,
80- "__file__" : "<markdown_code>" ,
81- }
82- 83- # Monkey patch plotly show method to capture figures
84- original_show = None
85- 86- # Execute the code with output capture
87- with redirect_stdout (stdout_buffer ), redirect_stderr (stderr_buffer ):
88- # Try to import plotly and patch the show method
89- def patched_show (self , * args , ** kwargs ):
90- capture_plotly_show (self )
91- import plotly .graph_objects as go
92- original_show = go .Figure .show
93- go .Figure .show = patched_show
39+ with open (input_file , "r" , encoding = "utf-8" ) as f :
40+ content = f .read ()
41+ except Exception as e :
42+ print (f"Error reading input file: { e } " , file = sys .stderr )
43+ sys .exit (1 )
9444
95- # Execute the code
96- exec (code , exec_globals )
45+ # Parse markdown and extract code blocks
46+ _report (args .verbose , f"Processing { input_file } ..." )
47+ code_blocks = _parse_md (content )
48+ _report (args .verbose , f"- Found { len (code_blocks )} code blocks" )
9749
98- # Try to find and handle any plotly figures that were created and not already processed
99- for name , obj in exec_globals .items ():
100- if (
101- hasattr (obj , "__class__" )
102- and "plotly" in str (type (obj )).lower ()
103- and hasattr (obj , "show" )
104- ):
105- # This looks like a plotly figure that wasn't already processed by show()
106- if obj not in figures :
107- print ("NOT ALREADY PROCESSED" , obj , file = sys .stderr )
108- capture_plotly_show (obj )
109- 110- # Restore original show method if we patched it
111- if original_show :
112- import plotly .graph_objects as go
113- go .Figure .show = original_show
50+ # Execute code blocks and collect results
51+ execution_results = []
52+ figure_counter = 0
53+ for i , block in enumerate (code_blocks ):
54+ _report (args .verbose , f"- Executing block { i + 1 } /{ len (code_blocks )} " )
55+ figure_counter , result = _run_code (block ["code" ], args .outdir , stem , figure_counter )
56+ execution_results .append (result )
57+ _report (result ["error" ], f" - Warning: block { i + 1 } had an error" )
58+ _report (result ["images" ], f" - Generated { len (result ['images' ])} image(s)" )
11459
60+ # Generate and save output
61+ content = _generate_markdown (args , content , code_blocks , execution_results , args .outdir )
62+ try :
63+ with open (output_file , "w" , encoding = "utf-8" ) as f :
64+ f .write (content )
65+ _report (args .verbose , f"- Output written to { output_file } " )
66+ _report (any (result ["images" ] for result in execution_results ), f"- Images saved to { args .outdir } " )
11567 except Exception as e :
116- result ["error" ] = f"Error executing code: { str (e )} \n { traceback .format_exc ()} "
68+ print (f"Error writing output file: { e } " , file = sys .stderr )
69+ sys .exit (1 )
11770
118- result ["stdout" ] = stdout_buffer .getvalue ()
119- result ["stderr" ] = stderr_buffer .getvalue ()
12071
121- # Check for any additional files created
122- output_path = Path (output_dir )
123- if output_path .exists ():
124- files_after = set (f .name for f in output_path .iterdir ())
125- for f in (files_after - files_before ):
126- if f not in result ["images" ] and file .lower ().endswith (".png" ):
127- result ["images" ].append (f )
72+ def _capture_plotly_show (fig , counter , result , output_dir , stem ):
73+ """Saves figures instead of displaying them."""
74+ print (f"CAPTURE SHOW counter is { counter } " )
12875
129- return result
76+ # Save PNG
77+ png_filename = f"{ stem } _{ counter } .png"
78+ png_path = output_dir / png_filename
79+ fig .write_image (png_path , width = 800 , height = 600 )
80+ result ["images" ].append (png_filename )
13081
82+ # Save HTML and get the content for embedding
83+ html_filename = f"{ stem } _{ counter } .html"
84+ html_path = output_dir / html_filename
85+ fig .write_html (html_path , include_plotlyjs = "cdn" )
86+ html_content = fig .to_html (include_plotlyjs = "cdn" , div_id = f"plotly-div-{ counter } " , full_html = False )
87+ result ["html_files" ].append (html_filename )
88+ result .setdefault ("html_content" , []).append (html_content )
13189
132- def generate_output_markdown (content , code_blocks , execution_results , output_dir ):
90+ 91+ def _generate_markdown (args , content , code_blocks , execution_results , output_dir ):
13392 """Generate the output markdown with embedded results."""
13493 lines = content .split ("\n " )
135- output_lines = []
13694
13795 # Sort code blocks by start line in reverse order for safe insertion
13896 sorted_blocks = sorted (
@@ -173,10 +131,13 @@ def generate_output_markdown(content, code_blocks, execution_results, output_dir
173131 insert_lines .append ("" )
174132 insert_lines .append (f"" )
175133
176- # Add HTML files (for plotly figures)
177- for html_file in result .get ("html_files" , []):
178- insert_lines .append ("" )
179- insert_lines .append (f"[Interactive Plot](./{ html_file } )" )
134+ # Embed HTML content for plotly figures
135+ if args .inline :
136+ for html_content in result .get ("html_content" , []):
137+ insert_lines .append ("" )
138+ insert_lines .append ("**Interactive Plot:**" )
139+ insert_lines .append ("" )
140+ insert_lines .extend (html_content .split ("\n " ))
180141
181142 # Insert the results after the code block
182143 if insert_lines :
@@ -187,75 +148,100 @@ def generate_output_markdown(content, code_blocks, execution_results, output_dir
187148 return "\n " .join (lines )
188149
189150
190- def main ():
191- parser = argparse .ArgumentParser (
192- description = "Process Markdown files with Python code blocks and generate output with results"
193- )
194- parser .add_argument ("input_file" , help = "Input Markdown file" )
195- parser .add_argument (
196- "-o" , "--output" , help = "Output Markdown file (default: input_output.md)"
197- )
198- args = parser .parse_args ()
151+ def _parse_args ():
152+ """Parse command-line arguments."""
153+ parser = argparse .ArgumentParser (description = "Process Markdown files with code blocks" )
154+ parser .add_argument ("input" , nargs = "+" , help = "Input .md file" )
155+ parser .add_argument ("--inline" , action = "store_true" , help = "Inline HTML in .md" )
156+ parser .add_argument ("--outdir" , type = Path , help = "Output directory" )
157+ parser .add_argument ("--verbose" , action = "store_true" , help = "Report progress" )
158+ return parser .parse_args ()
199159
200- # Validate input file
201- if not Path (args .input_file ).exists ():
202- print (f"Error: Input file '{ args .input_file } ' not found" , file = sys .stderr )
203- sys .exit (1 )
204160
205- # Determine output file path
206- if args .output :
207- output_file = args .output
208- else :
209- input_path = Path (args .input_file )
210- output_file = str (
211- input_path .parent / f"{ input_path .stem } _output{ input_path .suffix } "
212- )
161+ def _parse_md (content ):
162+ """Parse Markdown and extract Python code blocks."""
163+ lines = content .split ("\n " )
164+ blocks = []
165+ current_block = None
166+ in_code_block = False
213167
214- # Determine output directory for images
215- output_dir = str (Path (output_file ).parent )
168+ for i , line in enumerate (lines ):
169+ # Start of Python code block
170+ if line .strip ().startswith ("```python" ):
171+ in_code_block = True
172+ current_block = {
173+ "start_line" : i ,
174+ "end_line" : None ,
175+ "code" : [],
176+ "type" : "python" ,
177+ }
216178
217- # Read input file
218- try :
219- with open ( args . input_file , "r" , encoding = "utf-8" ) as f :
220- content = f . read ()
221- except Exception as e :
222- print ( f"Error reading input file: { e } " , file = sys . stderr )
223- sys . exit ( 1 )
179+ # End of code block
180+ elif line . strip () == "```" and in_code_block :
181+ in_code_block = False
182+ current_block [ "end_line" ] = i
183+ current_block [ "code" ] = " \n " . join ( current_block [ "code" ])
184+ blocks . append ( current_block )
185+ current_block = None
224186
225- print (f"Processing { args .input_file } ..." )
226- output_figure_stem = Path (output_file ).stem
187+ # Line inside code block
188+ elif in_code_block :
189+ current_block ["code" ].append (line )
227190
228- # Parse markdown and extract code blocks
229- code_blocks = parse_markdown (content )
230- print (f"Found { len (code_blocks )} Python code blocks" )
191+ return blocks
231192
232- # Execute code blocks and collect results
233- execution_results = []
234- for i , block in enumerate (code_blocks ):
235- print (f"Executing code block { i + 1 } /{ len (code_blocks )} ..." )
236- result = execute_python_code (block ["code" ], output_dir , output_figure_stem )
237- execution_results .append (result )
238193
239- if result [ "error" ] :
240- print ( f" Warning: Code block { i + 1 } had an error" )
241- if result [ "images" ] :
242- print (f" Generated { len ( result [ 'images' ]) } image(s)" )
194+ def _report ( condition , message ) :
195+ """Report if condition is true."""
196+ if condition :
197+ print (message , file = sys . stderr )
243198
244- # Generate output markdown
245- output_content = generate_output_markdown (
246- content , code_blocks , execution_results , output_dir
247- )
248199
249- # Write output file
200+ def _run_code (code , output_dir , stem , figure_counter ):
201+ """Execute code capturing output and generated files."""
202+ # Capture stdout and stderr
203+ stdout_buffer = io .StringIO ()
204+ stderr_buffer = io .StringIO ()
205+ 206+ # Track files created during execution
207+ if not output_dir .exists ():
208+ output_dir .mkdir (parents = True , exist_ok = True )
209+ 210+ files_before = set (f .name for f in output_dir .iterdir ())
211+ result = {"stdout" : "" , "stderr" : "" , "error" : None , "images" : [], "html_files" : []}
250212 try :
251- with open (output_file , "w" , encoding = "utf-8" ) as f :
252- f .write (output_content )
253- print (f"Output written to { output_file } " )
254- if any (result ["images" ] for result in execution_results ):
255- print (f"Images saved to { output_dir } " )
213+ 214+ # Create a namespace for code execution
215+ exec_globals = {
216+ "__name__" : "__main__" ,
217+ "__file__" : "<markdown_code>" ,
218+ }
219+ 220+ # Execute the code with output capture
221+ with redirect_stdout (stdout_buffer ), redirect_stderr (stderr_buffer ):
222+ # Try to import plotly and patch the show method
223+ def patched_show (self , * args , ** kwargs ):
224+ nonlocal figure_counter
225+ figure_counter += 1
226+ _capture_plotly_show (self , figure_counter , result , output_dir , stem )
227+ original_show = go .Figure .show
228+ go .Figure .show = patched_show
229+ exec (code , exec_globals )
230+ go .Figure .show = original_show
231+ 256232 except Exception as e :
257- print (f"Error writing output file: { e } " , file = sys .stderr )
258- sys .exit (1 )
233+ result ["error" ] = f"Error executing code: { str (e )} \n { traceback .format_exc ()} "
234+ 235+ result ["stdout" ] = stdout_buffer .getvalue ()
236+ result ["stderr" ] = stderr_buffer .getvalue ()
237+ 238+ # Check for any additional files created
239+ files_after = set (f .name for f in output_dir .iterdir ())
240+ for f in (files_after - files_before ):
241+ if f not in result ["images" ] and f .lower ().endswith (".png" ):
242+ result ["images" ].append (f )
243+ 244+ return figure_counter , result
259245
260246
261247if __name__ == "__main__" :
0 commit comments