Skip to content

Commit e353abb

Browse files
committed
Add Jupyter notebook to Markdown conversion for GitHub Pages
- Add convert_notebooks.py script to convert .ipynb to .md - Add GitHub Actions workflow to auto-convert notebooks on push - Generate Markdown versions of all 8 notebooks - Create examples/index.md with links to all notebooks - Exclude .ipynb files from Jekyll build (use .md versions) - Add nbviewer and Colab links for interactive viewing
1 parent cc47c30 commit e353abb

12 files changed

+7703
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Convert Notebooks to Markdown
2+
3+
on:
4+
push:
5+
branches: [master]
6+
paths:
7+
- 'docs/examples/*.ipynb'
8+
- 'docs/convert_notebooks.py'
9+
workflow_dispatch: # Allow manual trigger
10+
11+
permissions:
12+
contents: write
13+
14+
jobs:
15+
convert-notebooks:
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- name: Checkout repository
20+
uses: actions/checkout@v4
21+
with:
22+
fetch-depth: 0
23+
24+
- name: Set up Python
25+
uses: actions/setup-python@v5
26+
with:
27+
python-version: '3.11'
28+
29+
- name: Convert notebooks to Markdown
30+
run: |
31+
cd docs
32+
python convert_notebooks.py
33+
34+
- name: Check for changes
35+
id: git-check
36+
run: |
37+
git diff --exit-code docs/examples/*.md || echo "changes=true" >> $GITHUB_OUTPUT
38+
39+
- name: Commit and push changes
40+
if: steps.git-check.outputs.changes == 'true'
41+
run: |
42+
git config --local user.email "github-actions[bot]@users.noreply.github.com"
43+
git config --local user.name "github-actions[bot]"
44+
git add docs/examples/*.md
45+
git commit -m "Auto-convert notebooks to Markdown for GitHub Pages"
46+
git push

docs/_config.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,13 @@ include:
3939
# Exclude from processing
4040
exclude:
4141
- generate_manual.py
42+
- convert_notebooks.py
4243
- "*.py"
4344
- __pycache__
4445
- "*.pyc"
4546
- Gemfile
4647
- Gemfile.lock
48+
- "*.ipynb" # Jupyter notebooks - use converted .md versions instead
4749

4850
# Navigation structure (for themes that support it)
4951
nav:
@@ -95,6 +97,10 @@ defaults:
9597
path: "safety"
9698
values:
9799
layout: "default"
100+
- scope:
101+
path: "examples"
102+
values:
103+
layout: "default"
98104

99105
# Enable GitHub metadata
100106
plugins:

docs/convert_notebooks.py

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
#!/usr/bin/env python3
2+
"""
3+
NeqSim Jupyter Notebook to Markdown Converter
4+
5+
This script converts Jupyter notebooks (.ipynb) in the docs/examples folder
6+
to Markdown files for proper rendering on GitHub Pages.
7+
8+
Usage:
9+
python convert_notebooks.py
10+
11+
Requirements:
12+
pip install nbconvert nbformat
13+
"""
14+
15+
import os
16+
import json
17+
import re
18+
from pathlib import Path
19+
from datetime import datetime
20+
21+
22+
def escape_liquid_tags(content):
23+
"""
24+
Escape Liquid template tags that would cause Jekyll errors.
25+
26+
Only escape {{ }} when they look like Liquid variable interpolation,
27+
NOT when they're part of LaTeX equations (which typically use single braces).
28+
"""
29+
# Only escape {{ ... }} patterns that look like Liquid variables
30+
# These typically have spaces around them and contain variable names
31+
# LaTeX uses single braces like \frac{a}{b}, not double braces
32+
33+
# Pattern: {{ followed by word characters (possibly with dots/brackets), then }}
34+
# This matches {{ variable }} but not nested braces in LaTeX
35+
def escape_liquid_var(match):
36+
inner = match.group(1)
37+
# If it looks like a Liquid variable (word chars, dots, brackets, pipes)
38+
if re.match(r'^[\s\w\.\[\]\|\'":\-]+$', inner):
39+
return '{% raw %}{{' + inner + '}}{% endraw %}'
40+
return match.group(0)
41+
42+
content = re.sub(r'\{\{([^{}]*)\}\}', escape_liquid_var, content)
43+
return content
44+
45+
46+
def notebook_to_markdown(notebook_path):
47+
"""
48+
Convert a Jupyter notebook to Markdown format suitable for Jekyll.
49+
50+
Args:
51+
notebook_path: Path to the .ipynb file
52+
53+
Returns:
54+
Markdown string with Jekyll front matter
55+
"""
56+
with open(notebook_path, 'r', encoding='utf-8') as f:
57+
nb = json.load(f)
58+
59+
notebook_name = Path(notebook_path).stem
60+
title = notebook_name.replace('_', ' ').replace('-', ' ')
61+
62+
# Jekyll front matter
63+
front_matter = f"""---
64+
layout: default
65+
title: "{title}"
66+
description: "Jupyter notebook tutorial for NeqSim"
67+
parent: Examples
68+
nav_order: 1
69+
---
70+
71+
# {title}
72+
73+
> **Note:** This is an auto-generated Markdown version of the Jupyter notebook
74+
> [`{notebook_name}.ipynb`](https://github.com/equinor/neqsim/blob/master/docs/examples/{notebook_name}.ipynb).
75+
> You can also [view it on nbviewer](https://nbviewer.org/github/equinor/neqsim/blob/master/docs/examples/{notebook_name}.ipynb)
76+
> or [open in Google Colab](https://colab.research.google.com/github/equinor/neqsim/blob/master/docs/examples/{notebook_name}.ipynb).
77+
78+
---
79+
80+
"""
81+
82+
markdown_content = []
83+
84+
for cell in nb.get('cells', []):
85+
cell_type = cell.get('cell_type', '')
86+
source = ''.join(cell.get('source', []))
87+
88+
if cell_type == 'markdown':
89+
# Add markdown content directly
90+
markdown_content.append(source)
91+
markdown_content.append('\n\n')
92+
93+
elif cell_type == 'code':
94+
# Determine language from notebook metadata
95+
language = nb.get('metadata', {}).get('language_info', {}).get('name', 'python')
96+
97+
# Add code block
98+
markdown_content.append(f'```{language}\n')
99+
markdown_content.append(source)
100+
if not source.endswith('\n'):
101+
markdown_content.append('\n')
102+
markdown_content.append('```\n\n')
103+
104+
# Add outputs if present
105+
outputs = cell.get('outputs', [])
106+
if outputs:
107+
has_output = False
108+
output_text = []
109+
110+
for output in outputs:
111+
output_type = output.get('output_type', '')
112+
113+
if output_type == 'stream':
114+
text = ''.join(output.get('text', []))
115+
if text.strip():
116+
output_text.append(text)
117+
has_output = True
118+
119+
elif output_type == 'execute_result':
120+
data = output.get('data', {})
121+
if 'text/plain' in data:
122+
text = ''.join(data['text/plain'])
123+
if text.strip():
124+
output_text.append(text)
125+
has_output = True
126+
127+
elif output_type == 'error':
128+
# Include error traceback
129+
traceback = output.get('traceback', [])
130+
if traceback:
131+
# Strip ANSI codes
132+
error_text = '\n'.join(traceback)
133+
error_text = re.sub(r'\x1b\[[0-9;]*m', '', error_text)
134+
output_text.append(f"Error: {error_text}")
135+
has_output = True
136+
137+
if has_output:
138+
markdown_content.append('<details>\n<summary>Output</summary>\n\n')
139+
markdown_content.append('```\n')
140+
markdown_content.append('\n'.join(output_text))
141+
if not output_text[-1].endswith('\n'):
142+
markdown_content.append('\n')
143+
markdown_content.append('```\n\n')
144+
markdown_content.append('</details>\n\n')
145+
146+
full_content = front_matter + ''.join(markdown_content)
147+
148+
# Escape Liquid tags
149+
full_content = escape_liquid_tags(full_content)
150+
151+
return full_content
152+
153+
154+
def convert_all_notebooks(examples_dir):
155+
"""
156+
Convert all notebooks in the examples directory to Markdown.
157+
158+
Args:
159+
examples_dir: Path to the docs/examples directory
160+
"""
161+
examples_path = Path(examples_dir)
162+
163+
if not examples_path.exists():
164+
print(f"Error: Directory not found: {examples_dir}")
165+
return
166+
167+
notebooks = list(examples_path.glob('*.ipynb'))
168+
169+
if not notebooks:
170+
print("No notebooks found in examples directory")
171+
return
172+
173+
print(f"Found {len(notebooks)} notebooks to convert:")
174+
175+
for nb_path in notebooks:
176+
md_filename = nb_path.stem + '.md'
177+
md_path = examples_path / md_filename
178+
179+
print(f" Converting: {nb_path.name} -> {md_filename}")
180+
181+
try:
182+
markdown = notebook_to_markdown(nb_path)
183+
with open(md_path, 'w', encoding='utf-8') as f:
184+
f.write(markdown)
185+
print(f" ✓ Successfully created {md_filename}")
186+
except Exception as e:
187+
print(f" ✗ Error converting {nb_path.name}: {e}")
188+
189+
print("\nDone!")
190+
191+
192+
def create_examples_index(examples_dir):
193+
"""
194+
Create an index.md file listing all notebooks.
195+
196+
Args:
197+
examples_dir: Path to the docs/examples directory
198+
"""
199+
examples_path = Path(examples_dir)
200+
notebooks = sorted(examples_path.glob('*.ipynb'))
201+
java_files = sorted(examples_path.glob('*.java'))
202+
md_files = sorted([f for f in examples_path.glob('*.md')
203+
if f.name not in ['index.md', 'README.md']
204+
and not any(nb.stem == f.stem for nb in notebooks)])
205+
206+
content = """---
207+
layout: default
208+
title: "Examples"
209+
description: "NeqSim code examples and tutorials"
210+
nav_order: 5
211+
has_children: true
212+
---
213+
214+
# NeqSim Examples
215+
216+
This section contains tutorials, code examples, and Jupyter notebooks demonstrating NeqSim capabilities.
217+
218+
## Jupyter Notebook Tutorials
219+
220+
Interactive Python notebooks using NeqSim through [neqsim-python](https://github.com/equinor/neqsim-python):
221+
222+
| Notebook | Description | View Options |
223+
|----------|-------------|--------------|
224+
"""
225+
226+
for nb in notebooks:
227+
name = nb.stem
228+
title = name.replace('_', ' ').replace('-', ' ')
229+
230+
# Create links
231+
md_link = f"[Markdown]({name}.md)"
232+
nbviewer_link = f"[nbviewer](https://nbviewer.org/github/equinor/neqsim/blob/master/docs/examples/{name}.ipynb)"
233+
colab_link = f"[Colab](https://colab.research.google.com/github/equinor/neqsim/blob/master/docs/examples/{name}.ipynb)"
234+
github_link = f"[GitHub](https://github.com/equinor/neqsim/blob/master/docs/examples/{name}.ipynb)"
235+
236+
content += f"| **{title}** | See notebook for details | {md_link} \\| {nbviewer_link} \\| {colab_link} |\n"
237+
238+
if java_files:
239+
content += """
240+
## Java Examples
241+
242+
Example Java code demonstrating NeqSim APIs:
243+
244+
| Example | Description |
245+
|---------|-------------|
246+
"""
247+
for java_file in java_files:
248+
name = java_file.stem
249+
title = name.replace('_', ' ')
250+
github_link = f"https://github.com/equinor/neqsim/blob/master/docs/examples/{java_file.name}"
251+
content += f"| [{title}]({github_link}) | Java example |\n"
252+
253+
if md_files:
254+
content += """
255+
## Other Tutorials
256+
257+
Additional documentation and guides:
258+
259+
"""
260+
for md_file in md_files:
261+
name = md_file.stem
262+
title = name.replace('_', ' ').replace('-', ' ').title()
263+
content += f"- [{title}]({md_file.name})\n"
264+
265+
content += """
266+
---
267+
268+
## Running the Notebooks
269+
270+
### Prerequisites
271+
272+
1. Install neqsim-python:
273+
```bash
274+
pip install neqsim
275+
```
276+
277+
2. Or use Google Colab (click the Colab links above) - no installation needed!
278+
279+
### Local Jupyter Setup
280+
281+
```bash
282+
# Create a virtual environment
283+
python -m venv neqsim-env
284+
source neqsim-env/bin/activate # On Windows: neqsim-env\\Scripts\\activate
285+
286+
# Install dependencies
287+
pip install neqsim jupyter matplotlib pandas numpy
288+
289+
# Start Jupyter
290+
jupyter notebook
291+
```
292+
293+
Then open any of the `.ipynb` files from this directory.
294+
"""
295+
296+
index_path = examples_path / 'index.md'
297+
with open(index_path, 'w', encoding='utf-8') as f:
298+
f.write(content)
299+
300+
print(f"Created examples index: {index_path}")
301+
302+
303+
if __name__ == '__main__':
304+
# Get the docs/examples directory
305+
script_dir = Path(__file__).parent
306+
examples_dir = script_dir / 'examples'
307+
308+
print("NeqSim Notebook Converter")
309+
print("=" * 50)
310+
311+
# Convert all notebooks
312+
convert_all_notebooks(examples_dir)
313+
314+
print()
315+
316+
# Create index
317+
create_examples_index(examples_dir)

0 commit comments

Comments
 (0)