Skip to content

Commit e6a960d

Browse files
feat: link contents to .ipynb and expand LaTeX macros before conversion
- Point the "Contents" and "Exercise Answers" tables in basic_lessons/README.md at the generated .ipynb files instead of the .md sources, so readers land on the executable notebooks. - Add convert_to_ipynb.py: a two-step .md -> intermediate.md -> .ipynb pipeline that first expands the project.math LaTeX macros (myvec/mymatrix/quat/dual) into an intermediate .md, then runs jupytext on that file. Most .ipynb renderers cannot draw custom macros, so the generated notebooks now contain only standard LaTeX and render anywhere. The intermediate file is a transient build artifact (deleted after conversion; --keep retains it). Expansion mirrors KaTeX/TeX macro semantics (braced args incl. nested braces, single space-separated tokens, no-arg macros) and leaves {code-cell} bodies untouched so Python source is never corrupted. - Wire the script into the CI "Generate downloadable notebooks" step. - Document the new pipeline in AGENTS.md. Co-authored-by: openhands <openhands@all-hands.dev>
1 parent 5d28279 commit e6a960d

4 files changed

Lines changed: 294 additions & 34 deletions

File tree

.github/workflows/notebook_to_html.yml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,16 @@ jobs:
3333
# Checks out your repository under $GITHUB_WORKSPACE, so your job can access it
3434
- uses: actions/checkout@v4
3535

36-
# Convert basic_lessons/*.md → basic_lessons/*.ipynb so the site provides "Download notebook" buttons
36+
# Convert basic_lessons/*.md → basic_lessons/*.ipynb so the site provides "Download notebook" buttons.
37+
# The .md uses custom LaTeX macros (see `project.math` in myst.yml) that most .ipynb
38+
# renderers cannot draw, so convert_to_ipynb.py first expands them into an intermediate
39+
# .md (which is deleted afterwards) and then runs jupytext on that.
3740
# https://jupytext.readthedocs.io/ — supports md:myst format natively
3841
# Must run BEFORE build so myst/jupyter-book can pick up the generated notebooks
3942
- name: Generate downloadable notebooks
4043
run: |
41-
pip install jupytext
42-
for f in basic_lessons/lesson*_tutorial.md basic_lessons/lesson*_exercise_answers.md; do
43-
[ -f "$f" ] || continue
44-
out="${f%.md}.ipynb"
45-
python -m jupytext --from md:myst --to notebook --output "$out" "$f"
46-
echo "Generated: $out"
47-
done
44+
pip install jupytext pyyaml
45+
python convert_to_ipynb.py
4846
4947
# Runs a set of commands using the runner's shell
5048
# https://mystmd.org/guide/deployment-github-pages#fn-except-custom-domains

AGENTS.md

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
|------|---------|
1313
| `basic_lessons/` | Canonical source: MyST text notebooks (`.md` with `{code-cell}` directives) — 6 tutorials + 5 exercise answer keys |
1414
| `basic_lessons/.gitignore` | Excludes generated `.ipynb` files (produced at build time) |
15+
| `convert_to_ipynb.py` | Builds the downloadable `.ipynb` files: `.md` → intermediate `.md` (LaTeX macros expanded) → `.ipynb` |
1516
| `other/` | Supplementary content (e.g. `dqrobotics.md`) |
1617
| `myst.yml` | MyST project config (root): LaTeX macros, TOC, site options |
1718
| `build_html.sh` | Build script for `jupyter-book` |
@@ -59,18 +60,24 @@ x = np.array([1, 2, 3])
5960

6061
### Downloadable `.ipynb` from `.md` notebooks
6162

62-
The `basic_lessons/` `.md` files are the canonical source. `.ipynb` files are generated at build time so visitors can download them:
63+
The `basic_lessons/` `.md` files are the canonical source. `.ipynb` files are generated at build time so visitors can download them. The conversion is a **two-step pipeline** run by `convert_to_ipynb.py`:
6364

