Skip to content

mkdocs plugin for jupyter notebooks¤

i think i want more control of how mkdocs renders notebooks. i've been using mkdocs-jupyter for a while and it is great, but i need more knobs.

my particular need is to configure nbconvert exporters with more fine grain control than what mkdocs-jupyter offers. the major difference is we are going to target markdown output rather than html.

checklist for successfully integrating the plugin¤

steps to adding a mkdocs plugin to this the tonyfast project:

  • add plugin to mkdocs.yml
plugins:
    - markdown_notebook
  • define plugin entry point for tonyfast
[project.entry-points."mkdocs.plugins"]
markdown_notebook = "tonyfast.mkdocs:MarkdownNotebook"
  • build the MarkdownNotebook plugin
  • integrate the plugin
from tonyfast.mkdocs import MarkdownNotebook
  • add and improve the nbconvert export display renderers

building the mkdocs plugin¤

    import json, nbconvert, nbformat, pathlib, mkdocs.plugins, warnings, re, functools
    warnings.filterwarnings("ignore", category=DeprecationWarning)

the [mkdocs plugin]

we now use a custom template and expo defined in 2022-12-31-markdownish-notebook.ipynb

        with __import__("importnb").Notebook():
            from tonyfast.xxii.__markdownish_notebook import template, PidgyExporter
    class MarkdownNotebook(mkdocs.plugins.BasePlugin):
        exporter_cls = PidgyExporter
        config_scheme = (
            # ('foo', mkdocs.config.config_options.Type(str, default='a default value')),
        )

        @functools.lru_cache
        def get_exporter(self, key="mkdocs", **kw):
            with __import__("importnb").Notebook():
                from tonyfast.xxii.__markdownish_notebook import template, HEAD, replace_attachments
            kw.setdefault("template_file", key)
            exporter = self.exporter_cls(**kw)
            exporter.environment.filters.setdefault("attachment", replace_attachments)
            from jinja2 import DictLoader
            for loader in exporter.environment.loader.loaders:
                if isinstance(loader, DictLoader):
                    loader.mapping[key] = template
                    loader.mapping["HEAD"] = HEAD
                    break
            return exporter

        def on_page_read_source(self, page, config):
            if page.file.is_modified():
                if page.file.src_uri.endswith((".ipynb", )):
                    body = pathlib.Path(page.file.abs_src_path).read_text()
                    nb = nbformat.v4.reads(body)
                    exporter = self.get_exporter()
                    return "\n".join((
                        "---", json.dumps(nb.metadata),"---", # add metadata as front matter
                        exporter.from_notebook_node(nb)[0]
                    ))

        def on_post_page(self, output, page, config):
            if '<script type="application/vnd.jupyter.widget-view+json">' in output:
                left, sep, right = output.partition("</head")
                exporter = self.get_exporter()
                return left + self.get_exporter().environment.get_template("HEAD").render(resources=dict(
                            jupyter_widgets_base_url=exporter.jupyter_widgets_base_url, 
                            html_manager_semver_range=exporter.html_manager_semver_range, 
                            widget_renderer_url=exporter.widget_renderer_url,
                            require_js_url=exporter.require_js_url
                        )) + sep + right

        def on_page_markdown(self, markdown, page, config, files):
            import markdown
            title = markdown.Markdown(extensions=config['markdown_extensions']).convert(page.title)
            page.title = title[len("<p>"):-len("</p>")].strip().replace("code>", "pre>")

we trick mkdocs into thinking notebook files are markdown extensions.

    mkdocs.utils.markdown_extensions += ".ipynb", # this feels naughty.