Skip to content

Commit cee16c6

Browse files
authored
ENH: Lazy load thebe javascript (#41)
1 parent c036ac4 commit cee16c6

8 files changed

Lines changed: 93 additions & 73 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![Documentation](https://readthedocs.org/projects/sphinx-thebe/badge/?version=latest)](https://sphinx-thebe.readthedocs.io/en/latest/?badge=latest)
44
[![PyPI](https://img.shields.io/pypi/v/sphinx-thebe.svg)](https://pypi.org/project/sphinx-thebe)
55

6-
Integrate interactive code blocks into your documentation with Thebelab and Binder.
6+
Integrate interactive code blocks into your documentation with Thebe and Binder.
77

88
See [the sphinx-thebe documentation](https://sphinx-thebe.readthedocs.io/en/latest/) for more details!
99

docs/conf.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
# "selector": ".thebe",
4848
# "selector_input": ,
4949
# "selector_output": ,
50-
# "codemirror-theme": "blackboard" # Doesn't currently work
50+
# "codemirror-theme": "blackboard", # Doesn't currently work
51+
# "always_load": True, # To load thebe on every page
5152
}
5253

5354
myst_enable_extensions = ["colon_fence"]
@@ -86,6 +87,7 @@
8687
# a list of builtin themes.
8788
#
8889
html_theme = "sphinx_book_theme"
90+
html_title = "sphinx-thebe"
8991

9092
# Theme options are theme-specific and customize the look and feel of a theme
9193
# further. For a list of options available for each theme, see the

docs/configure.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,23 +202,24 @@ thebe_config = {
202202
See [the CodeMirror theme demo](https://codemirror.net/demo/theme.html) for a list
203203
of themes that you can use, and what they look like.
204204

205-
## Only load JS on certain pages
205+
## Load `thebe` automatically on all pages
206206

207-
By default, `sphinx-thebe` will load the JS/CSS from `thebe` on all of your documentation's pages.
208-
Alternatively, you may load `thebe` only on pages that use the `thebe-button` directive.
209-
To do so, use the following configuration:
207+
By default, `sphinx-thebe` will lazily load the JS/CSS from `thebe` when the `sphinx-thebe` initialization button is pressed.
208+
This means that no Javascript is loaded until a person explicitly tries to start thebe, which reduces page load times.
209+
210+
If you want `thebe` to be loaded on every page, in an "eager" fashion, you may do so with the following configuration:
210211

211212
```python
212213
thebe_config = {
213-
"always_load": False
214+
"always_load": True
214215
}
215216
```
216217

217218
## Configuration reference
218219

219220
Here's a reference of all of the configuration values avialable to `sphinx-thebe`.
220221
Many of these eventually make their was into the `thebe` configuration. You can
221-
find a [reference for `thebe` configuration here](https://thebelab.readthedocs.io/en/latest/config_reference.html).
222+
find a [reference for `thebe` configuration here](https://thebe.readthedocs.io/en/latest/config_reference.html).
222223

223224
```python
224225
thebe_config = {

docs/index.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111
:alt: PyPi page
1212
```
1313

14-
Make your code cells interactive with a kernel provided by [Thebe](http://thebelab.readthedocs.org/)
15-
and [Binder](https://mybinder.org).
14+
Make your code cells interactive with a kernel provided by [Thebe](http://thebe.readthedocs.org/) and [Binder](https://mybinder.org).
1615

1716
For example, click the button below. Notice that the code block beneath becomes
1817
editable and runnable!
@@ -29,7 +28,7 @@ print("hi")
2928
See [](use.md) for more information about what you can do with `sphinx-thebe`.
3029

3130
```{note}
32-
This package is a Sphinx wrapper around the excellent [thebe project](http://thebelab.readthedocs.org/),
31+
This package is a Sphinx wrapper around the excellent [thebe project](http://thebe.readthedocs.org/),
3332
a javascript tool to convert static code cells into interactive cells backed
3433
by a kernel.
3534
```

sphinx_thebe/__init__.py

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import os
55
from pathlib import Path
6+
from textwrap import dedent
67

78
from docutils.parsers.rst import Directive, directives
89
from docutils import nodes
@@ -12,7 +13,7 @@
1213

1314
logger = logging.getLogger(__name__)
1415

15-
THEBE_VERSION = "0.5.1"
16+
THEBE_VERSION = "0.8.2"
1617

1718

1819
def st_static_path(app):
@@ -21,16 +22,22 @@ def st_static_path(app):
2122

2223

2324
def init_thebe_default_config(app, env, docnames):
25+
"""Create a default config for fields that aren't given by the user."""
2426
thebe_config = app.config.thebe_config
2527
defaults = {
26-
"always_load": True,
28+
"always_load": False,
2729
"selector": ".thebe",
2830
"selector_input": "pre",
2931
"selector_output": ".output",
3032
}
3133
for key, val in defaults.items():
3234
if key not in thebe_config:
3335
thebe_config[key] = val
36+
37+
# Standardize types for certain values
38+
BOOL_KEYS = ["always_load"]
39+
for key in BOOL_KEYS:
40+
thebe_config[key] = _bool(thebe_config[key])
3441

3542

3643
def _bool(b):
@@ -42,46 +49,45 @@ def _bool(b):
4249

4350
def _do_load_thebe(doctree, config_thebe):
4451
"""Decide whether to load thebe based on the page's context."""
52+
# No doctree means there's no page content at all
4553
if not doctree:
4654
return False
4755

4856
# If we aren't properly configured
4957
if not config_thebe:
50-
logger.warning("Didn't find `thebe_config` in conf.py, add to use thebe")
58+
logger.warning("[sphinx-thebe]: Didn't find `thebe_config` in conf.py, add to use thebe")
5159
return False
60+
61+
return True
5262

53-
# Only load `thebe` if there is a thebe button somewhere
54-
if doctree.traverse(ThebeButtonNode) or _bool(config_thebe.get("always_load")):
55-
return True
56-
else:
57-
return False
5863

59-
60-
def init_thebe_core(app, pagename, templatename, context, doctree):
61-
"""Load thebe assets if there's a thebe button on this page."""
64+
def init_thebe_core(app, env, docnames):
65+
"""Add scripts to configure thebe, and optionally add thebe itself.
66+
67+
By default, defer loading the `thebe` JS bundle until bootstrap is called
68+
in order to speed up page load times.
69+
"""
6270
config_thebe = app.config["thebe_config"]
63-
if not _do_load_thebe(doctree, config_thebe):
64-
return
65-
66-
# Add core libraries
67-
opts = {"async": "async"}
68-
app.add_js_file(
69-
filename=f"https://unpkg.com/thebe@{THEBE_VERSION}/lib/index.js", **opts
70-
)
7171

7272
# Add configuration variables
73-
thebe_config = f"""
73+
THEBE_JS_URL = f"https://unpkg.com/thebe@{THEBE_VERSION}/lib/index.js"
74+
thebe_config = f"""\
75+
const THEBE_JS_URL = "{ THEBE_JS_URL }"
7476
const thebe_selector = "{ app.config.thebe_config['selector'] }"
7577
const thebe_selector_input = "{ app.config.thebe_config['selector_input'] }"
7678
const thebe_selector_output = "{ app.config.thebe_config['selector_output'] }"
7779
"""
78-
app.add_js_file(None, body=thebe_config)
79-
app.add_js_file(filename="sphinx-thebe.js", **opts)
80+
app.add_js_file(None, body=dedent(thebe_config))
81+
app.add_js_file(filename="sphinx-thebe.js", **{"async": "async"})
8082

83+
if config_thebe.get("always_load") is True:
84+
# If we've got `always load` on, then load thebe on every page.
85+
app.add_js_file(THEBE_JS_URL, **{"async": "async"})
8186

8287
def update_thebe_context(app, doctree, docname):
83-
"""Add thebe config nodes to this doctree."""
88+
"""Add thebe config nodes to this doctree using page-dependent information."""
8489
config_thebe = app.config["thebe_config"]
90+
# Skip modifying the doctree if we don't need to load thebe
8591
if not _do_load_thebe(doctree, config_thebe):
8692
return
8793

@@ -94,7 +100,6 @@ def update_thebe_context(app, doctree, docname):
94100
)
95101
codemirror_theme = config_thebe.get("codemirror-theme", "abcdef")
96102

97-
# Thebe configuration
98103
# Choose the kernel we'll use
99104
meta = app.env.metadata.get(docname, {})
100105
kernel_name = meta.get("thebe-kernel")
@@ -142,6 +147,7 @@ def update_thebe_context(app, doctree, docname):
142147
</script>
143148
"""
144149

150+
# Append to the docutils doctree so it makes it into the build outputs
145151
doctree.append(nodes.raw(text=thebe_html_config, format="html"))
146152
doctree.append(
147153
nodes.raw(text=f"<script>kernelName = '{kernel_name}'</script>", format="html")
@@ -154,7 +160,7 @@ def _split_repo_url(url):
154160
end = url.split("github.com/")[-1]
155161
org, repo = end.split("/")[:2]
156162
else:
157-
logger.warning(f"Currently Thebe repositories must be on GitHub, got {url}")
163+
logger.warning(f"[sphinx-thebe]: Currently Thebe repositories must be on GitHub, got {url}")
158164
org = repo = None
159165
return org, repo
160166

@@ -220,12 +226,12 @@ def setup(app):
220226
# Set default values for the configuration
221227
app.connect("env-before-read-docs", init_thebe_default_config)
222228

229+
# Load the JS/CSS assets for thebe if needed
230+
app.connect("env-before-read-docs", init_thebe_core)
231+
223232
# Update the doctree with thebe-specific information if needed
224233
app.connect("doctree-resolved", update_thebe_context)
225234

226-
# Load the JS/CSS assets for thebe if needed
227-
app.connect("html-page-context", init_thebe_core)
228-
229235
# configuration for this tool
230236
app.add_config_value("thebe_config", {}, "html")
231237

sphinx_thebe/_static/sphinx-thebe.js

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,11 @@
11
/**
22
* Add attributes to Thebe blocks to initialize thebe properly
33
*/
4-
5-
var initThebe = () => {
6-
// If Thebelab hasn't loaded, wait a bit and try again. This
7-
// happens because we load ClipboardJS asynchronously.
8-
if (window.thebelab === undefined) {
9-
console.log("thebe not loaded, retrying...");
10-
setTimeout(initThebe, 500)
11-
return
12-
}
13-
14-
console.log("Adding thebe to code cells...");
15-
4+
var configureThebe = () => {
165
// Load thebe config in case we want to update it as some point
6+
console.log("[sphinx-thebe]: Loading thebe config...");
177
thebe_config = $('script[type="text/x-thebe-config"]')[0]
188

19-
209
// If we already detect a Thebe cell, don't re-run
2110
if (document.querySelectorAll('div.thebe-cell').length > 0) {
2211
return;
@@ -56,8 +45,12 @@ var initThebe = () => {
5645
});
5746
}
5847
});
48+
}
5949

60-
50+
/**
51+
* Update the page DOM to use Thebe elements
52+
*/
53+
var modifyDOMForThebe = () => {
6154
// Find all code cells, replace with Thebe interactive code cells
6255
const codeCells = document.querySelectorAll(thebe_selector)
6356
codeCells.forEach((codeCell, index) => {
@@ -80,9 +73,31 @@ var initThebe = () => {
8073
}
8174
}
8275
});
76+
}
8377

84-
// Init thebe
85-
thebelab.bootstrap();
78+
var initThebe = () => {
79+
// Load thebe dynamically if it's not already loaded
80+
if (typeof thebelab === "undefined") {
81+
console.log("[sphinx-thebe]: Loading thebe from CDN...");
82+
$(".thebe-launch-button ").text("Loading thebe from CDN...");
83+
84+
const script = document.createElement('script');
85+
script.src = `${THEBE_JS_URL}`;
86+
document.head.appendChild(script);
87+
88+
// Runs once the script has finished loading
89+
script.addEventListener('load', () => {
90+
console.log("[sphinx-thebe]: Finished loading thebe from CDN...");
91+
configureThebe();
92+
modifyDOMForThebe();
93+
thebelab.bootstrap();
94+
});
95+
} else {
96+
console.log("[sphinx-thebe]: thebe already loaded, not loading from CDN...");
97+
configureThebe();
98+
modifyDOMForThebe();
99+
thebelab.bootstrap();
100+
}
86101
}
87102

88103
// Helper function to munge the language name

tests/test_build.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ class SphinxBuild:
1818
path_html = path_build.joinpath("html")
1919
path_pg_index = path_html.joinpath("index.html")
2020
path_pg_config = path_html.joinpath("configure.html")
21-
path_pg_chglg = path_html.joinpath("changelog.html")
2221
cmd_base = ["sphinx-build", ".", "_build/html", "-a", "-W"]
2322

2423
def copy(self, path=None):
@@ -70,25 +69,23 @@ def test_sphinx_thebe(file_regression, sphinx_build):
7069
lb_text = "\n\n".join([ii.prettify() for ii in launch_buttons])
7170
file_regression.check(lb_text, basename="launch_buttons", extension=".html")
7271

73-
# Changelog has no thebe button directive, but should have the JS anyway
74-
soup_chlg = BeautifulSoup(
75-
Path(sphinx_build.path_pg_chglg).read_text(), "html.parser"
76-
)
77-
assert "https://unpkg.com/thebe" in soup_chlg.prettify()
78-
7972

80-
def test_always_load(file_regression, sphinx_build):
73+
def test_lazy_load(file_regression, sphinx_build):
8174
"""Test building with thebe."""
8275
sphinx_build.copy()
76+
url = "https://unpkg.com/thebe@0.8.2/lib/index.js" # URL to search for
8377

84-
# Basic build with defaults
85-
sphinx_build.build(cmd=["-D", "thebe_config.always_load=false"])
78+
# Thebe JS should not be loaded by default (is loaded lazily)
79+
sphinx_build.build()
80+
soup_ix = BeautifulSoup(Path(sphinx_build.path_pg_index).read_text(), "html.parser")
81+
sources = [ii.attrs.get("src") for ii in soup_ix.select("script")]
82+
thebe_source = [ii for ii in sources if ii == url]
83+
assert len(thebe_source) == 0
8684

87-
# Thebe should be loaded on a page *with* the directive and not on pages w/o it
85+
# always_load=True should force this script to load on all pages
86+
sphinx_build.build(cmd=["-D", "thebe_config.always_load=true"])
8887
soup_ix = BeautifulSoup(Path(sphinx_build.path_pg_index).read_text(), "html.parser")
89-
assert "https://unpkg.com/thebe" in soup_ix.prettify()
90-
# Changelog has no thebe button directive, so shouldn't have JS
91-
soup_chlg = BeautifulSoup(
92-
Path(sphinx_build.path_pg_chglg).read_text(), "html.parser"
93-
)
94-
assert "https://unpkg.com/thebe" not in soup_chlg.prettify()
88+
sources = [ii.attrs.get("src") for ii in soup_ix.select("script")]
89+
thebe_source = [ii for ii in sources if ii == url]
90+
assert len(thebe_source) == 1
91+

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ envlist = py37-sphinx3
1616
[testenv]
1717
usedevelop = true
1818

19-
[testenv:py{36,37,38}-sphinx{2,3}]
19+
[testenv:py{36,37,38,39}-sphinx{3,4}]
2020
extras = sphinx,testing
2121
deps =
2222
sphinx3: sphinx>=3,<4

0 commit comments

Comments
 (0)