Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

👌 IMPROVE: Replace sphinx-togglebutton with built-in functionality #446

Merged
merged 1 commit into from
Sep 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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