From dd29cacda057007cb1e23f33874d118598205883 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Sat, 4 Jan 2020 18:45:37 +0100 Subject: [PATCH] Add gallery feature Closes #254. --- .gitignore | 1 + doc/a-normal-rst-file.rst | 40 ++++ doc/conf.py | 9 + doc/gallery/cell-metadata.ipynb | 120 ++++++++++ doc/gallery/cell-tag.ipynb | 81 +++++++ doc/gallery/due-rst.ipynb | 46 ++++ doc/gallery/multiple-outputs.ipynb | 79 +++++++ doc/gallery/no-thumbnail.ipynb | 46 ++++ doc/gallery/thumbnail-from-conf-py.ipynb | 123 ++++++++++ doc/gallery/uno-rst.ipynb | 46 ++++ doc/requirements.txt | 1 + doc/subdir/gallery.ipynb | 122 ++++++++++ doc/usage.ipynb | 15 ++ src/nbsphinx.py | 287 +++++++++++++++++++++-- 14 files changed, 1000 insertions(+), 16 deletions(-) create mode 100644 doc/gallery/cell-metadata.ipynb create mode 100644 doc/gallery/cell-tag.ipynb create mode 100644 doc/gallery/due-rst.ipynb create mode 100644 doc/gallery/multiple-outputs.ipynb create mode 100644 doc/gallery/no-thumbnail.ipynb create mode 100644 doc/gallery/thumbnail-from-conf-py.ipynb create mode 100644 doc/gallery/uno-rst.ipynb create mode 100644 doc/subdir/gallery.ipynb diff --git a/.gitignore b/.gitignore index 24da0617..0168d91e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ nbsphinx.egg-info/ .python-version .vscode doc/_build +doc/gallery/a-local-file.png diff --git a/doc/a-normal-rst-file.rst b/doc/a-normal-rst-file.rst index cb6c6f9a..185bac9d 100644 --- a/doc/a-normal-rst-file.rst +++ b/doc/a-normal-rst-file.rst @@ -217,3 +217,43 @@ see https://github.com/mcmtroffaes/sphinxcontrib-bibtex/issues/156. There is an alternative Sphinx extension for creating bibliographies: https://bitbucket.org/wnielson/sphinx-natbib/. However, this project seems to be abandoned (last commit in 2011). + + +Thumbnail Galleries +------------------- + +With ``nbsphinx`` you can create thumbnail galleries in notebook files +as described in :ref:`/subdir/gallery.ipynb`. + +If you like, you can also create such galleries in reST files +using the ``nbgallery`` directive. + +It takes the same parameters as the `toctree`__ directive. + +__ https://www.sphinx-doc.org/en/master/usage/restructuredtext/ + directives.html#directive-toctree + +.. note:: + + The notes regarding LaTeX in :ref:`/subdir/gallery.ipynb` + and :ref:`/subdir/toctree.ipynb` also apply here! + +The following example gallery was created using: + +.. code-block:: rest + + .. nbgallery:: + :caption: This is a thumbnail gallery: + :name: rst-gallery + :glob: + :reversed: + + gallery/*-rst + +.. nbgallery:: + :caption: This is a thumbnail gallery: + :name: rst-gallery + :glob: + :reversed: + + gallery/*-rst diff --git a/doc/conf.py b/doc/conf.py index 44bf94a4..c399a37f 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -14,6 +14,10 @@ 'sphinxcontrib.rsvgconverter', # for SVG->PDF conversion in LaTeX output ] +import sphinx_gallery +html_static_path = [sphinx_gallery.glr_path_static()] +html_css_files = ['gallery.css'] + # Exclude build directory and Jupyter backup files: exclude_patterns = ['_build', '**.ipynb_checkpoints'] @@ -35,6 +39,11 @@ # Environment variables to be passed to the kernel: os.environ['MY_DUMMY_VARIABLE'] = 'Hello from conf.py!' +nbsphinx_thumbnails = { + 'gallery/thumbnail-from-conf-py': 'gallery/a-local-file.png', + 'gallery/*-rst': '_static/copy-button.svg', +} + # This is processed by Jinja2 and inserted before each notebook nbsphinx_prolog = r""" {% set docname = 'doc/' + env.doc2path(env.docname, base=None) %} diff --git a/doc/gallery/cell-metadata.ipynb b/doc/gallery/cell-metadata.ipynb new file mode 100644 index 00000000..fbb5a627 --- /dev/null +++ b/doc/gallery/cell-metadata.ipynb @@ -0,0 +1,120 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "This notebook is part of the `nbsphinx` documentation: https://nbsphinx.readthedocs.io/." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using Cell Metadata to Select a Thumbnail\n", + "\n", + "If the [nbsphinx-thumbnail](cell-tag.ipynb) cell tag is not enough,\n", + "you can use cell metadata to specify more options.\n", + "\n", + "The last cell in this notebook has this metadata:\n", + "\n", + "```json\n", + "{\n", + " \"nbsphinx-thumbnail\": {\n", + " \"tooltip\": \"This tooltip message was defined in cell metadata\"\n", + " }\n", + "}\n", + "```\n", + "\n", + "If there are multiple outputs in the selected cell,\n", + "the last one is used.\n", + "See [Choosing from Multiple Outputs](multiple-outputs.ipynb)\n", + "for how to select a specific output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.rcParams['image.cmap'] = 'coolwarm'\n", + "plt.rcParams['image.origin'] = 'lower'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Some example data stolen from\n", + "https://matplotlib.org/examples/pylab_examples/pcolor_demo.html:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x, y = np.meshgrid(np.arange(-3, 3, 0.1), np.arange(-2, 2, 0.1))\n", + "z = (1 - x / 2 + x ** 5 + y ** 3) * np.exp(-x ** 2 - y ** 2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "zmax = np.max(np.abs(z))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx-thumbnail": { + "tooltip": "This tooltip message was defined in cell metadata" + } + }, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "ax.imshow(z, vmin=-zmax, vmax=zmax)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/doc/gallery/cell-tag.ipynb b/doc/gallery/cell-tag.ipynb new file mode 100644 index 00000000..79d0fdba --- /dev/null +++ b/doc/gallery/cell-tag.ipynb @@ -0,0 +1,81 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "This notebook is part of the `nbsphinx` documentation: https://nbsphinx.readthedocs.io/." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Using a Cell Tag to Select a Thumbnail\n", + "\n", + "You can select any code cell (with appropriate output)\n", + "by tagging it with the `nbsphinx-thumbnail` tag.\n", + "\n", + "If there are multiple outputs in the selected cell,\n", + "the last one is used.\n", + "See [Choosing from Multiple Outputs](multiple-outputs.ipynb)\n", + "for how to select a specific output.\n", + "If you want to show a tooltip, have a look at\n", + "[Using Cell Metadata to Select a Thumbnail](cell-metadata.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following cell has the `nbsphinx-thumbnail` tag:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "nbsphinx-thumbnail" + ] + }, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(figsize=[6, 3])\n", + "ax.plot([4, 9, 7, 20, 6, 33, 13, 23, 16, 62, 8])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/doc/gallery/due-rst.ipynb b/doc/gallery/due-rst.ipynb new file mode 100644 index 00000000..c6922c1a --- /dev/null +++ b/doc/gallery/due-rst.ipynb @@ -0,0 +1,46 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "This notebook is part of the `nbsphinx` documentation: https://nbsphinx.readthedocs.io/." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Dummy Notebook 2 for Gallery\n", + "\n", + "This is a dummy file just to fill\n", + "[the gallery in the reST file](../a-normal-rst-file.rst#thumbnail-galleries).\n", + "\n", + "The thumbnail image is assigned in [conf.py](../conf.py)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/doc/gallery/multiple-outputs.ipynb b/doc/gallery/multiple-outputs.ipynb new file mode 100644 index 00000000..94969cd8 --- /dev/null +++ b/doc/gallery/multiple-outputs.ipynb @@ -0,0 +1,79 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "This notebook is part of the `nbsphinx` documentation: https://nbsphinx.readthedocs.io/." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Choosing from Multiple Outputs\n", + "\n", + "By default, the last output of the selected cell is used as a thumbnail.\n", + "If that's what you want, you can simply use the\n", + "[nbsphinx-thumbnail](cell-tag.ipynb) cell tag.\n", + "\n", + "If you want to specify one of multiple outputs,\n", + "you can add a (zero-based) `\"output-index\"`\n", + "to your `\"nbsphinx-thumbnail\"` cell metadata.\n", + "\n", + "The following cell has this metadata,\n", + "selecting the third output to be used as thumbnail in\n", + "[the gallery](../subdir/gallery.ipynb).\n", + "\n", + "```json\n", + "{\n", + " \"nbsphinx-thumbnail\": {\n", + " \"output-index\": 2\n", + " }\n", + "}\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx-thumbnail": { + "output-index": 2 + } + }, + "outputs": [], + "source": [ + "from IPython.display import Image\n", + "\n", + "display(Image(url='https://jupyter.org/assets/nav_logo.svg'))\n", + "print('Hello!')\n", + "display(Image(filename='../images/notebook_icon.png'))\n", + "display(Image(url='https://www.python.org/static/img/python-logo-large.png', embed=True))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/doc/gallery/no-thumbnail.ipynb b/doc/gallery/no-thumbnail.ipynb new file mode 100644 index 00000000..f5f584ed --- /dev/null +++ b/doc/gallery/no-thumbnail.ipynb @@ -0,0 +1,46 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "This notebook is part of the `nbsphinx` documentation: https://nbsphinx.readthedocs.io/." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# A Notebook without Thumbnail\n", + "\n", + "This notebook doesn't contain any thumbnail metadata.\n", + "\n", + "It should be displayed with the default thumbnail image in the\n", + "[gallery](../subdir/gallery.ipynb)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/doc/gallery/thumbnail-from-conf-py.ipynb b/doc/gallery/thumbnail-from-conf-py.ipynb new file mode 100644 index 00000000..b656bd92 --- /dev/null +++ b/doc/gallery/thumbnail-from-conf-py.ipynb @@ -0,0 +1,123 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "This notebook is part of the `nbsphinx` documentation: https://nbsphinx.readthedocs.io/." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Specifying Thumbnails in `conf.py`\n", + "\n", + "This notebook doesn't contain any thumbnail metadata.\n", + "\n", + "But in the file [conf.py](../conf.py),\n", + "a thumbnail is specified (via the\n", + "[nbsphinx_thumbnails](../usage.ipynb#nbsphinx_thumbnails)\n", + "option),\n", + "which will be used in the [gallery](../subdir/gallery.ipynb).\n", + "\n", + "The keys in the `nbsphinx_thumbnails` dictionary can contain wildcards,\n", + "which behave very similarly to the\n", + "[html_sidebars](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_sidebars)\n", + "option.\n", + "\n", + "The thumbnail files can be local image files somewhere in the source directory,\n", + "but you'll need to create at least one\n", + "[link](../markdown-cells.ipynb#Links-to-Local-Files)\n", + "to them in order to copy them to the HTML output directory.\n", + "\n", + "You can also use files from the `_static` directory\n", + "(which contains all files in your [html_static_path](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_static_path)).\n", + "\n", + "If you want, you can also use files from the `_images` directory,\n", + "which contains all notebook outputs." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To demonstrate this feature,\n", + "we are creating an image file here:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib agg" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots()\n", + "ax.plot([4, 8, 15, 16, 23, 42])\n", + "fig.savefig('a-local-file.png')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Please note that the previous cell doesn't have any outputs,\n", + "but it has generated a file named `a-local-file.png` in the notebook's directory.\n", + "\n", + "We have to create a link to this file (which is a good idea anyway):\n", + "[a-local-file.png](a-local-file.png).\n", + "\n", + "Now we can use this file in our [conf.py](../conf.py) like this:\n", + "\n", + "```python\n", + "nbsphinx_thumbnails = {\n", + " 'gallery/thumbnail-from-conf-py': 'gallery/a-local-file.png',\n", + "}\n", + "```\n", + "\n", + "Please note that the notebook name does *not* contain the `.ipynb` suffix." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/doc/gallery/uno-rst.ipynb b/doc/gallery/uno-rst.ipynb new file mode 100644 index 00000000..3d93d8ac --- /dev/null +++ b/doc/gallery/uno-rst.ipynb @@ -0,0 +1,46 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "This notebook is part of the `nbsphinx` documentation: https://nbsphinx.readthedocs.io/." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Dummy Notebook 1 for Gallery\n", + "\n", + "This is a dummy file just to fill\n", + "[the gallery in the reST file](../a-normal-rst-file.rst#thumbnail-galleries).\n", + "\n", + "The thumbnail image is assigned in [conf.py](../conf.py)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/doc/requirements.txt b/doc/requirements.txt index 527c134d..b2b93907 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -6,3 +6,4 @@ sphinxcontrib-bibtex sphinxcontrib-svg2pdfconverter ipywidgets sphinx-copybutton +sphinx-gallery diff --git a/doc/subdir/gallery.ipynb b/doc/subdir/gallery.ipynb new file mode 100644 index 00000000..6d8d1c62 --- /dev/null +++ b/doc/subdir/gallery.ipynb @@ -0,0 +1,122 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "This notebook is part of the `nbsphinx` documentation: https://nbsphinx.readthedocs.io/." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Creating Thumbnail Galleries\n", + "\n", + "Inspired by [Sphinx-Gallery](https://sphinx-gallery.github.io/),\n", + "you can create thumbnail galleries from a list of Jupyter notebooks\n", + "(or other Sphinx source files).\n", + "\n", + "`nbsphinx` does *not* provide any gallery styles,\n", + "but you can easily use the styles from Sphinx-Gallery\n", + "by installing it:\n", + "\n", + " python3 -m pip install sphinx-gallery --user\n", + "\n", + "... and loading the styles in your `conf.py` with:\n", + "\n", + "```python\n", + "import sphinx_gallery\n", + "html_static_path = [sphinx_gallery.glr_path_static()]\n", + "html_css_files = ['gallery.css']\n", + "```\n", + "\n", + "However, you can also create your own CSS styles if you prefer\n", + "(then you don't need to install Sphinx-Gallery).\n", + "You can load your CSS files with\n", + "[html_css_files](https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_css_files).\n", + "\n", + "You can create\n", + "[Thumbnail Galleries in reST Files](../a-normal-rst-file.rst#thumbnail-galleries)\n", + "and you can create galleries by adding the `\"nbsphinx-gallery\"`\n", + "cell tag or metadata to notebooks,\n", + "which is identical to the\n", + "[\"nbsphinx-toctree\"](toctree.ipynb) cell tag/metadata.\n", + "\n", + "For possible options, see the [toctree](toctree.ipynb) notebook.\n", + "\n", + "
\n", + "\n", + "Note\n", + "\n", + "In LaTeX output this behaves just like ``toctree``,\n", + "i.e. no thumbnail gallery is shown,\n", + "but the linked files are included in the document.\n", + "\n", + "Like with ``toctree`` you should avoid adding content\n", + "after a gallery (except other toctrees and galleries)\n", + "because this content would appear in the LaTeX output\n", + "*after* the content of all included source files,\n", + "which is probably not what you want.\n", + "\n", + "
\n", + "\n", + "The following cell has the `\"nbsphinx-gallery\"` tag,\n", + "which creates a thumbnail gallery.\n", + "The *first* section title in that cell (if available)\n", + "is used as `\"caption\"` (unless it's given in the metadata).\n", + "\n", + "The notebooks in the following gallery describe different ways\n", + "how to select which images are used as thumbnails." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [ + "nbsphinx-gallery" + ] + }, + "source": [ + "This section title will be used as ``:caption:``:\n", + "\n", + "## This is a thumbnail gallery:\n", + "\n", + "This line will be ignored.\n", + "\n", + "* [Using a Cell Tag to Select a Thumbnail](../gallery/cell-tag.ipynb)\n", + "* [Using Cell Metadata to Select a Thumbnail](../gallery/cell-metadata.ipynb)\n", + "* [Choosing from Multiple Outputs](../gallery/multiple-outputs.ipynb)\n", + "* [No Thumbnail Available](../gallery/no-thumbnail.ipynb)\n", + "* [Specifying a Thumbnail File](../gallery/thumbnail-from-conf-py.ipynb)\n", + "\n", + "## This section title will be ignored\n", + "\n", + "... because only the first title in this cell is used." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/doc/usage.ipynb b/doc/usage.ipynb index f65b97b6..7d46d94a 100644 --- a/doc/usage.ipynb +++ b/doc/usage.ipynb @@ -372,6 +372,21 @@ "Any CSS length can be specified." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### `nbsphinx_thumbnails`\n", + "\n", + "A dictionary mapping from a document name\n", + "(i.e. source file without suffix but with path)\n", + "-- optionally containing wildcards --\n", + "to a thumbnail path to be used in a\n", + "[thumbnail gallery](subdir/gallery.ipynb).\n", + "\n", + "See [Specifying Thumbnails](gallery/thumbnail-from-conf-py.ipynb)." + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/src/nbsphinx.py b/src/nbsphinx.py index f3829541..202b5e02 100644 --- a/src/nbsphinx.py +++ b/src/nbsphinx.py @@ -26,7 +26,7 @@ __version__ = '0.5.1' import copy -from html.parser import HTMLParser +import html import json import os import re @@ -39,14 +39,19 @@ import nbconvert import nbformat import sphinx +import sphinx.directives +import sphinx.environment import sphinx.errors import sphinx.transforms.post_transforms.images +from sphinx.util.matching import patmatch import traitlets _ipynbversion = 4 logger = sphinx.util.logging.getLogger(__name__) +_BROKEN_THUMBNAIL = object() + # See nbconvert/exporters/html.py: DISPLAY_DATA_PRIORITY_HTML = ( 'application/vnd.jupyter.widget-state+json', @@ -215,8 +220,11 @@ {% block markdowncell %} -{%- if 'nbsphinx-toctree' in cell.metadata or 'nbsphinx-toctree' in cell.metadata.tags %} -{{ cell | extract_toctree }} +{%- if 'nbsphinx-gallery' in cell.metadata + or 'nbsphinx-gallery' in cell.metadata.tags + or 'nbsphinx-toctree' in cell.metadata + or 'nbsphinx-toctree' in cell.metadata.tags %} +{{ cell | extract_gallery_or_toctree }} {%- else %} {{ cell | save_attachments or super() | replace_attachments }} {% endif %} @@ -739,7 +747,7 @@ def replace_attachments(text): 'convert_pandoc': convert_pandoc, 'markdown2rst': markdown2rst, 'get_empty_lines': _get_empty_lines, - 'extract_toctree': _extract_toctree, + 'extract_gallery_or_toctree': _extract_gallery_or_toctree, 'save_attachments': save_attachments, 'replace_attachments': replace_attachments, 'get_output_type': _get_output_type, @@ -798,6 +806,74 @@ def from_notebook_node(self, nb, resources=None, **kw): 'widgets', {}): resources['nbsphinx_widgets'] = True + thumbnail = {} + + def warning(msg, *args): + logger.warning( + '"nbsphinx-thumbnail": ' + msg, *args, + location=resources.get('nbsphinx_docname')) + thumbnail['filename'] = _BROKEN_THUMBNAIL + + for cell_index, cell in enumerate(nb.cells): + if 'nbsphinx-thumbnail' in cell.metadata: + data = cell.metadata['nbsphinx-thumbnail'].copy() + output_index = data.pop('output-index', -1) + tooltip = data.pop('tooltip', '') + if data: + warning('Invalid key(s): %s', set(data)) + break + elif 'nbsphinx-thumbnail' in cell.metadata.get('tags', []): + output_index = -1 + tooltip = '' + else: + continue + if cell.cell_type != 'code': + warning('Only allowed in code cells; cell %s has type "%s"', + cell_index, cell.cell_type) + break + if thumbnail: + warning('Only allowed once per notebook') + break + if not cell.outputs: + warning('No outputs in cell %s', cell_index) + break + if tooltip: + thumbnail['tooltip'] = tooltip + if output_index == -1: + output_index = len(cell.outputs) - 1 + elif output_index >= len(cell.outputs): + warning('Invalid "output-index" in cell %s: %s', + cell_index, output_index) + break + out = cell.outputs[output_index] + if out.output_type not in {'display_data', 'execute_result'}: + warning('Unsupported output type in cell %s/output %s: "%s"', + cell_index, output_index, out.output_type) + break + + for mime_type in DISPLAY_DATA_PRIORITY_HTML: + if mime_type not in out.data: + continue + if mime_type == 'image/svg+xml': + suffix = '.svg' + elif mime_type == 'image/png': + suffix = '.png' + elif mime_type == 'image/jpeg': + suffix = '.jpg' + else: + continue + thumbnail['filename'] = '{}_{}_{}{}'.format( + resources['unique_key'], + cell_index, + output_index, + suffix, + ) + break + else: + warning('Unsupported MIME type(s) in cell %s/output %s: %s', + cell_index, output_index, set(out.data)) + break + resources['nbsphinx_thumbnail'] = thumbnail return rststr, resources @@ -867,6 +943,7 @@ def parse(self, inputstring, document): # Sphinx doesn't accept absolute paths in images etc. resources['output_files_dir'] = os.path.relpath(auxdir, srcdir) resources['unique_key'] = re.sub('[/ ]', '_', env.docname) + resources['nbsphinx_docname'] = env.docname # NB: The source file could have a different suffix # if nbsphinx_custom_formats is used. @@ -930,6 +1007,9 @@ def parse(self, inputstring, document): if resources.get('nbsphinx_widgets', False): env.nbsphinx_widgets.add(env.docname) + env.nbsphinx_thumbnails[env.docname] = resources.get( + 'nbsphinx_thumbnail', {}) + class NotebookError(sphinx.errors.SphinxError): """Error during notebook parsing.""" @@ -1010,6 +1090,14 @@ class AdmonitionNode(docutils.nodes.Element): """A custom node for info and warning boxes.""" +class GalleryToc(docutils.nodes.Element): + """A wrapper node used for creating galleries.""" + + +class GalleryNode(docutils.nodes.Element): + """A custom node for thumbnail galleries.""" + + # See http://docutils.sourceforge.net/docs/howto/rst-directives.html class NbInput(rst.Directive): @@ -1076,6 +1164,19 @@ class NbWarning(_NbAdmonition): _class = 'warning' +class NbGallery(sphinx.directives.other.TocTree): + """A thumbnail gallery for notebooks.""" + + def run(self): + """Wrap GalleryToc arount toctree.""" + ret = super().run() + toctree = ret[-1][-1] + gallerytoc = GalleryToc() + gallerytoc.append(toctree) + ret[-1][-1] = gallerytoc + return ret + + def convert_pandoc(text, from_format, to_format): """Simple wrapper for markdown2rst. @@ -1088,7 +1189,7 @@ def convert_pandoc(text, from_format, to_format): return markdown2rst(text) -class CitationParser(HTMLParser): +class CitationParser(html.parser.HTMLParser): def handle_starttag(self, tag, attrs): if self._check_cite(attrs): @@ -1108,7 +1209,7 @@ def _check_cite(self, attrs): return False def reset(self): - HTMLParser.reset(self) + super().reset() self.starttag = '' self.endtag = '' self.cite = '' @@ -1216,10 +1317,23 @@ def decode(data): return decode(out).rstrip('\n') -def _extract_toctree(cell): - """Extract links from Markdown cell and create toctree.""" - lines = ['.. toctree::'] - options = cell.metadata.get('nbsphinx-toctree', {}) +def _extract_gallery_or_toctree(cell): + """Extract links from Markdown cell and create gallery/toctree.""" + # If both are available, "gallery" takes precedent + if 'nbsphinx-gallery' in cell.metadata: + lines = ['.. nbgallery::'] + options = cell.metadata['nbsphinx-gallery'] + elif 'nbsphinx-gallery' in cell.metadata.get('tags', []): + lines = ['.. nbgallery::'] + options = {} + elif 'nbsphinx-toctree' in cell.metadata: + lines = ['.. toctree::'] + options = cell.metadata['nbsphinx-toctree'] + elif 'nbsphinx-toctree' in cell.metadata.get('tags', []): + lines = ['.. toctree::'] + options = {} + else: + assert False try: for option, value in options.items(): if value is True: @@ -1230,24 +1344,25 @@ def _extract_toctree(cell): lines.append(':{}: {}'.format(option, value)) except AttributeError: raise ValueError( - 'invalid nbsphinx-toctree option: {!r}'.format(options)) + 'invalid nbsphinx-gallery/nbsphinx-toctree option: {!r}' + .format(options)) text = nbconvert.filters.markdown2rst(cell.source) settings = docutils.frontend.OptionParser( components=(rst.Parser,)).get_default_values() - toctree_node = docutils.utils.new_document('extract_toctree', settings) + node = docutils.utils.new_document('gallery_or_toctree', settings) parser = rst.Parser() - parser.parse(text, toctree_node) + parser.parse(text, node) if 'caption' not in options: - for sec in toctree_node.traverse(docutils.nodes.section): + for sec in node.traverse(docutils.nodes.section): assert sec.children assert isinstance(sec.children[0], docutils.nodes.title) title = sec.children[0].astext() lines.append(':caption: ' + title) break lines.append('') # empty line - for ref in toctree_node.traverse(docutils.nodes.reference): + for ref in node.traverse(docutils.nodes.reference): lines.append(ref.astext().replace('\n', '') + ' <' + unquote(ref.get('refuri')) + '>') return '\n '.join(lines) @@ -1565,6 +1680,35 @@ def handle(self, node): node['width'], node['height'] = map(str, size) +original_toctree_resolve = sphinx.environment.adapters.toctree.TocTree.resolve + + +def patched_toctree_resolve(self, docname, builder, toctree, *args, **kwargs): + """Method for monkey-patching Sphinx's TocTree adapter. + + The list of section links is never shown, regardless of the + ``:hidden:`` option. + However, this option can still be used to control whether the + section links are shown in higher-level tables of contents. + + """ + gallery = toctree.get('nbsphinx_gallery') + if gallery is not None: + toctree = toctree.copy() + toctree['hidden'] = False + node = original_toctree_resolve( + self, docname, builder, toctree, *args, **kwargs) + if gallery is None: + return node + assert node is not None + if isinstance(node[0], docutils.nodes.caption): + del node[1:] + else: + del node[:] + node += gallery + return node + + def config_inited(app, config): # Set default value for CSS prompt width (optimized for two-digit numbers) if config.nbsphinx_prompt_width is None: @@ -1624,6 +1768,8 @@ def builder_inited(app): env = app.env env.nbsphinx_notebooks = {} env.nbsphinx_files = {} + if not hasattr(env, 'nbsphinx_thumbnails'): + env.nbsphinx_thumbnails = {} env.nbsphinx_widgets = set() env.nbsphinx_auxdir = os.path.join(env.doctreedir, 'nbsphinx') sphinx.util.ensuredir(env.nbsphinx_auxdir) @@ -1676,6 +1822,10 @@ def env_purge_doc(app, env, docname): del env.nbsphinx_files[docname] except KeyError: pass + try: + del env.nbsphinx_thumbnails[docname] + except KeyError: + pass env.nbsphinx_widgets.discard(docname) @@ -1698,6 +1848,75 @@ def env_updated(app, env): app.add_js_file(widgets_path, **app.config.nbsphinx_widgets_options) +def doctree_resolved(app, doctree, fromdocname): + # Replace GalleryToc with toctree + GalleryNode + for node in doctree.traverse(GalleryToc): + toctree = node[0] + if not isinstance(toctree, sphinx.addnodes.toctree): + # This happens for LaTeX output + node.replace_self(node.children) + continue + entries = [] + for title, doc in toctree['entries']: + if doc in toctree['includefiles']: + if title is None: + title = app.env.titles[doc].astext() + uri = app.builder.get_relative_uri(fromdocname, doc) + base = sphinx.util.osutil.relative_uri( + app.builder.get_target_uri(fromdocname), '') + + # NB: This is how Sphinx implements the "html_sidebars" + # config value in StandaloneHTMLBuilder.add_sidebars() + + def has_wildcard(pattern): + return any(char in pattern for char in '*?[') + + matched = None + conf_py_thumbnail = None + conf_py_thumbnails = app.env.config.nbsphinx_thumbnails.items() + for pattern, candidate in conf_py_thumbnails: + if patmatch(doc, pattern): + if matched: + if has_wildcard(pattern): + # warn if both patterns contain wildcards + if has_wildcard(matched): + logger.warning( + 'page %s matches two patterns in ' + 'nbsphinx_thumbnails: %r and %r', + doc, matched, pattern) + # else the already matched pattern is more + # specific than the present one, because it + # contains no wildcard + continue + matched = pattern + conf_py_thumbnail = candidate + + thumbnail = app.env.nbsphinx_thumbnails.get(doc, {}) + tooltip = thumbnail.get('tooltip', '') + filename = thumbnail.get('filename', '') + if filename is _BROKEN_THUMBNAIL: + filename = os.path.join( + base, '_static', 'broken_example.png') + elif filename: + filename = os.path.join( + base, app.builder.imagedir, filename) + elif conf_py_thumbnail: + # NB: Settings from conf.py can be overwritten in notebook + filename = os.path.join(base, conf_py_thumbnail) + else: + filename = os.path.join(base, '_static', 'no_image.png') + entries.append((title, uri, filename, tooltip)) + else: + logger.warning( + 'External links are not supported in gallery: %s', doc, + location=fromdocname) + gallery = GalleryNode() + gallery['entries'] = entries + toctree['nbsphinx_gallery'] = gallery + node.replace_self(toctree) + # NB: Further processing happens in patched_toctree_resolve() + + def depart_codearea_html(self, node): """Add empty lines before and after the code.""" text = self.body[-1] @@ -1814,6 +2033,32 @@ def depart_admonition_latex(self, node): self.body.append('\\end{sphinxadmonition}\n') +def depart_gallery_html(self, node): + for title, uri, filename, tooltip in node['entries']: + if tooltip: + tooltip = ' tooltip="{}"'.format(html.escape(tooltip)) + self.body.append("""\ +
+
+ thumbnail +

+ + + {title} + + +

+
+
+""".format( + uri=html.escape(uri), + title=html.escape(title), + tooltip=tooltip, + filename=html.escape(filename), + )) + self.body.append('
') + + def do_nothing(self, node): pass @@ -1844,11 +2089,13 @@ def setup(app): # This will be updated in env_updated(): app.add_config_value('nbsphinx_widgets_path', None, rebuild='html') app.add_config_value('nbsphinx_widgets_options', {}, rebuild='html') + app.add_config_value('nbsphinx_thumbnails', {}, rebuild='html') app.add_directive('nbinput', NbInput) app.add_directive('nboutput', NbOutput) app.add_directive('nbinfo', NbInfo) app.add_directive('nbwarning', NbWarning) + app.add_directive('nbgallery', NbGallery) app.add_node(CodeAreaNode, html=(do_nothing, depart_codearea_html), latex=(visit_codearea_latex, depart_codearea_latex)) @@ -1858,12 +2105,16 @@ def setup(app): app.add_node(AdmonitionNode, html=(visit_admonition_html, depart_admonition_html), latex=(visit_admonition_latex, depart_admonition_latex)) + app.add_node(GalleryNode, + html=(do_nothing, depart_gallery_html), + latex=(do_nothing, do_nothing)) app.connect('builder-inited', builder_inited) app.connect('config-inited', config_inited) app.connect('html-page-context', html_page_context) app.connect('html-collect-pages', html_collect_pages) app.connect('env-purge-doc', env_purge_doc) app.connect('env-updated', env_updated) + app.connect('doctree-resolved', doctree_resolved) app.add_transform(CreateSectionLabels) app.add_transform(CreateDomainObjectLabels) app.add_transform(RewriteLocalLinks) @@ -1893,9 +2144,13 @@ def setup(app): latex_elements.get('preamble', ''), ]) + # Monkey-patch Sphinx TocTree adapter + sphinx.environment.adapters.toctree.TocTree.resolve = \ + patched_toctree_resolve + return { 'version': __version__, 'parallel_read_safe': True, 'parallel_write_safe': True, - 'env_version': 2, + 'env_version': 3, }