|
| 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 | +{: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 | +``` |
0 commit comments