64-
1. **CI pipeline** (`.github/workflows/notebook_to_html.yml`) runs `jupytext --from md:myst --to notebook` before the MyST build, converting each `basic_lessons/*.md``basic_lessons/*.ipynb`.
65-
2. **`myst.yml` TOC** references the generated `.ipynb` for the lesson section — MyST renders these identically to the `.md` but provides native "Download notebook" buttons.
66-
3. **`basic_lessons/.gitignore`** excludes `.ipynb` so only `.md` is tracked in git.
65+
```
66+
basic_lessons/*.md -> <stem>_expanded.md (LaTeX macros expanded) -> basic_lessons/*.ipynb
67+
```
68+
69+
1. **CI pipeline** (`.github/workflows/notebook_to_html.yml`) runs `python convert_to_ipynb.py` before the MyST build.
70+
2. **Why the intermediate step?** The lessons use custom LaTeX macros (e.g. `\myvec{q}`) that MyST expands at build time by passing them to KaTeX. Most standalone `.ipynb` renderers (JupyterLab, VS Code, nbviewer, ...) do **not** know these macros and would show them literally. So `convert_to_ipynb.py` first expands every macro to its definition from `myst.yml` (`project.math`), writing a temporary `<stem>_expanded.md`; `jupytext` then converts that expanded file to `.ipynb`. The generated `.ipynb` therefore contains **only standard LaTeX** and renders anywhere. You keep writing the original `.md` with the convenient macros.
71+
3. **`myst.yml` TOC** references the generated `.ipynb` for the lesson section — MyST renders these identically to the `.md` but provides native "Download notebook" buttons.
72+
4. **`basic_lessons/.gitignore`** excludes `.ipynb` so only `.md` is tracked in git.
73+
74+
The intermediate `<stem>_expanded.md` files are also build artifacts — deleted after conversion (use `--keep` to inspect them). They are not tracked and not referenced in `myst.yml`.
6775

6876
To generate locally (e.g. for testing):
6977
```bash
70-
pip install jupytext
71-
for f in basic_lessons/lesson*_tutorial.md basic_lessons/lesson*_exercise_answers.md; do
72-
python -m jupytext --from md:myst --to notebook --output "${f%.md}.ipynb" "$f"
73-
done
78+
pip install jupytext pyyaml
79+
python convert_to_ipynb.py # all lessons
80+
python convert_to_ipynb.py --keep # keep the intermediate _expanded.md files
7481
```
7582

7683
### Image references
@@ -101,10 +108,8 @@ pip install jupyter-book --pre
101108
**jupyter-book build (CI pipeline):**
102109
```bash
103110
# Step 1: Generate .ipynb from .md (required for download buttons)
104-
pip install jupytext
105-
for f in basic_lessons/lesson*_tutorial.md basic_lessons/lesson*_exercise_answers.md; do
106-
python -m jupytext --from md:myst --to notebook --output "${f%.md}.ipynb" "$f"
107-
done
111+
pip install jupytext pyyaml
112+
python convert_to_ipynb.py
108113

109114
# Step 2: Build the site
110115
chmod +x build_html.sh
@@ -142,7 +147,7 @@ warning classes seen so far (all fixed in #11):
142147
### CI/CD
143148

144149
The GitHub Actions workflow (`.github/workflows/notebook_to_html.yml`) runs on pushes to `main` and on pull requests:
145-
1. Generates `.ipynb` from `.md` using jupytext
150+
1. Generates `.ipynb` from `.md` via `python convert_to_ipynb.py` (expanding LaTeX macros into an intermediate `.md` first)
146151
2. Runs `./build_html.sh` (jupyter-book pipeline)
147152
3. Uploads `_build/html/` as Pages artifact
148153
4. Deploys to GitHub Pages
@@ -174,6 +179,12 @@ Thank you! Please report it at https://github.com/MarinhoLab/OpenExecutableBooks
174179
- `\quat{}` for quaternions
175180
- `\dual{}` for dual numbers
176181

182+
These are defined in `myst.yml` under `project.math` (KaTeX `#1` substitutions) and
183+
are expanded by MyST when building the site. Use them freely in the `.md` source —
184+
`convert_to_ipynb.py` expands them into the downloadable `.ipynb` so the notebooks
185+
also render in standalone viewers. **Code cells are left untouched** (a macro name
186+
in a Python comment/variable is never expanded).
187+
177188
### Image references:
178189
- Use relative paths: `![alt](Lesson4.png)` (relative to `basic_lessons/`)
179190
- Images live alongside the lesson files in `basic_lessons/`.
@@ -193,6 +204,7 @@ Thank you! Please report it at https://github.com/MarinhoLab/OpenExecutableBooks
193204

194205
### Files excluded from version control:
195206
- `basic_lessons/*.ipynb` — Generated at build time from `.md` files (via `basic_lessons/.gitignore`)
207+
- `basic_lessons/*_expanded.md` — transient intermediate files from `convert_to_ipynb.py` (deleted after conversion; `--keep` retains them for inspection)
196208

