Skip to content

Commit

Permalink
👌 IMPROVE: Replace sphinx-togglebutton with built-in functionality (#446
Browse files Browse the repository at this point in the history
)

This allows for tighter integration with myst-nb:

- Nicer rendering of the hidden content buttons
- Customisation of the hide/show prompts

The implementation uses "static" details/summary HTML tags directly + CSS,
similar to [sphinx-design](https:/executablebooks/sphinx-design),
rather than the JS implementation used by sphinx-togglebutton.
  • Loading branch information
chrisjsewell authored Sep 29, 2022
1 parent 4dcf7c5 commit bd852a0
Show file tree
Hide file tree
Showing 11 changed files with 414 additions and 38 deletions.
28 changes: 26 additions & 2 deletions docs/render/hiding.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions myst_nb/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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={
Expand Down
40 changes: 31 additions & 9 deletions myst_nb/core/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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):

Expand Down
78 changes: 78 additions & 0 deletions myst_nb/sphinx_.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

from collections import defaultdict
from html import escape
import json
from pathlib import Path
import re
Expand All @@ -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
Expand Down Expand Up @@ -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'<details class="hide {classes}">\n')
self.body.append('<summary aria-label="Toggle hidden content">\n')
self.body.append(f'<span class="collapsed">{escape(node["prompt_show"])}</span>\n')
self.body.append(f'<span class="expanded">{escape(node["prompt_hide"])}</span>\n')
self.body.append("</summary>\n")


def depart_HideCellInput(self: SphinxTranslator, node: HideCodeCellNode):
self.body.append("</details>\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)
31 changes: 11 additions & 20 deletions myst_nb/sphinx_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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}"
59 changes: 58 additions & 1 deletion myst_nb/static/mystnb.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit bd852a0

Please sign in to comment.