Styled Exercises in Jupyter Book (but still not JupyterLab…)

Pondering yet again, yet again, how to port our old notebooks+styling extensions from classic notebook to JupyterLab/RetroLab using off-the-shelf tools and hacks, and not having to become a hardcore Jupyter-core Typescript-core complex-development-environment download-an-internet’s-worth of devtools wtf-are-all-those-setup-and-config-files developer, I had a poke around what’s currently available (I’ve really slacked off Tracking Juypter over the last seems-like-forever).

One of the possible routes I’d explored before involved a couple of JupyterLab extensions, one for adding class attributes to rendered notebooks based on cell tags (rafelyall/celltag2dom ), the other for easily adding custom CSS (wallneradam/jupyterlab-custom-css ). Both these extensions fail to install in current JupyterLab, but that doesn’t really surpirse me. FWIW, I still consider to be a really hostile environment to casual, have-a-go end-user developers… ;-)

If notebook tags mapped onto rendered notebook HTML classes it would open up so many lightweight end-user development routes for notebook sensitive custom styling / rendering, even at just the “tinkering with CSS” level. There have been various PRs attempting this in the past, initially https://github.com/jupyterlab/jupyterlab/pull/8410, which was then deprecated in favour of https://github.com/jupyterlab/jupyterlab/pull/8627, but from what I can tell, that is still languishing in the PR queue. I’ve no idea if anything else has replaced it. From a quick skim of a notebook with tagged cells in JupyterLab (which took so long to load in my browser that JupyterLab popped up a message if I wanted to continue waiting, which suggests this is a not-just-me pain point with slowness with which JupyterLab loads (give me VS Code or RStudio any day for a Jupyer IDE…), I couldn’t spot any likely class attributes related to tags propagating through.

With things like the JupyterLab-MyST extension supporting some custom rendering via admonition blocks, we can get some custom block styling into the JupyterLab/Retrolab context, but still not to the extent we can with our current classic notebook extensions.

JupyterLab-MyST lets you preview rich MySt content in JupterLab and RetroLab environments

So using the old trick of “if you can’t solve the problem, change the problem”, I’ve start wondering again about how far we might get making instructional materials available via a Jupyter Book UI, which is much easier to work with. There are still blockers to this: whilst code can be executed within a Jupyter Book context using Thebe, and code can be edited and executed, there is still no way to save edits to browser storage (this should be acheivable: JupyterLite does it, and JupyterLite notebooks embedded in a Jupyter Book page using jupyterlite-sphinx). I’m guessing saving to and load from the host file system may be deemed a little riskier, although I think Chrome does have file system integration?

Another reason for working in the Jupyter Book / Sphinx environment is that you can quicky get strated developing your own custom UI features.

For example, a recipe described by @choldgraf on the Executable Books dicussion forum reveals that you can create custom styled admontition blocks with just a handful of files and a few lines of relatively simple Python:

  • in a new directory example, create a setup.py containing at least:
from setuptools import setup, find_packages

setup(
    name="custom-directive",
    packages=find_packages()
    )
  • in example/custom-directive create __init__.py containing the example code given in the example;
# Via: https://github.com/executablebooks/meta/discussions/655#discussion-3855822

from docutils.parsers.rst.directives.admonitions import Admonition

class Example(Admonition):
    def run(self):
        # Manually add a "tip" class to style it
        if "class" not in self.options:
            self.options["class"] = ["tip"]
        else:
            self.options["class"].append("tip")
        # Add `Example` to the title so we don't have to type it
        self.arguments[0] = f"Example: {self.arguments[0]}"
        # Now run the Admonition logic so it behaves the same way
        nodes = super().run()
        return nodes

def setup(app):
    app.add_directive("example", Example)
  • build and install your package: run pip install ./example
  • add the extension to you Jupyter Book _config.yml file, for example:
sphinx:
  extra_extensions:
    - custom-directive
  • build your Jupyter Book in the normal way: jupyter book build .

I also note that you can easily add your own custom CSS to Jupyter Book environments to provide custom styling for class attributes, the class attributes themselves being trivially set in admonition blocks via a :class: element, for example, or via a custom classed div element. As the docs describe, you can easily add a custom CSS file (eg my-custom-css-file.css) and then just place it in a static directory; the file(s) will then be automatically be copied into an appropriate location in the output book when the book is built:

├── _config.yml
├── _toc.yml
├── page1.md
└── _static
    └── my-custom-css-file.css

Take that, JupyterLab..! ;-)

Another approach is to consider embedding JupyterLite notebooks into an HTML text. One advantage of this approach is that the embedded notebook executes against an in-browser Pyhton environment rather than requiring a connection to a remote server or Binder environment; another is that changes to the notebook are saved to browser storage and will be available if you view the notebook again from the same browser (see, for example, Embedding JupyterLite In-Browser Notebooks in Documentation and Online Educational Materials). A downside is that the JupyterLite environment is a large download, which just add to the long start up time.

Perhaps the best solution, however, is the executablebooks/sphinx-exercise extension; but that’ll have to be the subject for another post, not least because I hit publish on this post rather too quickly!

Author: Tony Hirst

I'm a Senior Lecturer at The Open University, with an interest in #opendata policy and practice, as well as general web tinkering...

%d bloggers like this: