Skip to content

Commit 3cded3e

Browse files
committed
Add nbsphinx code cell toggle blog
1 parent e748d93 commit 3cded3e

File tree

2 files changed

+129
-0
lines changed

2 files changed

+129
-0
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
---
2+
layout: post
3+
title: "Toggle Jupyter Code Cells in nbsphinx Documentation"
4+
date: 2023-12-20
5+
no_toc: true
6+
---
7+
8+
Gallery and cookbook sections punch way above their weight in software documentation.
9+
The best way to get started with something new is often with something that already works.
10+
11+
To keep content in sync and stay sane managing generated output files, some kind of tooling beyond manual updatesbecomes necessary.
12+
A Jupyter notebook would be great for this --- and, indeed, there's a nice existing ecosystem to marry notebooks and Sphinx documentation.
13+
Among other tools, `nbsphinx` will execute `.ipynb` files and inject the output into the documentation build.
14+
ReadTheDocs has [a great primer on the subject](https://docs.readthedocs.io/en/stable/guides/jupyter.html).
15+
16+
## Problem
17+
18+
Although the code should certainly be available, I've found it's nice to have it collapsed away to make gallery content more skimmable.
19+
Toggling seems like an ideal solution --- readers can pop open cells as they read along and click to expand snippets they're particularly interested.
20+
The nbsphinx package only supports [entirely-hidden cells](https://nbsphinx.readthedocs.io/en/0.9.3/hidden-cells.html).
21+
22+
Although there appears to be something a cottage industry of one-off code snippets related to this problem in interactive Jupyter notebooks, I came up empty trawling Google and StackOverflow for Sphinx-specific solutions.
23+
Eoin Travers in particular has a nice [blog post](http://eointravers.com/post/jupyter-toggle/) on toggle buttons in Jupyter notebooks by way of IPython JavaScript magic (i.e., `%%javascript`) for runtime injection of code into the notebook's DOM at runtime.
24+
Most of the other solutions I found were variations on this theme.
25+
26+
For compatibility with the `nbsphinx` pipeline, where notebooks execute only at build time and not in viewer's DOMs, I thought to take another tack to injecting buttons: having Sphinx bundle JavaScript with the built html docs.
27+
Sphinx has a straightforward hook for this.
28+
29+
After a little styling and animation fiddling, here's what it looks like in action.
30+
31+
![toggling notebook cell in sphinx docs](/resources/2023-12-20-nbsphinx-code-toggle.gif){:style="width: 40%;display:block; margin-left:auto; margin-right:auto;"}
32+
33+
To get things readable, I ended up adding `<hline>` breaks around code cells and hiding the cell numbers.
34+
35+
## Ctrl-C Ctrl-V
36+
37+
Anyway, here's the code you'll want.
38+
39+
add to `docs/conf.py`:
40+
```python
41+
# -- Options for HTML output -------------------------------------------------
42+
43+
# Add any paths that contain custom static files (such as style sheets) here,
44+
# relative to this directory. They are copied after the builtin static files,
45+
# so a file named "default.css" will overwrite the builtin "default.css".
46+
html_static_path = ["_static"]
47+
48+
html_js_files = [
49+
"hide_code_cells.js",
50+
]
51+
```
52+
53+
`docs/_static/hide_code_cells.js`:
54+
```javascript
55+
document.addEventListener("DOMContentLoaded", () => {
56+
// Function to create and append a toggle button to a div
57+
const addToggleButton = (div) => {
58+
const button = document.createElement("button");
59+
button.textContent = "Show Code »";
60+
styleButton(button);
61+
initializeDivStyle(div);
62+
addClickEvent(button, div);
63+
insertButtonAboveDiv(div, button);
64+
};
65+
66+
// Styling the toggle button
67+
const styleButton = (button) => {
68+
Object.assign(button.style, {
69+
backgroundColor: "#F5F5F5",
70+
color: "#333333",
71+
border: "1px solid #DDD",
72+
padding: "2px 5px",
73+
marginTop: "5px",
74+
cursor: "pointer",
75+
borderRadius: "3px",
76+
fontSize: "0.9em",
77+
width: "100%",
78+
maxWidth: "100px",
79+
transition: "max-width 0.5s ease, background-color 1s ease"
80+
});
81+
};
82+
83+
// Initialize div style for hiding
84+
const initializeDivStyle = (div) => {
85+
Object.assign(div.style, {
86+
opacity: '0',
87+
maxHeight: '0px',
88+
overflow: 'hidden',
89+
transition: 'opacity 0.2s ease, max-height 0.6s ease'
90+
});
91+
};
92+
93+
// Handle click event for the toggle button
94+
const addClickEvent = (button, div) => {
95+
button.onclick = () => {
96+
if (div.style.maxHeight === '0px') {
97+
Object.assign(div.style, {
98+
opacity: '1',
99+
maxHeight: '500px',
100+
});
101+
button.textContent = "»» Hide Code ««";
102+
button.style.maxWidth = "100%";
103+
} else {
104+
Object.assign(div.style, {
105+
opacity: '0',
106+
maxHeight: '0px'
107+
});
108+
button.textContent = "Show Code »";
109+
button.style.maxWidth = "100px";
110+
}
111+
};
112+
};
113+
114+
// Insert the toggle button above the div
115+
const insertButtonAboveDiv = (div, button) => {
116+
const hr = document.createElement("hr");
117+
div.parentNode.insertBefore(hr, div);
118+
div.parentNode.insertBefore(button, div);
119+
};
120+
121+
// Select and apply toggle functionality to code and output divs
122+
const codeDivs = document.querySelectorAll('.nbinput.docutils.container');
123+
codeDivs.forEach(addToggleButton);
124+
125+
// Hide cell numbers (which don't play nice with button layout)
126+
const outputDivs = document.querySelectorAll('.prompt');
127+
outputDivs.forEach(div => div.style.display = 'none');
128+
});
129+
```
624 KB
Loading

0 commit comments

Comments
 (0)