diff --git a/docs/render/hiding.md b/docs/render/hiding.md
index cf6d18e5..f12cae0b 100644
--- a/docs/render/hiding.md
+++ b/docs/render/hiding.md
@@ -7,8 +7,7 @@ kernelspec:
# Hide cell contents
You can use Jupyter Notebook **cell tags** to control some of the behavior of
-the rendered notebook. This uses the [**`sphinx-togglebutton`**](https://sphinx-togglebutton.readthedocs.io/en/latest/)
-package to add a little button that toggles the visibility of content.[^download]
+the rendered notebook.[^download]
[^download]: This notebook can be downloaded as
**{nb-download}`hiding.ipynb`** and {download}`hiding.md`
@@ -66,6 +65,31 @@ fig, ax = plt.subplots()
points =ax.scatter(*data, c=data[0], s=data[0])
```
+You can control the hide/show prompts by using the `code_prompt_show` and `code_prompt_hide` configuration options.
+`{type}` will be replaced with `content`, `source`, or `outputs`, depending on the hide tag.
+See the {ref}`config/intro` section for more details.
+
+````markdown
+
+```{code-cell} ipython3
+:tags: [hide-cell]
+:mystnb:
+: code_prompt_show: "My show prompt"
+: code_prompt_hide: "My hide prompt"
+
+print("hallo world")
+```
+````
+
+```{code-cell} ipython3
+:tags: [hide-cell]
+:mystnb:
+: code_prompt_show: "My show prompt for {type}"
+: code_prompt_hide: "My hide prompt for {type}"
+
+print("hallo world")
+```
+
(use/hiding/markdown)=
## Hide markdown cells
diff --git a/myst_nb/core/config.py b/myst_nb/core/config.py
index 62c162f6..d90e85ab 100644
--- a/myst_nb/core/config.py
+++ b/myst_nb/core/config.py
@@ -323,6 +323,35 @@ def __post_init__(self):
),
},
)
+
+ code_prompt_show: str = dc.field(
+ default="Show code cell {type}",
+ metadata={
+ "validator": instance_of(str),
+ "help": "Prompt to expand hidden code cell {content|source|outputs}",
+ "sections": (
+ Section.global_lvl,
+ Section.file_lvl,
+ Section.cell_lvl,
+ Section.render,
+ ),
+ },
+ )
+
+ code_prompt_hide: str = dc.field(
+ default="Hide code cell {type}",
+ metadata={
+ "validator": instance_of(str),
+ "help": "Prompt to collapse hidden code cell {content|source|outputs}",
+ "sections": (
+ Section.global_lvl,
+ Section.file_lvl,
+ Section.cell_lvl,
+ Section.render,
+ ),
+ },
+ )
+
number_source_lines: bool = dc.field(
default=False,
metadata={
diff --git a/myst_nb/core/render.py b/myst_nb/core/render.py
index 8f697065..108a349c 100644
--- a/myst_nb/core/render.py
+++ b/myst_nb/core/render.py
@@ -128,38 +128,49 @@ def render_nb_cell_raw(self: SelfType, token: SyntaxTreeNode) -> None:
def render_nb_cell_code(self: SelfType, token: SyntaxTreeNode) -> None:
"""Render a notebook code cell."""
cell_index = token.meta["index"]
+ cell_line = token_line(token, 0) or None
tags = token.meta["metadata"].get("tags", [])
exec_count, outputs = self._get_nb_code_cell_outputs(token)
+ classes = ["cell"]
+ for tag in tags:
+ classes.append(f"tag_{tag.replace(' ', '_')}")
+
# TODO do we need this -/_ duplication of tag names, or can we deprecate one?
+ hide_cell = "hide-cell" in tags
remove_input = (
self.get_cell_level_config(
- "remove_code_source",
- token.meta["metadata"],
- line=token_line(token, 0) or None,
+ "remove_code_source", token.meta["metadata"], line=cell_line
)
or ("remove_input" in tags)
or ("remove-input" in tags)
)
+ hide_input = "hide-input" in tags
remove_output = (
self.get_cell_level_config(
- "remove_code_outputs",
- token.meta["metadata"],
- line=token_line(token, 0) or None,
+ "remove_code_outputs", token.meta["metadata"], line=cell_line
)
or ("remove_output" in tags)
or ("remove-output" in tags)
)
+ hide_output = "hide-output" in tags
# if we are remove both the input and output, we can skip the cell
if remove_input and remove_output:
return
+ hide_mode = None
+ if hide_cell:
+ hide_mode = "all"
+ elif hide_input and hide_output:
+ hide_mode = "all"
+ elif hide_input:
+ hide_mode = "input"
+ elif hide_output:
+ hide_mode = "output"
+
# create a container for all the input/output
- classes = ["cell"]
- for tag in tags:
- classes.append(f"tag_{tag.replace(' ', '_')}")
cell_container = nodes.container(
nb_element="cell_code",
cell_index=cell_index,
@@ -168,6 +179,17 @@ def render_nb_cell_code(self: SelfType, token: SyntaxTreeNode) -> None:
cell_metadata=token.meta["metadata"],
classes=classes,
)
+ if hide_mode:
+ cell_container["hide_mode"] = hide_mode
+ code_prompt_show = self.get_cell_level_config(
+ "code_prompt_show", token.meta["metadata"], line=cell_line
+ )
+ code_prompt_hide = self.get_cell_level_config(
+ "code_prompt_hide", token.meta["metadata"], line=cell_line
+ )
+ cell_container["prompt_show"] = code_prompt_show
+ cell_container["prompt_hide"] = code_prompt_hide
+
self.add_line_and_source_path(cell_container, token)
with self.current_node_context(cell_container, append=True):
diff --git a/myst_nb/sphinx_.py b/myst_nb/sphinx_.py
index 4a3a57a0..ccdfc9e4 100644
--- a/myst_nb/sphinx_.py
+++ b/myst_nb/sphinx_.py
@@ -2,6 +2,7 @@
from __future__ import annotations
from collections import defaultdict
+from html import escape
import json
from pathlib import Path
import re
@@ -21,6 +22,7 @@
from sphinx.environment.collectors import EnvironmentCollector
from sphinx.transforms.post_transforms import SphinxPostTransform
from sphinx.util import logging as sphinx_logging
+from sphinx.util.docutils import SphinxTranslator
from myst_nb._compat import findall
from myst_nb.core.config import NbParserConfig
@@ -472,3 +474,79 @@ def default(self, obj):
if isinstance(obj, bytes):
return obj.decode("ascii")
return json.JSONEncoder.default(self, obj)
+
+
+class HideCodeCellNode(nodes.Element):
+ """Node for hiding cell input."""
+
+ @classmethod
+ def add_to_app(cls, app: Sphinx):
+ app.add_node(cls, html=(visit_HideCellInput, depart_HideCellInput))
+
+
+def visit_HideCellInput(self: SphinxTranslator, node: HideCodeCellNode):
+ classes = " ".join(node["classes"])
+ self.body.append(f'\n')
+ self.body.append('\n')
+ self.body.append(f'{escape(node["prompt_show"])}\n')
+ self.body.append(f'{escape(node["prompt_hide"])}\n')
+ self.body.append("
\n")
+
+
+def depart_HideCellInput(self: SphinxTranslator, node: HideCodeCellNode):
+ self.body.append(" \n")
+
+
+class HideInputCells(SphinxPostTransform):
+ """Hide input cells in the HTML output."""
+
+ default_priority = 199
+ formats = ("html",)
+
+ def run(self, **kwargs):
+
+ for node in findall(self.document)(nodes.container):
+
+ if (
+ node.get("nb_element") == "cell_code"
+ and node.get("hide_mode")
+ and node.children
+ ):
+ hide_mode = node.get("hide_mode")
+ has_input = node.children[0].get("nb_element") == "cell_code_source"
+ has_output = node.children[-1].get("nb_element") == "cell_code_output"
+
+ # if we have the code source (input) element,
+ # and we are collapsing the input or input+output
+ # then we attach the "collapse button" above the input
+ if has_input and hide_mode == "input":
+ wrap_node = HideCodeCellNode(
+ prompt_show=node["prompt_show"].replace("{type}", "source"),
+ prompt_hide=node["prompt_hide"].replace("{type}", "source"),
+ )
+ wrap_node["classes"].append("above-input")
+ code = node.children[0]
+ wrap_node.append(code)
+ node.replace(code, wrap_node)
+
+ elif has_input and hide_mode == "all":
+ wrap_node = HideCodeCellNode(
+ prompt_show=node["prompt_show"].replace("{type}", "content"),
+ prompt_hide=node["prompt_hide"].replace("{type}", "content"),
+ )
+ wrap_node["classes"].append("above-input")
+ wrap_node.extend(node.children)
+ node.children = [wrap_node]
+
+ # if we don't have the code source (input) element,
+ # or are only hiding the output,
+ # then we place the "collapse button" above the output
+ elif has_output and hide_mode in ("output", "all"):
+ wrap_node = HideCodeCellNode(
+ prompt_show=node["prompt_show"].replace("{type}", "outputs"),
+ prompt_hide=node["prompt_hide"].replace("{type}", "outputs"),
+ )
+ wrap_node["classes"].append("above-output")
+ output = node.children[-1]
+ wrap_node.append(output)
+ node.replace(output, wrap_node)
diff --git a/myst_nb/sphinx_ext.py b/myst_nb/sphinx_ext.py
index e339e177..f19f2171 100644
--- a/myst_nb/sphinx_ext.py
+++ b/myst_nb/sphinx_ext.py
@@ -19,7 +19,13 @@
from myst_nb.ext.eval import load_eval_sphinx
from myst_nb.ext.glue import load_glue_sphinx
from myst_nb.ext.glue.crossref import ReplacePendingGlueReferences
-from myst_nb.sphinx_ import NbMetadataCollector, Parser, SelectMimeType
+from myst_nb.sphinx_ import (
+ HideCodeCellNode,
+ HideInputCells,
+ NbMetadataCollector,
+ Parser,
+ SelectMimeType,
+)
SPHINX_LOGGER = sphinx_logging.getLogger(__name__)
OUTPUT_FOLDER = "jupyter_execute"
@@ -83,17 +89,16 @@ def sphinx_setup(app: Sphinx):
app.add_post_transform(SelectMimeType)
app.add_post_transform(ReplacePendingGlueReferences)
+ # setup collapsible content
+ app.add_post_transform(HideInputCells)
+ HideCodeCellNode.add_to_app(app)
+
# add HTML resources
app.add_css_file("mystnb.css")
app.connect("build-finished", add_global_html_resources)
# note, this event is only available in Sphinx >= 3.5
app.connect("html-page-context", add_per_page_html_resources)
- # add configuration for hiding cell input/output
- # TODO replace this, or make it optional
- app.setup_extension("sphinx_togglebutton")
- app.connect("config-inited", update_togglebutton_classes)
-
# Note lexers are registered as `pygments.lexers` entry-points
# and so do not need to be added here.
@@ -191,17 +196,3 @@ def add_per_page_html_resources(
js_files = NbMetadataCollector.get_js_files(app.env, pagename) # type: ignore
for path, kwargs in js_files.values():
app.add_js_file(path, **kwargs) # type: ignore
-
-
-def update_togglebutton_classes(app: Sphinx, config):
- """Update togglebutton classes to recognise hidden cell inputs/outputs."""
- to_add = [
- ".tag_hide_input div.cell_input",
- ".tag_hide-input div.cell_input",
- ".tag_hide_output div.cell_output",
- ".tag_hide-output div.cell_output",
- ".tag_hide_cell.cell",
- ".tag_hide-cell.cell",
- ]
- for selector in to_add:
- config.togglebutton_selector += f", {selector}"
diff --git a/myst_nb/static/mystnb.css b/myst_nb/static/mystnb.css
index 46655d85..7b8b4d91 100644
--- a/myst_nb/static/mystnb.css
+++ b/myst_nb/static/mystnb.css
@@ -15,13 +15,70 @@ div.container.cell {
}
/* Input cells */
-div.cell div.cell_input {
+div.cell div.cell_input,
+div.cell details.above-input > summary {
padding-left: 0em;
padding-right: 0em;
border: 1px #ccc solid;
background-color: #f7f7f7;
border-left-color: green;
border-left-width: medium;
+ border-radius: .4em;
+}
+
+div.cell details.above-input div.cell_input {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ border-top: 1px #ccc dashed;
+}
+
+div.cell details.above-input > summary {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ border-bottom: 1px #ccc dashed;
+ padding-left: 1em;
+ margin-bottom: 0;
+}
+
+div.cell details.above-output > summary {
+ background-color: #f7f7f7;
+ padding-left: 1em;
+ padding-right: 0em;
+ border: 1px #ccc solid;
+ border-bottom: 1px #ccc dashed;
+ border-left-color: blue;
+ border-left-width: medium;
+ border-top-left-radius: .4em;
+ border-top-right-radius: .4em;
+}
+
+div.cell details.hide > summary::marker {
+ opacity: 50%;
+}
+
+div.cell details.hide > summary > span {
+ opacity: 50%;
+}
+
+div.cell details.hide[open] > summary > span.collapsed {
+ display: none;
+}
+div.cell details.hide:not([open]) > summary > span.expanded {
+ display: none;
+}
+
+@keyframes collapsed-fade-in {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+div.cell details.hide[open] > summary ~ * {
+ -moz-animation: collapsed-fade-in 0.3s ease-in-out;
+ -webkit-animation: collapsed-fade-in 0.3s ease-in-out;
+ animation: collapsed-fade-in 0.3s ease-in-out;
}
div.cell_input > div, div.cell_output div.output > div.highlight {
diff --git a/pyproject.toml b/pyproject.toml
index 89570c9e..18261905 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -43,7 +43,6 @@ dependencies = [
"nbformat~=5.0",
"pyyaml",
"sphinx>=4,<6",
- "sphinx-togglebutton~=0.3.0",
"typing-extensions",
# ipykernel is not a requirement of the library,
# but is a common requirement for users (registers the python3 kernel)
@@ -68,7 +67,7 @@ ipythontb = "myst_nb.core.lexers:IPythonTracebackLexer"
myst_nb_md = "myst_nb.core.read:myst_nb_reader_plugin"
[project.optional-dependencies]
-code_style = ["pre-commit~=2.12"]
+code_style = ["pre-commit"]
rtd = [
"alabaster",
"altair",
@@ -91,10 +90,13 @@ testing = [
"coverage~=6.4",
"beautifulsoup4",
"ipykernel~=5.5",
- "ipython!=8.1.0", # see https://github.com/ipython/ipython/issues/13554
+ # for issue with 8.1.0 see https://github.com/ipython/ipython/issues/13554
+ # TODO ipython 8.5 subtly changes output of test regressions
+ # see https://ipython.readthedocs.io/en/stable/whatsnew/version8.html#restore-line-numbers-for-input
+ "ipython!=8.1.0,<8.5",
"ipywidgets>=8",
"jupytext~=1.11.2",
- "matplotlib>=3.5.3",
+ "matplotlib>=3.5.3,<3.6", # TODO mpl 3.6 subtly changes output of test regressions
"nbdime",
"numpy",
"pandas",
diff --git a/tests/notebooks/hide_cell_content.ipynb b/tests/notebooks/hide_cell_content.ipynb
new file mode 100644
index 00000000..8cbbca9c
--- /dev/null
+++ b/tests/notebooks/hide_cell_content.ipynb
@@ -0,0 +1,126 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Hide Code Cell Content"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "tags": [
+ "hide-input"
+ ]
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "hide-input\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(\"hide-input\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "tags": [
+ "hide-output"
+ ]
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "hide-output\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(\"hide-output\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {
+ "tags": [
+ "hide-cell"
+ ]
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "hide-cell\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(\"hide-cell\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {
+ "tags": [
+ "hide-cell"
+ ],
+ "mystnb": {
+ "code_prompt_show": "My show message",
+ "code_prompt_hide": "My hide message"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "hide-cell custom message\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(\"hide-cell custom message\")"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3.8.13",
+ "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.8.13"
+ },
+ "orig_nbformat": 4,
+ "vscode": {
+ "interpreter": {
+ "hash": "321f99720af1749431335326d75386e6232ab33d0a78426e9f427a66c2c329a4"
+ }
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/tests/test_render_outputs.py b/tests/test_render_outputs.py
index f4b8862f..5cb72f1a 100644
--- a/tests/test_render_outputs.py
+++ b/tests/test_render_outputs.py
@@ -144,3 +144,12 @@ def test_unknown_mimetype(sphinx_run, file_regression):
assert warning in sphinx_run.warnings()
doctree = sphinx_run.get_resolved_doctree("unknown_mimetype")
file_regression.check(doctree.pformat(), extension=".xml", encoding="utf8")
+
+
+@pytest.mark.sphinx_params("hide_cell_content.ipynb", conf={"nb_execution_mode": "off"})
+def test_hide_cell_content(sphinx_run, file_regression):
+ """Test that hiding cell contents produces the correct AST."""
+ sphinx_run.build()
+ assert sphinx_run.warnings() == ""
+ doctree = sphinx_run.get_resolved_doctree("hide_cell_content")
+ file_regression.check(doctree.pformat(), extension=".xml", encoding="utf8")
diff --git a/tests/test_render_outputs/test_hide_cell_content.xml b/tests/test_render_outputs/test_hide_cell_content.xml
new file mode 100644
index 00000000..63f0e8e2
--- /dev/null
+++ b/tests/test_render_outputs/test_hide_cell_content.xml
@@ -0,0 +1,36 @@
+
+
+
+ Hide Code Cell Content
+
+
+
+
+ print("hide-input")
+
+
+ hide-input
+
+
+
+ print("hide-output")
+
+
+
+ hide-output
+
+
+
+
+ print("hide-cell")
+
+
+ hide-cell
+
+
+
+
+ print("hide-cell custom message")
+
+
+ hide-cell custom message
diff --git a/tox.ini b/tox.ini
index dabade26..817a6f79 100644
--- a/tox.ini
+++ b/tox.ini
@@ -27,13 +27,15 @@ commands = pytest {posargs}
extras = rtd
deps =
ipython<=7.11.0 # required by coconut
+setenv =
+ BUILDER = {env:BUILDER:html}
whitelist_externals =
echo
rm
commands =
clean: rm -rf docs/_build
- sphinx-build -nW --keep-going -b {posargs:html} docs/ docs/_build/{posargs:html}
-commands_post = echo "open file://{toxinidir}/docs/_build/{posargs:html}/index.html"
+ sphinx-build {posargs} -nW --keep-going -b {env:BUILDER} docs/ docs/_build/{env:BUILDER}
+commands_post = echo "open file://{toxinidir}/docs/_build/{env:BUILDER}/index.html"
[pytest]