197209
There is intentionally no root `.gitignore`. If you create local `venv/` or `_build/`
198210
directories, keep them out of commits (e.g. via `.git/info/exclude`).
@@ -207,6 +219,6 @@ directories, keep them out of commits (e.g. via `.git/info/exclude`).
207219
4. Use `{code-cell}` directives for Python code blocks
208220
5. Use `%%capture` on `%pip install` cells to suppress output
209221
6. Update `myst.yml` — add new file(s) to `project.toc` list as `.ipynb` (generated at build time)
210-
7. Update `basic_lessons/README.md` — add the new lesson to the contents table
211-
8. Test: `./build_html.sh` from the repository root
222+
7. Update `basic_lessons/README.md` — add the new lesson to the contents table, linking to the generated `.ipynb` (not the `.md`)
223+
8. Test: run `python convert_to_ipynb.py` to regenerate the `.ipynb`, then `./build_html.sh` from the repository root
212224
9. Open PR with descriptive title and body

basic_lessons/README.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,19 @@ The reader is expected to follow it sequentially.
1515

1616
| Number | Title and Link | Content |
1717
|--------|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
18-
| 0 | [](./lesson0_tutorial.md) | Setting up the virtual environment and installing all required dependencies. |
19-
| 1 | [](./lesson1_tutorial.md) | Basic operations in Python and `numpy` |
20-
| 2 | [](./lesson2_tutorial.md) | Learn about elements and operations in $\mathbb{R}^n$, $SO(n)$, and $SE(n)$ with $n\in{\{2,3\}}$ related to positions, orientations, and poses, respectively. |
21-
| 3 | [](./lesson3_tutorial.md) | Learn about the composition of rigid body motion in series to obtain the forward kinematics model of a robotic manipulator, mapping their configuration space $\myvec{q}\in\mathbb{R}^n$ into their task space $\myvec{x}\in\mathbb{R}^m$. |
22-
| 4 | [](./lesson4_tutorial.md) | Learn about the first-order differential mapping $\dot{\myvec{x}}=\mymatrix{J}\dot{\myvec{q}}$ between joint space and task space velocities through the calculation of the Jacobian $\mymatrix{J}$. |
23-
| 5 | [](./lesson5_tutorial.md) | Employ the previous knowledge in all previous lessons to employ a Lyapunov-stable control law to move a manipulator in task space using configuration-space signals. |
18+
| 0 | [](./lesson0_tutorial.ipynb) | Setting up the virtual environment and installing all required dependencies. |
19+
| 1 | [](./lesson1_tutorial.ipynb) | Basic operations in Python and `numpy` |
20+
| 2 | [](./lesson2_tutorial.ipynb) | Learn about elements and operations in $\mathbb{R}^n$, $SO(n)$, and $SE(n)$ with $n\in{\{2,3\}}$ related to positions, orientations, and poses, respectively. |
21+
| 3 | [](./lesson3_tutorial.ipynb) | Learn about the composition of rigid body motion in series to obtain the forward kinematics model of a robotic manipulator, mapping their configuration space $\myvec{q}\in\mathbb{R}^n$ into their task space $\myvec{x}\in\mathbb{R}^m$. |
22+
| 4 | [](./lesson4_tutorial.ipynb) | Learn about the first-order differential mapping $\dot{\myvec{x}}=\mymatrix{J}\dot{\myvec{q}}$ between joint space and task space velocities through the calculation of the Jacobian $\mymatrix{J}$. |
23+
| 5 | [](./lesson5_tutorial.ipynb) | Employ the previous knowledge in all previous lessons to employ a Lyapunov-stable control law to move a manipulator in task space using configuration-space signals. |
2424

2525
### Exercise Answers
2626

2727
| Lesson | Link |
2828
|--------|------|
29-
| L1 | [](./lesson1_exercise_answers.md) |
30-
| L2 | [](./lesson2_exercise_answers.md) |
31-
| L3 | [](./lesson3_exercise_answers.md) |
32-
| L4 | [](./lesson4_exercise_answers.md) |
33-
| L5 | [](./lesson5_exercise_answers.md) |
29+
| L1 | [](./lesson1_exercise_answers.ipynb) |
30+
| L2 | [](./lesson2_exercise_answers.ipynb) |
31+
| L3 | [](./lesson3_exercise_answers.ipynb) |
32+
| L4 | [](./lesson4_exercise_answers.ipynb) |
33+
| L5 | [](./lesson5_exercise_answers.ipynb) |

0 commit comments

Comments
 (0)