From 0e33c8221c121195527dc484e9a8ad5b2f2e10d7 Mon Sep 17 00:00:00 2001 From: Chris Holdgraf Date: Sun, 2 Jan 2022 18:17:44 -0800 Subject: [PATCH 1/6] ENH: Lazy load thebe javascript --- README.md | 2 +- docs/configure.md | 2 +- docs/index.md | 5 +- sphinx_thebe/__init__.py | 15 ++- sphinx_thebe/_static/sphinx-thebe.js | 141 +++++++++++++-------------- 5 files changed, 81 insertions(+), 84 deletions(-) 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/configure.md b/docs/configure.md index 875d869..5e5cf5b 100644 --- a/docs/configure.md +++ b/docs/configure.md @@ -218,7 +218,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..e62cf14 100644 --- a/sphinx_thebe/__init__.py +++ b/sphinx_thebe/__init__.py @@ -58,25 +58,24 @@ def _do_load_thebe(doctree, config_thebe): def init_thebe_core(app, pagename, templatename, context, doctree): - """Load thebe assets if there's a thebe button on this page.""" + """Load thebe assets if there's a thebe button on this page. + + We defer loading the `thebe` javascript 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""" + const THEBE_VERSION = "{ THEBE_VERSION }" 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(filename="sphinx-thebe.js", **{"async": "async"}) def update_thebe_context(app, doctree, docname): diff --git a/sphinx_thebe/_static/sphinx-thebe.js b/sphinx_thebe/_static/sphinx-thebe.js index 4842c44..b2f6773 100644 --- a/sphinx_thebe/_static/sphinx-thebe.js +++ b/sphinx_thebe/_static/sphinx-thebe.js @@ -3,86 +3,85 @@ */ 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..."); - - // Load thebe config in case we want to update it as some point - 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; - } - - // Update thebe buttons with loading message - $(".thebe-launch-button").each((ii, button) => { - button.innerHTML = ` -
-
-
-
-
-
- `; - }) - - // Set thebe event hooks - var thebeStatus; - thebelab.on("status", function (evt, data) { - console.log("Status changed:", data.status, data.message); - - $(".thebe-launch-button ") - .removeClass("thebe-status-" + thebeStatus) - .addClass("thebe-status-" + data.status) - .find(".loading-text").html("Launching from mybinder.org: " + data.status + ""); - - // Now update our thebe status - thebeStatus = data.status; - - // Find any cells with an initialization tag and ask thebe to run them when ready - if (data.status === "ready") { - var thebeInitCells = document.querySelectorAll('.thebe-init, .tag_thebe-init'); - thebeInitCells.forEach((cell) => { - console.log("Initializing Thebe with cell: " + cell.id); - cell.querySelector('.thebelab-run-button').click(); - }); + // Load thebe dynamically so we can reduce page size + console.log("[sphinx-thebe]: Loading thebe from CDN..."); + const script = document.createElement('script'); + script.src = `https://unpkg.com/thebe@${THEBE_VERSION}/lib/index.js`; + document.head.appendChild(script); + + // Runs once the script has finished loading + script.addEventListener('load', () => { + // Load thebe config in case we want to update it as some point + console.log("[sphinx-thebe]: Initializing thebe..."); + 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; } - }); + + // Update thebe buttons with loading message + $(".thebe-launch-button").each((ii, button) => { + button.innerHTML = ` +
+
+
+
+
+
+ `; + }) + + // Set thebe event hooks + var thebeStatus; + thebe.on("status", function (evt, data) { + console.log("Status changed:", data.status, data.message); + + $(".thebe-launch-button ") + .removeClass("thebe-status-" + thebeStatus) + .addClass("thebe-status-" + data.status) + .find(".loading-text").html("Launching from mybinder.org: " + data.status + ""); + + // Now update our thebe status + thebeStatus = data.status; + + // Find any cells with an initialization tag and ask thebe to run them when ready + if (data.status === "ready") { + var thebeInitCells = document.querySelectorAll('.thebe-init, .tag_thebe-init'); + thebeInitCells.forEach((cell) => { + console.log("Initializing Thebe with cell: " + cell.id); + cell.querySelector('.thebelab-run-button').click(); + }); + } + }); - // Find all code cells, replace with Thebe interactive code cells - const codeCells = document.querySelectorAll(thebe_selector) - codeCells.forEach((codeCell, index) => { - const codeCellId = index => `codecell${index}`; - codeCell.id = codeCellId(index); - codeCellText = codeCell.querySelector(thebe_selector_input); - codeCellOutput = codeCell.querySelector(thebe_selector_output); + // Find all code cells, replace with Thebe interactive code cells + const codeCells = document.querySelectorAll(thebe_selector) + codeCells.forEach((codeCell, index) => { + const codeCellId = index => `codecell${index}`; + codeCell.id = codeCellId(index); + codeCellText = codeCell.querySelector(thebe_selector_input); + codeCellOutput = codeCell.querySelector(thebe_selector_output); - // Clean up the language to make it work w/ CodeMirror and add it to the cell - dataLanguage = detectLanguage(kernelName); + // Clean up the language to make it work w/ CodeMirror and add it to the cell + dataLanguage = detectLanguage(kernelName); - if (codeCellText) { - codeCellText.setAttribute('data-language', dataLanguage); - codeCellText.setAttribute('data-executable', 'true'); + if (codeCellText) { + codeCellText.setAttribute('data-language', dataLanguage); + codeCellText.setAttribute('data-executable', 'true'); - // If we had an output, insert it just after the `pre` cell - if (codeCellOutput) { - $(codeCellOutput).attr("data-output", ""); - $(codeCellOutput).insertAfter(codeCellText); + // If we had an output, insert it just after the `pre` cell + if (codeCellOutput) { + $(codeCellOutput).attr("data-output", ""); + $(codeCellOutput).insertAfter(codeCellText); + } } - } - }); + }); // Init thebe - thebelab.bootstrap(); + thebe.bootstrap(); + }); } // Helper function to munge the language name From 378089b37d9c4cc7b0e37ead7dceb06390c4d356 Mon Sep 17 00:00:00 2001 From: Chris Holdgraf Date: Sun, 2 Jan 2022 18:32:38 -0800 Subject: [PATCH 2/6] Update thebe version --- sphinx_thebe/__init__.py | 2 +- sphinx_thebe/_static/sphinx-thebe.js | 141 ++++++++++++++------------- 2 files changed, 74 insertions(+), 69 deletions(-) diff --git a/sphinx_thebe/__init__.py b/sphinx_thebe/__init__.py index e62cf14..7e8f2fa 100644 --- a/sphinx_thebe/__init__.py +++ b/sphinx_thebe/__init__.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) -THEBE_VERSION = "0.5.1" +THEBE_VERSION = "0.8.2" def st_static_path(app): diff --git a/sphinx_thebe/_static/sphinx-thebe.js b/sphinx_thebe/_static/sphinx-thebe.js index b2f6773..358902c 100644 --- a/sphinx_thebe/_static/sphinx-thebe.js +++ b/sphinx_thebe/_static/sphinx-thebe.js @@ -1,86 +1,91 @@ /** * Add attributes to Thebe blocks to initialize thebe properly */ +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] -var initThebe = () => { - // Load thebe dynamically so we can reduce page size - console.log("[sphinx-thebe]: Loading thebe from CDN..."); - const script = document.createElement('script'); - script.src = `https://unpkg.com/thebe@${THEBE_VERSION}/lib/index.js`; - document.head.appendChild(script); - - // Runs once the script has finished loading - script.addEventListener('load', () => { - // Load thebe config in case we want to update it as some point - console.log("[sphinx-thebe]: Initializing thebe..."); - 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; - } - - // Update thebe buttons with loading message - $(".thebe-launch-button").each((ii, button) => { - button.innerHTML = ` -
-
-
-
-
-
- `; - }) + // If we already detect a Thebe cell, don't re-run + if (document.querySelectorAll('div.thebe-cell').length > 0) { + return; + } - // Set thebe event hooks - var thebeStatus; - thebe.on("status", function (evt, data) { - console.log("Status changed:", data.status, data.message); + // Update thebe buttons with loading message + $(".thebe-launch-button").each((ii, button) => { + button.innerHTML = ` +
+
+
+
+
+
+ `; + }) - $(".thebe-launch-button ") - .removeClass("thebe-status-" + thebeStatus) - .addClass("thebe-status-" + data.status) - .find(".loading-text").html("Launching from mybinder.org: " + data.status + ""); + // Set thebe event hooks + var thebeStatus; + thebelab.on("status", function (evt, data) { + console.log("Status changed:", data.status, data.message); - // Now update our thebe status - thebeStatus = data.status; + $(".thebe-launch-button ") + .removeClass("thebe-status-" + thebeStatus) + .addClass("thebe-status-" + data.status) + .find(".loading-text").html("Launching from mybinder.org: " + data.status + ""); - // Find any cells with an initialization tag and ask thebe to run them when ready - if (data.status === "ready") { - var thebeInitCells = document.querySelectorAll('.thebe-init, .tag_thebe-init'); - thebeInitCells.forEach((cell) => { - console.log("Initializing Thebe with cell: " + cell.id); - cell.querySelector('.thebelab-run-button').click(); - }); - } - }); + // Now update our thebe status + thebeStatus = data.status; + // Find any cells with an initialization tag and ask thebe to run them when ready + if (data.status === "ready") { + var thebeInitCells = document.querySelectorAll('.thebe-init, .tag_thebe-init'); + thebeInitCells.forEach((cell) => { + console.log("Initializing Thebe with cell: " + cell.id); + cell.querySelector('.thebelab-run-button').click(); + }); + } + }); +} - // Find all code cells, replace with Thebe interactive code cells - const codeCells = document.querySelectorAll(thebe_selector) - codeCells.forEach((codeCell, index) => { - const codeCellId = index => `codecell${index}`; - codeCell.id = codeCellId(index); - codeCellText = codeCell.querySelector(thebe_selector_input); - codeCellOutput = codeCell.querySelector(thebe_selector_output); +/** + * 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) => { + const codeCellId = index => `codecell${index}`; + codeCell.id = codeCellId(index); + codeCellText = codeCell.querySelector(thebe_selector_input); + codeCellOutput = codeCell.querySelector(thebe_selector_output); - // Clean up the language to make it work w/ CodeMirror and add it to the cell - dataLanguage = detectLanguage(kernelName); + // Clean up the language to make it work w/ CodeMirror and add it to the cell + dataLanguage = detectLanguage(kernelName); - if (codeCellText) { - codeCellText.setAttribute('data-language', dataLanguage); - codeCellText.setAttribute('data-executable', 'true'); + if (codeCellText) { + codeCellText.setAttribute('data-language', dataLanguage); + codeCellText.setAttribute('data-executable', 'true'); - // If we had an output, insert it just after the `pre` cell - if (codeCellOutput) { - $(codeCellOutput).attr("data-output", ""); - $(codeCellOutput).insertAfter(codeCellText); - } + // If we had an output, insert it just after the `pre` cell + if (codeCellOutput) { + $(codeCellOutput).attr("data-output", ""); + $(codeCellOutput).insertAfter(codeCellText); } - }); + } + }); +} - // Init thebe - thebe.bootstrap(); +var initThebe = () => { + // Load thebe dynamically so we can reduce page size + console.log("[sphinx-thebe]: Loading thebe from CDN..."); + const script = document.createElement('script'); + script.src = `https://unpkg.com/thebe@${THEBE_VERSION}/lib/index.js`; + document.head.appendChild(script); + // Runs once the script has finished loading + script.addEventListener('load', () => { + configureThebe(); + modifyDOMForThebe(); + thebelab.bootstrap(); }); } From d34a3049994dd25c154ed06259fd9f956f6fff2f Mon Sep 17 00:00:00 2001 From: Chris Holdgraf Date: Sun, 2 Jan 2022 18:33:57 -0800 Subject: [PATCH 3/6] fix test --- tests/test_build.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_build.py b/tests/test_build.py index b638633..5e9fc31 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -70,11 +70,11 @@ 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 + # Make sure thebe JS is not on pages (it should be loaded dynamically) soup_chlg = BeautifulSoup( Path(sphinx_build.path_pg_chglg).read_text(), "html.parser" ) - assert "https://unpkg.com/thebe" in soup_chlg.prettify() + assert "https://unpkg.com/thebe" not in soup_chlg.prettify() def test_always_load(file_regression, sphinx_build): From dfc808c457b0ebee4e0cdf5be23a66491697e06c Mon Sep 17 00:00:00 2001 From: Chris Holdgraf Date: Mon, 3 Jan 2022 12:06:20 -0800 Subject: [PATCH 4/6] Fixing up JS code --- docs/conf.py | 4 ++- docs/configure.md | 11 ++++--- sphinx_thebe/__init__.py | 49 ++++++++++++++++------------ sphinx_thebe/_static/sphinx-thebe.js | 26 ++++++++++----- tests/test_build.py | 11 ++----- tox.ini | 2 +- 6 files changed, 59 insertions(+), 44 deletions(-) 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 5e5cf5b..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 } ``` diff --git a/sphinx_thebe/__init__.py b/sphinx_thebe/__init__.py index 7e8f2fa..a6b339e 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 @@ -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,45 +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") - return False - - # Only load `thebe` if there is a thebe button somewhere - if doctree.traverse(ThebeButtonNode) or _bool(config_thebe.get("always_load")): - return True - else: + logger.warning("[sphinx-thebe]: Didn't find `thebe_config` in conf.py, add to use thebe") return False + + return True -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. - We defer loading the `thebe` javascript bundle until bootstrap is called + 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 configuration variables - thebe_config = f""" + thebe_config = f"""\ const THEBE_VERSION = "{ THEBE_VERSION }" 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(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. + thebejs = f"https://unpkg.com/thebe@{THEBE_VERSION}/lib/index.js" + app.add_js_file(thebejs) 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 @@ -93,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") @@ -141,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") @@ -153,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 @@ -219,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 358902c..248a687 100644 --- a/sphinx_thebe/_static/sphinx-thebe.js +++ b/sphinx_thebe/_static/sphinx-thebe.js @@ -76,17 +76,27 @@ var modifyDOMForThebe = () => { } var initThebe = () => { - // Load thebe dynamically so we can reduce page size - console.log("[sphinx-thebe]: Loading thebe from CDN..."); - const script = document.createElement('script'); - script.src = `https://unpkg.com/thebe@${THEBE_VERSION}/lib/index.js`; - document.head.appendChild(script); - // Runs once the script has finished loading - script.addEventListener('load', () => { + // 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 = `https://unpkg.com/thebe@${THEBE_VERSION}/lib/index.js`; + 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 5e9fc31..71bbe38 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -70,7 +70,7 @@ 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") - # Make sure thebe JS is not on pages (it should be loaded dynamically) + # Thebe JS should not be loaded by default (is loaded lazily) soup_chlg = BeautifulSoup( Path(sphinx_build.path_pg_chglg).read_text(), "html.parser" ) @@ -82,13 +82,8 @@ def test_always_load(file_regression, sphinx_build): sphinx_build.copy() # Basic build with defaults - sphinx_build.build(cmd=["-D", "thebe_config.always_load=false"]) + sphinx_build.build(cmd=["-D", "thebe_config.always_load=true"]) - # Thebe should be loaded on a page *with* the directive and not on pages w/o it + # Thebe should not 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() 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 From 1494b300d6b6f202bcd0db3482d686b0ceed485c Mon Sep 17 00:00:00 2001 From: Chris Holdgraf Date: Mon, 3 Jan 2022 12:15:07 -0800 Subject: [PATCH 5/6] Rearranging URL load --- sphinx_thebe/__init__.py | 6 +++--- sphinx_thebe/_static/sphinx-thebe.js | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/sphinx_thebe/__init__.py b/sphinx_thebe/__init__.py index a6b339e..0ed526d 100644 --- a/sphinx_thebe/__init__.py +++ b/sphinx_thebe/__init__.py @@ -70,8 +70,9 @@ def init_thebe_core(app, env, docnames): config_thebe = app.config["thebe_config"] # Add configuration variables + THEBE_JS_URL = f"https://unpkg.com/thebe@{THEBE_VERSION}/lib/index.js" thebe_config = f"""\ - const THEBE_VERSION = "{ THEBE_VERSION }" + 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'] }" @@ -81,8 +82,7 @@ def init_thebe_core(app, env, docnames): if config_thebe.get("always_load") is True: # If we've got `always load` on, then load thebe on every page. - thebejs = f"https://unpkg.com/thebe@{THEBE_VERSION}/lib/index.js" - app.add_js_file(thebejs) + app.add_js_file(THEBE_JS_URL, **{"async": "async"}) def update_thebe_context(app, doctree, docname): """Add thebe config nodes to this doctree using page-dependent information.""" diff --git a/sphinx_thebe/_static/sphinx-thebe.js b/sphinx_thebe/_static/sphinx-thebe.js index 248a687..26b71d8 100644 --- a/sphinx_thebe/_static/sphinx-thebe.js +++ b/sphinx_thebe/_static/sphinx-thebe.js @@ -80,8 +80,9 @@ var initThebe = () => { 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 = `https://unpkg.com/thebe@${THEBE_VERSION}/lib/index.js`; + script.src = `${THEBE_JS_URL}`; document.head.appendChild(script); // Runs once the script has finished loading From 4517397f53e06ebc3e8dc9036954b9de18c05491 Mon Sep 17 00:00:00 2001 From: Chris Holdgraf Date: Wed, 5 Jan 2022 17:08:29 -0800 Subject: [PATCH 6/6] Fix test --- tests/test_build.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/test_build.py b/tests/test_build.py index 71bbe38..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,20 +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") - # Thebe JS should not be loaded by default (is loaded lazily) - soup_chlg = BeautifulSoup( - Path(sphinx_build.path_pg_chglg).read_text(), "html.parser" - ) - assert "https://unpkg.com/thebe" not 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=true"]) + # 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 not + # 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() + 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 +