Skip to content

Commit 8e7ecf8

Browse files
committed
more work on running legacy docs
1 parent b030c94 commit 8e7ecf8

File tree

2 files changed

+154
-171
lines changed

2 files changed

+154
-171
lines changed

Makefile

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ RUN = uv run
44
PACKAGE_DIRS = _plotly_utils plotly
55
CODE_DIRS = ${PACKAGE_DIRS} scripts
66
EXAMPLE_SRC = $(wildcard doc/python/*.md)
7-
EXAMPLE_DST = $(patsubst doc/python/%.md,pages/examples/%.md,${EXAMPLE_SRC})
87

98
## commands: show available commands
109
commands:
@@ -23,11 +22,9 @@ docs-lint:
2322
docs-tmp:
2423
MKDOCS_TEMP_DIR=./docs_tmp ${RUN} mkdocs build
2524

26-
## examples: temporary target to copy and run doc/python
27-
examples: ${EXAMPLE_DST}
28-
29-
pages/examples/%.md: doc/python/%.md
30-
${RUN} bin/run_markdown.py --output $@ $<
25+
## examples: generate Markdown from doc/python
26+
examples:
27+
${RUN} bin/run_markdown.py --outdir pages/examples --inline --verbose ${EXAMPLE_SRC}
3128

3229
## format: reformat code
3330
format:

bin/run_markdown.py

100755100644
Lines changed: 151 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -8,131 +8,89 @@
88
from contextlib import redirect_stdout, redirect_stderr
99
import io
1010
from pathlib import Path
11+
import plotly.graph_objects as go
1112
import sys
1213
import 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"![Generated Plot](./{image})")
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

261247
if __name__ == "__main__":

0 commit comments

Comments
 (0)