diff --git a/README.md b/README.md index 4b52c0c..c5410ec 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Documentation](https://readthedocs.org/projects/sphinx-thebe/badge/?version=latest)](https://sphinx-thebe.readthedocs.io/en/latest/?badge=latest) [![PyPI](https://img.shields.io/pypi/v/sphinx-thebe.svg)](https://pypi.org/project/sphinx-thebe) -Integrate interactive code blocks into your documentation with Thebelab and Binder. +Integrate interactive code blocks into your documentation with Thebe and Binder. See [the sphinx-thebe documentation](https://sphinx-thebe.readthedocs.io/en/latest/) for more details! diff --git a/docs/conf.py b/docs/conf.py index 4171c53..30bbac4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,7 +47,8 @@ # "selector": ".thebe", # "selector_input": , # "selector_output": , - # "codemirror-theme": "blackboard" # Doesn't currently work + # "codemirror-theme": "blackboard", # Doesn't currently work + # "always_load": True, # To load thebe on every page } myst_enable_extensions = ["colon_fence"] @@ -86,6 +87,7 @@ # a list of builtin themes. # html_theme = "sphinx_book_theme" +html_title = "sphinx-thebe" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/configure.md b/docs/configure.md index 875d869..25ce467 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -202,15 +202,16 @@ thebe_config = { See [the CodeMirror theme demo](https://codemirror.net/demo/theme.html) for a list of themes that you can use, and what they look like. -## Only load JS on certain pages +## Load `thebe` automatically on all pages -By default, `sphinx-thebe` will load the JS/CSS from `thebe` on all of your documentation's pages. -Alternatively, you may load `thebe` only on pages that use the `thebe-button` directive. -To do so, use the following configuration: +By default, `sphinx-thebe` will lazily load the JS/CSS from `thebe` when the `sphinx-thebe` initialization button is pressed. +This means that no Javascript is loaded until a person explicitly tries to start thebe, which reduces page load times. + +If you want `thebe` to be loaded on every page, in an "eager" fashion, you may do so with the following configuration: ```python thebe_config = { - "always_load": False + "always_load": True } ``` @@ -218,7 +219,7 @@ thebe_config = { Here's a reference of all of the configuration values avialable to `sphinx-thebe`. Many of these eventually make their was into the `thebe` configuration. You can -find a [reference for `thebe` configuration here](https://thebelab.readthedocs.io/en/latest/config_reference.html). +find a [reference for `thebe` configuration here](https://thebe.readthedocs.io/en/latest/config_reference.html). ```python thebe_config = { diff --git a/docs/index.md b/docs/index.md index 92b710e..e9b74d7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,8 +11,7 @@ :alt: PyPi page ``` -Make your code cells interactive with a kernel provided by [Thebe](http://thebelab.readthedocs.org/) -and [Binder](https://mybinder.org). +Make your code cells interactive with a kernel provided by [Thebe](http://thebe.readthedocs.org/) and [Binder](https://mybinder.org). For example, click the button below. Notice that the code block beneath becomes editable and runnable! @@ -29,7 +28,7 @@ print("hi") See [](use.md) for more information about what you can do with `sphinx-thebe`. ```{note} -This package is a Sphinx wrapper around the excellent [thebe project](http://thebelab.readthedocs.org/), +This package is a Sphinx wrapper around the excellent [thebe project](http://thebe.readthedocs.org/), a javascript tool to convert static code cells into interactive cells backed by a kernel. ``` diff --git a/sphinx_thebe/__init__.py b/sphinx_thebe/__init__.py index b7fde2c..0ed526d 100644 --- a/sphinx_thebe/__init__.py +++ b/sphinx_thebe/__init__.py @@ -3,6 +3,7 @@ import json import os from pathlib import Path +from textwrap import dedent from docutils.parsers.rst import Directive, directives from docutils import nodes @@ -12,7 +13,7 @@ logger = logging.getLogger(__name__) -THEBE_VERSION = "0.5.1" +THEBE_VERSION = "0.8.2" def st_static_path(app): @@ -21,9 +22,10 @@ def st_static_path(app): def init_thebe_default_config(app, env, docnames): + """Create a default config for fields that aren't given by the user.""" thebe_config = app.config.thebe_config defaults = { - "always_load": True, + "always_load": False, "selector": ".thebe", "selector_input": "pre", "selector_output": ".output", @@ -31,6 +33,11 @@ def init_thebe_default_config(app, env, docnames): for key, val in defaults.items(): if key not in thebe_config: thebe_config[key] = val + + # Standardize types for certain values + BOOL_KEYS = ["always_load"] + for key in BOOL_KEYS: + thebe_config[key] = _bool(thebe_config[key]) def _bool(b): @@ -42,46 +49,45 @@ def _bool(b): def _do_load_thebe(doctree, config_thebe): """Decide whether to load thebe based on the page's context.""" + # No doctree means there's no page content at all if not doctree: return False # If we aren't properly configured if not config_thebe: - logger.warning("Didn't find `thebe_config` in conf.py, add to use thebe") + logger.warning("[sphinx-thebe]: Didn't find `thebe_config` in conf.py, add to use thebe") return False + + return True - # Only load `thebe` if there is a thebe button somewhere - if doctree.traverse(ThebeButtonNode) or _bool(config_thebe.get("always_load")): - return True - else: - return False - -def init_thebe_core(app, pagename, templatename, context, doctree): - """Load thebe assets if there's a thebe button on this page.""" +def init_thebe_core(app, env, docnames): + """Add scripts to configure thebe, and optionally add thebe itself. + + By default, defer loading the `thebe` JS bundle until bootstrap is called + in order to speed up page load times. + """ config_thebe = app.config["thebe_config"] - if not _do_load_thebe(doctree, config_thebe): - return - - # Add core libraries - opts = {"async": "async"} - app.add_js_file( - filename=f"https://unpkg.com/thebe@{THEBE_VERSION}/lib/index.js", **opts - ) # Add configuration variables - thebe_config = f""" + THEBE_JS_URL = f"https://unpkg.com/thebe@{THEBE_VERSION}/lib/index.js" + thebe_config = f"""\ + const THEBE_JS_URL = "{ THEBE_JS_URL }" const thebe_selector = "{ app.config.thebe_config['selector'] }" const thebe_selector_input = "{ app.config.thebe_config['selector_input'] }" const thebe_selector_output = "{ app.config.thebe_config['selector_output'] }" """ - app.add_js_file(None, body=thebe_config) - app.add_js_file(filename="sphinx-thebe.js", **opts) + app.add_js_file(None, body=dedent(thebe_config)) + app.add_js_file(filename="sphinx-thebe.js", **{"async": "async"}) + if config_thebe.get("always_load") is True: + # If we've got `always load` on, then load thebe on every page. + app.add_js_file(THEBE_JS_URL, **{"async": "async"}) def update_thebe_context(app, doctree, docname): - """Add thebe config nodes to this doctree.""" + """Add thebe config nodes to this doctree using page-dependent information.""" config_thebe = app.config["thebe_config"] + # Skip modifying the doctree if we don't need to load thebe if not _do_load_thebe(doctree, config_thebe): return @@ -94,7 +100,6 @@ def update_thebe_context(app, doctree, docname): ) codemirror_theme = config_thebe.get("codemirror-theme", "abcdef") - # Thebe configuration # Choose the kernel we'll use meta = app.env.metadata.get(docname, {}) kernel_name = meta.get("thebe-kernel") @@ -142,6 +147,7 @@ def update_thebe_context(app, doctree, docname): """ + # Append to the docutils doctree so it makes it into the build outputs doctree.append(nodes.raw(text=thebe_html_config, format="html")) doctree.append( nodes.raw(text=f"", format="html") @@ -154,7 +160,7 @@ def _split_repo_url(url): end = url.split("github.com/")[-1] org, repo = end.split("/")[:2] else: - logger.warning(f"Currently Thebe repositories must be on GitHub, got {url}") + logger.warning(f"[sphinx-thebe]: Currently Thebe repositories must be on GitHub, got {url}") org = repo = None return org, repo @@ -220,12 +226,12 @@ def setup(app): # Set default values for the configuration app.connect("env-before-read-docs", init_thebe_default_config) + # Load the JS/CSS assets for thebe if needed + app.connect("env-before-read-docs", init_thebe_core) + # Update the doctree with thebe-specific information if needed app.connect("doctree-resolved", update_thebe_context) - # Load the JS/CSS assets for thebe if needed - app.connect("html-page-context", init_thebe_core) - # configuration for this tool app.add_config_value("thebe_config", {}, "html") diff --git a/sphinx_thebe/_static/sphinx-thebe.js b/sphinx_thebe/_static/sphinx-thebe.js index 4842c44..26b71d8 100644 --- a/sphinx_thebe/_static/sphinx-thebe.js +++ b/sphinx_thebe/_static/sphinx-thebe.js @@ -1,22 +1,11 @@ /** * Add attributes to Thebe blocks to initialize thebe properly */ - -var initThebe = () => { - // If Thebelab hasn't loaded, wait a bit and try again. This - // happens because we load ClipboardJS asynchronously. - if (window.thebelab === undefined) { - console.log("thebe not loaded, retrying..."); - setTimeout(initThebe, 500) - return - } - - console.log("Adding thebe to code cells..."); - +var configureThebe = () => { // Load thebe config in case we want to update it as some point + console.log("[sphinx-thebe]: Loading thebe config..."); thebe_config = $('script[type="text/x-thebe-config"]')[0] - // If we already detect a Thebe cell, don't re-run if (document.querySelectorAll('div.thebe-cell').length > 0) { return; @@ -56,8 +45,12 @@ var initThebe = () => { }); } }); +} - +/** + * Update the page DOM to use Thebe elements + */ +var modifyDOMForThebe = () => { // Find all code cells, replace with Thebe interactive code cells const codeCells = document.querySelectorAll(thebe_selector) codeCells.forEach((codeCell, index) => { @@ -80,9 +73,31 @@ var initThebe = () => { } } }); +} - // Init thebe - thebelab.bootstrap(); +var initThebe = () => { + // Load thebe dynamically if it's not already loaded + if (typeof thebelab === "undefined") { + console.log("[sphinx-thebe]: Loading thebe from CDN..."); + $(".thebe-launch-button ").text("Loading thebe from CDN..."); + + const script = document.createElement('script'); + script.src = `${THEBE_JS_URL}`; + document.head.appendChild(script); + + // Runs once the script has finished loading + script.addEventListener('load', () => { + console.log("[sphinx-thebe]: Finished loading thebe from CDN..."); + configureThebe(); + modifyDOMForThebe(); + thebelab.bootstrap(); + }); + } else { + console.log("[sphinx-thebe]: thebe already loaded, not loading from CDN..."); + configureThebe(); + modifyDOMForThebe(); + thebelab.bootstrap(); + } } // Helper function to munge the language name diff --git a/tests/test_build.py b/tests/test_build.py index b638633..40dcd28 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -18,7 +18,6 @@ class SphinxBuild: path_html = path_build.joinpath("html") path_pg_index = path_html.joinpath("index.html") path_pg_config = path_html.joinpath("configure.html") - path_pg_chglg = path_html.joinpath("changelog.html") cmd_base = ["sphinx-build", ".", "_build/html", "-a", "-W"] def copy(self, path=None): @@ -70,25 +69,23 @@ def test_sphinx_thebe(file_regression, sphinx_build): lb_text = "\n\n".join([ii.prettify() for ii in launch_buttons]) file_regression.check(lb_text, basename="launch_buttons", extension=".html") - # Changelog has no thebe button directive, but should have the JS anyway - soup_chlg = BeautifulSoup( - Path(sphinx_build.path_pg_chglg).read_text(), "html.parser" - ) - assert "https://unpkg.com/thebe" in soup_chlg.prettify() - -def test_always_load(file_regression, sphinx_build): +def test_lazy_load(file_regression, sphinx_build): """Test building with thebe.""" sphinx_build.copy() + url = "https://unpkg.com/thebe@0.8.2/lib/index.js" # URL to search for - # Basic build with defaults - sphinx_build.build(cmd=["-D", "thebe_config.always_load=false"]) + # Thebe JS should not be loaded by default (is loaded lazily) + sphinx_build.build() + soup_ix = BeautifulSoup(Path(sphinx_build.path_pg_index).read_text(), "html.parser") + sources = [ii.attrs.get("src") for ii in soup_ix.select("script")] + thebe_source = [ii for ii in sources if ii == url] + assert len(thebe_source) == 0 - # Thebe should be loaded on a page *with* the directive and not on pages w/o it + # always_load=True should force this script to load on all pages + sphinx_build.build(cmd=["-D", "thebe_config.always_load=true"]) soup_ix = BeautifulSoup(Path(sphinx_build.path_pg_index).read_text(), "html.parser") - assert "https://unpkg.com/thebe" in soup_ix.prettify() - # Changelog has no thebe button directive, so shouldn't have JS - soup_chlg = BeautifulSoup( - Path(sphinx_build.path_pg_chglg).read_text(), "html.parser" - ) - assert "https://unpkg.com/thebe" not in soup_chlg.prettify() + sources = [ii.attrs.get("src") for ii in soup_ix.select("script")] + thebe_source = [ii for ii in sources if ii == url] + assert len(thebe_source) == 1 + diff --git a/tox.ini b/tox.ini index aedf699..f40241c 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ envlist = py37-sphinx3 [testenv] usedevelop = true -[testenv:py{36,37,38}-sphinx{2,3}] +[testenv:py{36,37,38,39}-sphinx{3,4}] extras = sphinx,testing deps = sphinx3: sphinx>=3,<4