Skip to content

Commit 1ddc3a5

Browse files
Add scroll_outputs configuration (#683)
* Add config options for scroll_outputs * Add tests for scrollable outputs in notebooks * Document cell tags for scrollable output/input * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * merge input output scroll tags css * Add .config_scroll_outputs in css to handle images differently Simplify repeated long selectors with :is() selector * Adjust max-height for output cells with images * Fix scrollable output class in expected AST for tests * Remove deprecated `output_scroll` from the docs --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 9fe8848 commit 1ddc3a5

File tree

7 files changed

+380
-45
lines changed

7 files changed

+380
-45
lines changed

docs/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ Tag | Description
140140
`remove-input` | Remove the code cell input/source from the rendered output.
141141
`remove-output` | Remove the code cell output from the rendered output.
142142
`remove-stderr` | Remove the code cell output stderr from the rendered output.
143+
`scroll-output` | Make the cell output scrollable if it is too long.
144+
`scroll-input` | Make the cell input scrollable if it is too long.
143145

144146
Additionally, for code execution, these tags are provided (via `nbclient`):
145147

myst_nb/core/config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,20 @@ def __post_init__(self):
334334
},
335335
)
336336

337+
scroll_outputs: bool = dc.field(
338+
default=False,
339+
metadata={
340+
"validator": instance_of(bool),
341+
"help": "Make long cell outputs scrollable",
342+
"sections": (
343+
Section.global_lvl,
344+
Section.file_lvl,
345+
Section.cell_lvl,
346+
Section.render,
347+
),
348+
},
349+
)
350+
337351
code_prompt_show: str = dc.field(
338352
default="Show code cell {type}",
339353
metadata={

myst_nb/core/render.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,16 @@ def render_nb_cell_code(self: SelfType, token: SyntaxTreeNode) -> None:
138138
for tag in tags:
139139
classes.append(f"tag_{tag.replace(' ', '_')}")
140140

141+
# handle config option for scrollable outputs
142+
scroll_outputs = self.get_cell_level_config(
143+
"scroll_outputs", token.meta["metadata"], line=cell_line
144+
)
145+
if scroll_outputs and not any( # don't override cell tags
146+
tag in ["scroll-output", "output_scroll"] for tag in tags
147+
):
148+
# add the class defined in mystnb.css for scroll_outputs config option
149+
classes.append("config_scroll_outputs")
150+
141151
# TODO do we need this -/_ duplication of tag names, or can we deprecate one?
142152
hide_cell = "hide-cell" in tags
143153
remove_input = (

myst_nb/static/mystnb.css

Lines changed: 52 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -281,65 +281,72 @@ tbody span.pasted-inline img {
281281
*
282282
* It was before in https://github.com/executablebooks/sphinx-book-theme/blob/eb1b6baf098b27605e8f2b7b2979b17ebf1b9540/src/sphinx_book_theme/assets/styles/extensions/_myst-nb.scss
283283
*/
284-
285-
div.cell.tag_output_scroll div.cell_output, div.cell.tag_scroll-output div.cell_output {
286-
max-height: 24em;
287-
overflow-y: auto;
288-
max-width: 100%;
289-
overflow-x: auto;
290-
}
291-
292-
div.cell.tag_output_scroll div.cell_output::-webkit-scrollbar, div.cell.tag_scroll-output div.cell_output::-webkit-scrollbar {
293-
width: var(--mystnb-scrollbar-width);
294-
height: var(--mystnb-scrollbar-height);
295-
}
296-
297-
div.cell.tag_output_scroll div.cell_output::-webkit-scrollbar-thumb, div.cell.tag_scroll-output div.cell_output::-webkit-scrollbar-thumb {
298-
background: var(--mystnb-scrollbar-thumb-color);
299-
border-radius: var(--mystnb-scrollbar-thumb-border-radius);
300-
}
301-
302-
div.cell.tag_output_scroll div.cell_output::-webkit-scrollbar-thumb:hover, div.cell.tag_scroll-output div.cell_output::-webkit-scrollbar-thumb:hover {
303-
background: var(--mystnb-scrollbar-thumb-hover-color);
304-
}
305-
306-
@media print {
307-
div.cell.tag_output_scroll div.cell_output, div.cell.tag_scroll-output div.cell_output {
308-
max-height: unset;
309-
overflow-y: visible;
310-
max-width: unset;
311-
overflow-x: visible;
312-
}
284+
div.cell:is(
285+
.tag_output_scroll,
286+
.tag_scroll-output,
287+
.config_scroll_outputs
288+
)
289+
div.cell_output,
290+
div.cell.tag_scroll-input div.cell_input {
291+
max-height: 24em;
292+
overflow-y: auto;
293+
max-width: 100%;
294+
overflow-x: auto;
313295
}
314296

315-
div.cell.tag_scroll-input div.cell_input {
316-
max-height: 24em;
317-
overflow-y: auto;
318-
max-width: 100%;
319-
overflow-x: auto;
297+
div.cell.config_scroll_outputs div.cell_output:has(img) {
298+
/* If the output cell has image(s), allow it to take 90% of viewport height
299+
but still bounded between 24em and 60em */
300+
max-height: clamp(24em, 90vh, 60em);
320301
}
321302

303+
/* Custom scrollbars */
304+
div.cell:is(
305+
.tag_output_scroll,
306+
.tag_scroll-output,
307+
.config_scroll_outputs
308+
)
309+
div.cell_output::-webkit-scrollbar,
322310
div.cell.tag_scroll-input div.cell_input::-webkit-scrollbar {
323-
width: var(--mystnb-scrollbar-width);
324-
height: var(--mystnb-scrollbar-height);
311+
width: var(--mystnb-scrollbar-width);
312+
height: var(--mystnb-scrollbar-height);
325313
}
326314

315+
div.cell:is(
316+
.tag_output_scroll,
317+
.tag_scroll-output,
318+
.config_scroll_outputs
319+
)
320+
div.cell_output::-webkit-scrollbar-thumb,
327321
div.cell.tag_scroll-input div.cell_input::-webkit-scrollbar-thumb {
328-
background: var(--mystnb-scrollbar-thumb-color);
329-
border-radius: var(--mystnb-scrollbar-thumb-border-radius);
322+
background: var(--mystnb-scrollbar-thumb-color);
323+
border-radius: var(--mystnb-scrollbar-thumb-border-radius);
330324
}
331325

326+
div.cell:is(
327+
.tag_output_scroll,
328+
.tag_scroll-output,
329+
.config_scroll_outputs
330+
)
331+
div.cell_output::-webkit-scrollbar-thumb:hover,
332332
div.cell.tag_scroll-input div.cell_input::-webkit-scrollbar-thumb:hover {
333-
background: var(--mystnb-scrollbar-thumb-hover-color);
333+
background: var(--mystnb-scrollbar-thumb-hover-color);
334334
}
335335

336+
/* In print mode, unset scroll styles */
336337
@media print {
337-
div.cell.tag_scroll-input div.cell_input {
338-
max-height: unset;
339-
overflow-y: visible;
340-
max-width: unset;
341-
overflow-x: visible;
342-
}
338+
div.cell:is(
339+
.tag_output_scroll,
340+
.tag_scroll-output,
341+
.config_scroll_outputs
342+
)
343+
div.cell_output,
344+
div.cell.tag_scroll-input div.cell_input {
345+
max-height: unset;
346+
overflow-y: visible;
347+
max-width: unset;
348+
overflow-x: visible;
349+
}
343350
}
344351

345352
/* Font colors for translated ANSI escape sequences
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "356e9205",
6+
"metadata": {},
7+
"source": [
8+
"# Scroll long outputs"
9+
]
10+
},
11+
{
12+
"cell_type": "code",
13+
"execution_count": 2,
14+
"id": "cd1b32ee",
15+
"metadata": {},
16+
"outputs": [
17+
{
18+
"name": "stdout",
19+
"output_type": "stream",
20+
"text": [
21+
"short output\n",
22+
"short output\n"
23+
]
24+
}
25+
],
26+
"source": [
27+
"for i in range(2):\n",
28+
" print(\"short output\")"
29+
]
30+
},
31+
{
32+
"cell_type": "code",
33+
"execution_count": 1,
34+
"id": "91a31d3f",
35+
"metadata": {},
36+
"outputs": [
37+
{
38+
"name": "stdout",
39+
"output_type": "stream",
40+
"text": [
41+
"long output\n",
42+
"long output\n",
43+
"long output\n",
44+
"long output\n",
45+
"long output\n",
46+
"long output\n",
47+
"long output\n",
48+
"long output\n",
49+
"long output\n",
50+
"long output\n",
51+
"long output\n",
52+
"long output\n",
53+
"long output\n",
54+
"long output\n",
55+
"long output\n",
56+
"long output\n",
57+
"long output\n",
58+
"long output\n",
59+
"long output\n",
60+
"long output\n",
61+
"long output\n",
62+
"long output\n",
63+
"long output\n",
64+
"long output\n",
65+
"long output\n",
66+
"long output\n",
67+
"long output\n",
68+
"long output\n",
69+
"long output\n",
70+
"long output\n",
71+
"long output\n",
72+
"long output\n",
73+
"long output\n",
74+
"long output\n",
75+
"long output\n",
76+
"long output\n",
77+
"long output\n",
78+
"long output\n",
79+
"long output\n",
80+
"long output\n",
81+
"long output\n",
82+
"long output\n",
83+
"long output\n",
84+
"long output\n",
85+
"long output\n",
86+
"long output\n",
87+
"long output\n",
88+
"long output\n",
89+
"long output\n",
90+
"long output\n",
91+
"long output\n",
92+
"long output\n",
93+
"long output\n",
94+
"long output\n",
95+
"long output\n",
96+
"long output\n",
97+
"long output\n",
98+
"long output\n",
99+
"long output\n",
100+
"long output\n",
101+
"long output\n",
102+
"long output\n",
103+
"long output\n",
104+
"long output\n",
105+
"long output\n",
106+
"long output\n",
107+
"long output\n",
108+
"long output\n",
109+
"long output\n",
110+
"long output\n",
111+
"long output\n",
112+
"long output\n",
113+
"long output\n",
114+
"long output\n",
115+
"long output\n",
116+
"long output\n",
117+
"long output\n",
118+
"long output\n",
119+
"long output\n",
120+
"long output\n",
121+
"long output\n",
122+
"long output\n",
123+
"long output\n",
124+
"long output\n",
125+
"long output\n",
126+
"long output\n",
127+
"long output\n",
128+
"long output\n",
129+
"long output\n",
130+
"long output\n",
131+
"long output\n",
132+
"long output\n",
133+
"long output\n",
134+
"long output\n",
135+
"long output\n",
136+
"long output\n",
137+
"long output\n",
138+
"long output\n",
139+
"long output\n",
140+
"long output\n"
141+
]
142+
}
143+
],
144+
"source": [
145+
"for i in range(100):\n",
146+
" print(\"long output\")"
147+
]
148+
}
149+
],
150+
"metadata": {
151+
"kernelspec": {
152+
"display_name": "myst-nb-py311",
153+
"language": "python",
154+
"name": "python3"
155+
},
156+
"language_info": {
157+
"codemirror_mode": {
158+
"name": "ipython",
159+
"version": 3
160+
},
161+
"file_extension": ".py",
162+
"mimetype": "text/x-python",
163+
"name": "python",
164+
"nbconvert_exporter": "python",
165+
"pygments_lexer": "ipython3",
166+
"version": "3.11.13"
167+
}
168+
},
169+
"nbformat": 4,
170+
"nbformat_minor": 5
171+
}

tests/test_render_outputs.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,14 @@ def test_hide_cell_content(sphinx_run, file_regression):
194194
assert sphinx_run.warnings() == ""
195195
doctree = sphinx_run.get_resolved_doctree("hide_cell_content")
196196
file_regression.check(doctree.pformat(), extension=".xml", encoding="utf-8")
197+
198+
199+
@pytest.mark.sphinx_params(
200+
"scroll_outputs.ipynb", conf={"nb_execution_mode": "off", "nb_scroll_outputs": True}
201+
)
202+
def test_scroll_outputs(sphinx_run, file_regression):
203+
"""Test that scrollable outputs are rendered correctly."""
204+
sphinx_run.build()
205+
assert sphinx_run.warnings() == ""
206+
doctree = sphinx_run.get_resolved_doctree("scroll_outputs")
207+
file_regression.check(doctree.pformat(), extension=".xml", encoding="utf-8")

0 commit comments

Comments
 (0)