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.