Skip to content

Commit 9ed76f1

Browse files
author
Aoife
committed
added a script to do notebooks justice
1 parent 2a56d63 commit 9ed76f1

File tree

2 files changed

+187
-3
lines changed

2 files changed

+187
-3
lines changed

assets/scripts/generate_notebooks.sh

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
#!/bin/bash
22
# Generate Jupyter notebooks from .qmd files without re-executing code
3-
# This script converts rendered .qmd files to .ipynb format using quarto convert
3+
# This script converts .qmd files to .ipynb format with proper cell structure
44

55
set -e
66

77
echo "Generating Jupyter notebooks from .qmd files..."
88

9+
# Get the directory where this script is located
10+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11+
912
# Find all .qmd files in tutorials, usage, and developers directories
1013
find tutorials usage developers -name "index.qmd" | while read qmd_file; do
1114
dir=$(dirname "$qmd_file")
1215
ipynb_file="${dir}/index.ipynb"
1316

1417
echo "Converting $qmd_file to $ipynb_file"
1518

16-
# Convert qmd to ipynb without execution
17-
quarto convert "$qmd_file"
19+
# Convert qmd to ipynb using our custom Python script
20+
python3 "${SCRIPT_DIR}/qmd_to_ipynb.py" "$qmd_file" "$ipynb_file"
1821

1922
# Check if conversion was successful
2023
if [ -f "$ipynb_file" ]; then

assets/scripts/qmd_to_ipynb.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Convert Quarto .qmd files to Jupyter .ipynb notebooks with proper cell structure.
4+
Each code block becomes a code cell, and markdown content becomes markdown cells.
5+
"""
6+
7+
import sys
8+
import json
9+
import re
10+
from pathlib import Path
11+
from typing import List, Dict, Any, Optional
12+
13+
14+
class QmdToIpynb:
15+
def __init__(self, qmd_path: str):
16+
self.qmd_path = Path(qmd_path)
17+
self.cells: List[Dict[str, Any]] = []
18+
self.kernel_name = "julia-1.11" # Default kernel
19+
20+
def parse(self) -> None:
21+
"""Parse the .qmd file and extract cells."""
22+
with open(self.qmd_path, 'r', encoding='utf-8') as f:
23+
content = f.read()
24+
25+
lines = content.split('\n')
26+
i = 0
27+
28+
# Skip YAML frontmatter
29+
if lines[0].strip() == '---':
30+
i = 1
31+
while i < len(lines) and lines[i].strip() != '---':
32+
# Check for engine specification
33+
if lines[i].strip().startswith('engine:'):
34+
engine = lines[i].split(':', 1)[1].strip()
35+
if engine == 'julia':
36+
self.kernel_name = "julia-1.11"
37+
elif engine == 'python':
38+
self.kernel_name = "python3"
39+
i += 1
40+
i += 1 # Skip the closing ---
41+
42+
# Parse the rest of the document
43+
current_markdown = []
44+
45+
while i < len(lines):
46+
line = lines[i]
47+
48+
# Check for code block start
49+
code_block_match = re.match(r'^```\{(\w+)\}', line)
50+
if code_block_match:
51+
# Save any accumulated markdown
52+
if current_markdown:
53+
self._add_markdown_cell(current_markdown)
54+
current_markdown = []
55+
56+
# Extract code block
57+
lang = code_block_match.group(1)
58+
i += 1
59+
code_lines = []
60+
cell_options = []
61+
62+
# Collect code and options
63+
while i < len(lines) and not lines[i].startswith('```'):
64+
if lines[i].startswith('#|'):
65+
cell_options.append(lines[i])
66+
else:
67+
code_lines.append(lines[i])
68+
i += 1
69+
70+
# Add code cell (with options as comments at the top)
71+
full_code = cell_options + code_lines
72+
self._add_code_cell(full_code, lang)
73+
74+
i += 1 # Skip closing ```
75+
else:
76+
# Accumulate markdown
77+
current_markdown.append(line)
78+
i += 1
79+
80+
# Add any remaining markdown
81+
if current_markdown:
82+
self._add_markdown_cell(current_markdown)
83+
84+
def _add_markdown_cell(self, lines: List[str]) -> None:
85+
"""Add a markdown cell, stripping leading/trailing empty lines."""
86+
# Strip leading empty lines
87+
while lines and not lines[0].strip():
88+
lines.pop(0)
89+
90+
# Strip trailing empty lines
91+
while lines and not lines[-1].strip():
92+
lines.pop()
93+
94+
if not lines:
95+
return
96+
97+
content = '\n'.join(lines)
98+
cell = {
99+
"cell_type": "markdown",
100+
"metadata": {},
101+
"source": content
102+
}
103+
self.cells.append(cell)
104+
105+
def _add_code_cell(self, lines: List[str], lang: str) -> None:
106+
"""Add a code cell."""
107+
content = '\n'.join(lines)
108+
109+
# For non-Julia code blocks (like dot/graphviz), add as markdown with code formatting
110+
# since Jupyter notebooks typically use Julia kernel for these docs
111+
if lang != 'julia' and lang != 'python':
112+
# Convert to markdown with code fence
113+
markdown_content = f"```{lang}\n{content}\n```"
114+
cell = {
115+
"cell_type": "markdown",
116+
"metadata": {},
117+
"source": markdown_content
118+
}
119+
else:
120+
cell = {
121+
"cell_type": "code",
122+
"execution_count": None,
123+
"metadata": {},
124+
"outputs": [],
125+
"source": content
126+
}
127+
128+
self.cells.append(cell)
129+
130+
def to_notebook(self) -> Dict[str, Any]:
131+
"""Convert parsed cells to Jupyter notebook format."""
132+
notebook = {
133+
"cells": self.cells,
134+
"metadata": {
135+
"kernelspec": {
136+
"display_name": "Julia 1.11",
137+
"language": "julia",
138+
"name": self.kernel_name
139+
},
140+
"language_info": {
141+
"file_extension": ".jl",
142+
"mimetype": "application/julia",
143+
"name": "julia",
144+
"version": "1.11.0"
145+
}
146+
},
147+
"nbformat": 4,
148+
"nbformat_minor": 5
149+
}
150+
return notebook
151+
152+
def write(self, output_path: str) -> None:
153+
"""Write the notebook to a file."""
154+
notebook = self.to_notebook()
155+
with open(output_path, 'w', encoding='utf-8') as f:
156+
json.dump(notebook, f, indent=2, ensure_ascii=False)
157+
158+
159+
def main():
160+
if len(sys.argv) < 2:
161+
print("Usage: qmd_to_ipynb.py <input.qmd> [output.ipynb]")
162+
sys.exit(1)
163+
164+
qmd_path = sys.argv[1]
165+
166+
# Determine output path
167+
if len(sys.argv) >= 3:
168+
ipynb_path = sys.argv[2]
169+
else:
170+
ipynb_path = Path(qmd_path).with_suffix('.ipynb')
171+
172+
# Convert
173+
converter = QmdToIpynb(qmd_path)
174+
converter.parse()
175+
converter.write(ipynb_path)
176+
177+
print(f"Converted {qmd_path} -> {ipynb_path}")
178+
179+
180+
if __name__ == "__main__":
181+
main()

0 commit comments

Comments
 (0)